HASH 3f3defb760c2
DATE 2026-07-16
SUBJECT web: harden the frame pipeline
FILES 16 CHANGED
HASH 3f3defb760c2
DATE 2026-07-16
SUBJECT web: harden the frame pipeline
FILES 16 CHANGED
┌─ ooknet-design/package-lock.json ──────────────────────────────────────────┐
│ diff --git a/ooknet-design/package-lock.json b/ooknet-design/package-lock.json │
│ index ee26c9a..3482424 100644 │
│ --- a/ooknet-design/package-lock.json │
│ +++ b/ooknet-design/package-lock.json │
│ @@ -14,7 +14,8 @@ │
│ "fuse.js": "^7.3.0" │
│ }, │
│ "devDependencies": { │
│ - "sass": "^1.99.0" │
│ + "sass": "^1.99.0", │
│ + "vitest": "^4.1.10" │
│ }, │
│ "engines": { │
│ "node": ">=22.12.0" │
│ @@ -1527,6 +1528,24 @@ │
│ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzG │
│ eUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", │
│ "license": "MIT" │
│ }, │
│ + "node_modules/@standard-schema/spec": { │
│ + "version": "1.1.0", │
│ + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0. │
│ tgz", │
│ + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU │
│ 4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", │
│ + "dev": true, │
│ + "license": "MIT" │
│ + }, │
│ + "node_modules/@types/chai": { │
│ + "version": "5.2.3", │
│ + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", │
│ + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEH │
│ bS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@types/deep-eql": "*", │
│ + "assertion-error": "^2.0.1" │
│ + } │
│ + }, │
│ "node_modules/@types/debug": { │
│ "version": "4.1.13", │
│ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", │
│ @@ -1536,6 +1555,13 @@ │
│ "@types/ms": "*" │
│ } │
│ }, │
│ + "node_modules/@types/deep-eql": { │
│ + "version": "4.0.2", │
│ + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tg │
│ z", │
│ + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk54 │
│ 1lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", │
│ + "dev": true, │
│ + "license": "MIT" │
│ + }, │
│ "node_modules/@types/estree": { │
│ "version": "1.0.8", │
│ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", │
│ @@ -1587,6 +1613,129 @@ │
│ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1Ic │
│ nQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", │
│ "license": "ISC" │
│ }, │
│ + "node_modules/@vitest/expect": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz" │
│ , │
│ + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+D │
│ ASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@standard-schema/spec": "^1.1.0", │
│ + "@types/chai": "^5.2.2", │
│ + "@vitest/spy": "4.1.10", │
│ + "@vitest/utils": "4.1.10", │
│ + "chai": "^6.2.2", │
│ + "tinyrainbow": "^3.1.0" │
│ + }, │
│ + "funding": { │
│ + "url": "https://opencollective.com/vitest" │
│ + } │
│ + }, │
│ + "node_modules/@vitest/mocker": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz" │
│ , │
│ + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxa │
│ YURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@vitest/spy": "4.1.10", │
│ + "estree-walker": "^3.0.3", │
│ + "magic-string": "^0.30.21" │
│ + }, │
│ + "funding": { │
│ + "url": "https://opencollective.com/vitest" │
│ + }, │
│ + "peerDependencies": { │
│ + "msw": "^2.4.9", │
│ + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" │
│ + }, │
│ + "peerDependenciesMeta": { │
│ + "msw": { │
│ + "optional": true │
│ + }, │
│ + "vite": { │
│ + "optional": true │
│ + } │
│ + } │
│ + }, │
│ + "node_modules/@vitest/mocker/node_modules/estree-walker": { │
│ + "version": "3.0.3", │
│ + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3 │
│ .tgz", │
│ + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35 │
│ LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@types/estree": "^1.0.0" │
│ + } │
│ + }, │
│ + "node_modules/@vitest/pretty-format": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-form │
│ at-4.1.10.tgz", │
│ + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gko │
│ pdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "tinyrainbow": "^3.1.0" │
│ + }, │
│ + "funding": { │
│ + "url": "https://opencollective.com/vitest" │
│ + } │
│ + }, │
│ + "node_modules/@vitest/runner": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz" │
│ , │
│ + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6O │
│ aVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@vitest/utils": "4.1.10", │
│ + "pathe": "^2.0.3" │
│ + }, │
│ + "funding": { │
│ + "url": "https://opencollective.com/vitest" │
│ + } │
│ + }, │
│ + "node_modules/@vitest/snapshot": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10. │
│ tgz", │
│ + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhc │
│ FSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@vitest/pretty-format": "4.1.10", │
│ + "@vitest/utils": "4.1.10", │
│ + "magic-string": "^0.30.21", │
│ + "pathe": "^2.0.3" │
│ + }, │
│ + "funding": { │
│ + "url": "https://opencollective.com/vitest" │
│ + } │
│ + }, │
│ + "node_modules/@vitest/spy": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", │
│ + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIB │
│ EHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "funding": { │
│ + "url": "https://opencollective.com/vitest" │
│ + } │
│ + }, │
│ + "node_modules/@vitest/utils": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", │
│ + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEw │
│ RPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@vitest/pretty-format": "4.1.10", │
│ + "convert-source-map": "^2.0.0", │
│ + "tinyrainbow": "^3.1.0" │
│ + }, │
│ + "funding": { │
│ + "url": "https://opencollective.com/vitest" │
│ + } │
│ + }, │
│ "node_modules/anymatch": { │
│ "version": "3.1.3", │
│ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", │
│ @@ -1637,6 +1786,16 @@ │
│ "url": "https://github.com/sponsors/wooorm" │
│ } │
│ }, │
│ + "node_modules/assertion-error": { │
│ + "version": "2.0.1", │
│ + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2 │
│ .0.1.tgz", │
│ + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7j │
│ v12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "engines": { │
│ + "node": ">=12" │
│ + } │
│ + }, │
│ "node_modules/astro": { │
│ "version": "6.1.9", │
│ "resolved": "https://registry.npmjs.org/astro/-/astro-6.1.9.tgz", │
│ @@ -2206,6 +2365,16 @@ │
│ "url": "https://github.com/sponsors/wooorm" │
│ } │
│ }, │
│ + "node_modules/chai": { │
│ + "version": "6.2.2", │
│ + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", │
│ + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFo │
│ z5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "engines": { │
│ + "node": ">=18" │
│ + } │
│ + }, │
│ "node_modules/character-entities": { │
│ "version": "2.0.2", │
│ "resolved": "https://registry.npmjs.org/character-entities/-/character-enti │
│ ties-2.0.2.tgz", │
│ @@ -2303,6 +2472,13 @@ │
│ "node": ">= 18" │
│ } │
│ }, │
│ + "node_modules/convert-source-map": { │
│ + "version": "2.0.0", │
│ + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source │
│ -map-2.0.0.tgz", │
│ + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXv │
│ fJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", │
│ + "dev": true, │
│ + "license": "MIT" │
│ + }, │
│ "node_modules/cookie": { │
│ "version": "1.1.1", │
│ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", │
│ @@ -2618,6 +2794,16 @@ │
│ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4K │
│ cci7BS85f93veegeCPkL8R4GLClnjLFw==", │
│ "license": "MIT" │
│ }, │
│ + "node_modules/expect-type": { │
│ + "version": "1.4.0", │
│ + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz │
│ ", │
│ + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJS │
│ uwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", │
│ + "dev": true, │
│ + "license": "Apache-2.0", │
│ + "engines": { │
│ + "node": ">=12.0.0" │
│ + } │
│ + }, │
│ "node_modules/extend": { │
│ "version": "3.0.2", │
│ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", │
│ @@ -4131,6 +4317,13 @@ │
│ "url": "https://github.com/inikulin/parse5?sponsor=1" │
│ } │
│ }, │
│ + "node_modules/pathe": { │
│ + "version": "2.0.3", │
│ + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", │
│ + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugp │
│ hxIADwspS+AraAUePCKrSVtPLFj/F88w==", │
│ + "dev": true, │
│ + "license": "MIT" │
│ + }, │
│ "node_modules/piccolore": { │
│ "version": "0.1.3", │
│ "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", │
│ @@ -4628,6 +4821,13 @@ │
│ "node": ">=20" │
│ } │
│ }, │
│ + "node_modules/siginfo": { │
│ + "version": "2.0.0", │
│ + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", │
│ + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK │
│ 0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", │
│ + "dev": true, │
│ + "license": "ISC" │
│ + }, │
│ "node_modules/sisteransi": { │
│ "version": "1.0.5", │
│ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", │
│ @@ -4665,6 +4865,20 @@ │
│ "url": "https://github.com/sponsors/wooorm" │
│ } │
│ }, │
│ + "node_modules/stackback": { │
│ + "version": "0.0.2", │
│ + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", │
│ + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8 │
│ zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", │
│ + "dev": true, │
│ + "license": "MIT" │
│ + }, │
│ + "node_modules/std-env": { │
│ + "version": "4.2.0", │
│ + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", │
│ + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAs │
│ l2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", │
│ + "dev": true, │
│ + "license": "MIT" │
│ + }, │
│ "node_modules/stringify-entities": { │
│ "version": "4.0.4", │
│ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-enti │
│ ties-4.0.4.tgz", │
│ @@ -4710,6 +4924,13 @@ │
│ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7 │
│ nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", │
│ "license": "MIT" │
│ }, │
│ + "node_modules/tinybench": { │
│ + "version": "2.9.0", │
│ + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", │
│ + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7P │
│ c2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", │
│ + "dev": true, │
│ + "license": "MIT" │
│ + }, │
│ "node_modules/tinyclip": { │
│ "version": "0.1.12", │
│ "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.12.tgz", │
│ @@ -4744,6 +4965,16 @@ │
│ "url": "https://github.com/sponsors/SuperchupuDev" │
│ } │
│ }, │
│ + "node_modules/tinyrainbow": { │
│ + "version": "3.1.0", │
│ + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz │
│ ", │
│ + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEY │
│ dVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "engines": { │
│ + "node": ">=14.0.0" │
│ + } │
│ + }, │
│ "node_modules/trim-lines": { │
│ "version": "3.0.1", │
│ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", │
│ @@ -5650,6 +5881,96 @@ │
│ } │
│ } │
│ }, │
│ + "node_modules/vitest": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", │
│ + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0 │
│ SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@vitest/expect": "4.1.10", │
│ + "@vitest/mocker": "4.1.10", │
│ + "@vitest/pretty-format": "4.1.10", │
│ + "@vitest/runner": "4.1.10", │
│ + "@vitest/snapshot": "4.1.10", │
│ + "@vitest/spy": "4.1.10", │
│ + "@vitest/utils": "4.1.10", │
│ + "es-module-lexer": "^2.0.0", │
│ + "expect-type": "^1.3.0", │
│ + "magic-string": "^0.30.21", │
│ + "obug": "^2.1.1", │
│ + "pathe": "^2.0.3", │
│ + "picomatch": "^4.0.3", │
│ + "std-env": "^4.0.0-rc.1", │
│ + "tinybench": "^2.9.0", │
│ + "tinyexec": "^1.0.2", │
│ + "tinyglobby": "^0.2.15", │
│ + "tinyrainbow": "^3.1.0", │
│ + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", │
│ + "why-is-node-running": "^2.3.0" │
│ + }, │
│ + "bin": { │
│ + "vitest": "vitest.mjs" │
│ + }, │
│ + "engines": { │
│ + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" │
│ + }, │
│ + "funding": { │
│ + "url": "https://opencollective.com/vitest" │
│ + }, │
│ + "peerDependencies": { │
│ + "@edge-runtime/vm": "*", │
│ + "@opentelemetry/api": "^1.9.0", │
│ + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", │
│ + "@vitest/browser-playwright": "4.1.10", │
│ + "@vitest/browser-preview": "4.1.10", │
│ + "@vitest/browser-webdriverio": "4.1.10", │
│ + "@vitest/coverage-istanbul": "4.1.10", │
│ + "@vitest/coverage-v8": "4.1.10", │
│ + "@vitest/ui": "4.1.10", │
│ + "happy-dom": "*", │
│ + "jsdom": "*", │
│ + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" │
│ + }, │
│ + "peerDependenciesMeta": { │
│ + "@edge-runtime/vm": { │
│ + "optional": true │
│ + }, │
│ + "@opentelemetry/api": { │
│ + "optional": true │
│ + }, │
│ + "@types/node": { │
│ + "optional": true │
│ + }, │
│ + "@vitest/browser-playwright": { │
│ + "optional": true │
│ + }, │
│ + "@vitest/browser-preview": { │
│ + "optional": true │
│ + }, │
│ + "@vitest/browser-webdriverio": { │
│ + "optional": true │
│ + }, │
│ + "@vitest/coverage-istanbul": { │
│ + "optional": true │
│ + }, │
│ + "@vitest/coverage-v8": { │
│ + "optional": true │
│ + }, │
│ + "@vitest/ui": { │
│ + "optional": true │
│ + }, │
│ + "happy-dom": { │
│ + "optional": true │
│ + }, │
│ + "jsdom": { │
│ + "optional": true │
│ + }, │
│ + "vite": { │
│ + "optional": false │
│ + } │
│ + } │
│ + }, │
│ "node_modules/web-namespaces": { │
│ "version": "2.0.1", │
│ "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0 │
│ .1.tgz", │
│ @@ -5669,6 +5990,23 @@ │
│ "node": ">=4" │
│ } │
│ }, │
│ + "node_modules/why-is-node-running": { │
│ + "version": "2.3.0", │
│ + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-r │
│ unning-2.3.0.tgz", │
│ + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7X │
│ vlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "siginfo": "^2.0.0", │
│ + "stackback": "0.0.2" │
│ + }, │
│ + "bin": { │
│ + "why-is-node-running": "cli.js" │
│ + }, │
│ + "engines": { │
│ + "node": ">=8" │
│ + } │
│ + }, │
│ "node_modules/xxhash-wasm": { │
│ "version": "1.1.0", │
│ "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz │
│ ", │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ooknet-design/package-lock.json ────┐
│ diff --git a/ooknet-design/package-lock.json │
│ b/ooknet-design/package-lock.json │
│ index ee26c9a..3482424 100644 │
│ --- a/ooknet-design/package-lock.json │
│ +++ b/ooknet-design/package-lock.json │
│ @@ -14,7 +14,8 @@ │
│ "fuse.js": "^7.3.0" │
│ }, │
│ "devDependencies": { │
│ - "sass": "^1.99.0" │
│ + "sass": "^1.99.0", │
│ + "vitest": "^4.1.10" │
│ }, │
│ "engines": { │
│ "node": ">=22.12.0" │
│ @@ -1527,6 +1528,24 @@ │
│ "integrity": "sha512-83yeghZ2xxin3Nj8 │
│ z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9 │
│ FC3E/SBEMvzJRwWEOz5gGes9Qg==", │
│ "license": "MIT" │
│ }, │
│ + "node_modules/@standard-schema/spec": { │
│ + "version": "1.1.0", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/@standard-schema/spec/-/spec-1.1.0.tgz", │
│ + "integrity": "sha512-l2aFy5jALhniG5Hg │
│ qrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXav │
│ m/jPityDo5TCvKMnpjKnOriy0w==", │
│ + "dev": true, │
│ + "license": "MIT" │
│ + }, │
│ + "node_modules/@types/chai": { │
│ + "version": "5.2.3", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/@types/chai/-/chai-5.2.3.tgz", │
│ + "integrity": "sha512-Mw558oeA9fFbv65/ │
│ y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lah │
│ r+pwZFR8A7GQleRWeI6cGFC2UA==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@types/deep-eql": "*", │
│ + "assertion-error": "^2.0.1" │
│ + } │
│ + }, │
│ "node_modules/@types/debug": { │
│ "version": "4.1.13", │
│ "resolved": "https://registry.npmjs.o │
│ rg/@types/debug/-/debug-4.1.13.tgz", │
│ @@ -1536,6 +1555,13 @@ │
│ "@types/ms": "*" │
│ } │
│ }, │
│ + "node_modules/@types/deep-eql": { │
│ + "version": "4.0.2", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/@types/deep-eql/-/deep-eql-4.0.2.tgz", │
│ + "integrity": "sha512-c9h9dVVMigMPc4bw │
│ TvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7A │
│ qFQr5pB1BRdq0juY9db81BwyFw==", │
│ + "dev": true, │
│ + "license": "MIT" │
│ + }, │
│ "node_modules/@types/estree": { │
│ "version": "1.0.8", │
│ "resolved": "https://registry.npmjs.o │
│ rg/@types/estree/-/estree-1.0.8.tgz", │
│ @@ -1587,6 +1613,129 @@ │
│ "integrity": "sha512-WmoN8qaIAo7WTYWb │
│ AZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHq │
│ jDfX8Ft5j4YzDM23f87zBfDe9g==", │
│ "license": "ISC" │
│ }, │
│ + "node_modules/@vitest/expect": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/@vitest/expect/-/expect-4.1.10.tgz", │
│ + "integrity": "sha512-YsCn+qAk1GWjQOWF │
│ EsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7i │
│ QB3HGTXkdBwGddzxPlmiql5vlA==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@standard-schema/spec": "^1.1.0", │
│ + "@types/chai": "^5.2.2", │
│ + "@vitest/spy": "4.1.10", │
│ + "@vitest/utils": "4.1.10", │
│ + "chai": "^6.2.2", │
│ + "tinyrainbow": "^3.1.0" │
│ + }, │
│ + "funding": { │
│ + "url": "https://opencollective.com/ │
│ vitest" │
│ + } │
│ + }, │
│ + "node_modules/@vitest/mocker": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/@vitest/mocker/-/mocker-4.1.10.tgz", │
│ + "integrity": "sha512-v0xaezt+DKEmKfax │
│ g133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivw │
│ V7sWy5fqn3rH5uOrSp07bp44Ow==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@vitest/spy": "4.1.10", │
│ + "estree-walker": "^3.0.3", │
│ + "magic-string": "^0.30.21" │
│ + }, │
│ + "funding": { │
│ + "url": "https://opencollective.com/ │
│ vitest" │
│ + }, │
│ + "peerDependencies": { │
│ + "msw": "^2.4.9", │
│ + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0 │
│ " │
│ + }, │
│ + "peerDependenciesMeta": { │
│ + "msw": { │
│ + "optional": true │
│ + }, │
│ + "vite": { │
│ + "optional": true │
│ + } │
│ + } │
│ + }, │
│ + "node_modules/@vitest/mocker/node_modul │
│ es/estree-walker": { │
│ + "version": "3.0.3", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/estree-walker/-/estree-walker-3.0.3.tgz", │
│ + "integrity": "sha512-7RUKfXgSMMkzt6Zu │
│ XmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC │
│ 5PQZdn2DMqioAQ2NoWcrTKmm6g==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@types/estree": "^1.0.0" │
│ + } │
│ + }, │
│ + "node_modules/@vitest/pretty-format": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/@vitest/pretty-format/-/pretty-format-4.1 │
│ .10.tgz", │
│ + "integrity": "sha512-W1HsjSH4MXQ9Yfmm │
│ hLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaV │
│ CFO+JDJMrAJGwa5pRpNpvy4P8Q==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "tinyrainbow": "^3.1.0" │
│ + }, │
│ + "funding": { │
│ + "url": "https://opencollective.com/ │
│ vitest" │
│ + } │
│ + }, │
│ + "node_modules/@vitest/runner": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/@vitest/runner/-/runner-4.1.10.tgz", │
│ + "integrity": "sha512-IKI6kpIH+LmpROpl │
│ yLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kR │
│ vSEMD870Vd91rSSw0eegtWyLGg==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@vitest/utils": "4.1.10", │
│ + "pathe": "^2.0.3" │
│ + }, │
│ + "funding": { │
│ + "url": "https://opencollective.com/ │
│ vitest" │
│ + } │
│ + }, │
│ + "node_modules/@vitest/snapshot": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/@vitest/snapshot/-/snapshot-4.1.10.tgz", │
│ + "integrity": "sha512-xRkfOT1qpTAi/Ti4 │
│ Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQq │
│ jUNGksfjMJu3Y+oJ+4Ypn2OaJw==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@vitest/pretty-format": "4.1.10", │
│ + "@vitest/utils": "4.1.10", │
│ + "magic-string": "^0.30.21", │
│ + "pathe": "^2.0.3" │
│ + }, │
│ + "funding": { │
│ + "url": "https://opencollective.com/ │
│ vitest" │
│ + } │
│ + }, │
│ + "node_modules/@vitest/spy": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/@vitest/spy/-/spy-4.1.10.tgz", │
│ + "integrity": "sha512-PLf/Ugvoq5wO/b4r │
│ wYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+ │
│ l+cj7mNwAfZHzqXqDyjvOuwbDw==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "funding": { │
│ + "url": "https://opencollective.com/ │
│ vitest" │
│ + } │
│ + }, │
│ + "node_modules/@vitest/utils": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/@vitest/utils/-/utils-4.1.10.tgz", │
│ + "integrity": "sha512-fy9am/HWxbaGt/Sa │
│ wrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ │
│ 7vKfPTwo0diXifkpPvwH9v7nGA==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@vitest/pretty-format": "4.1.10", │
│ + "convert-source-map": "^2.0.0", │
│ + "tinyrainbow": "^3.1.0" │
│ + }, │
│ + "funding": { │
│ + "url": "https://opencollective.com/ │
│ vitest" │
│ + } │
│ + }, │
│ "node_modules/anymatch": { │
│ "version": "3.1.3", │
│ "resolved": "https://registry.npmjs.o │
│ rg/anymatch/-/anymatch-3.1.3.tgz", │
│ @@ -1637,6 +1786,16 @@ │
│ "url": "https://github.com/sponsors │
│ /wooorm" │
│ } │
│ }, │
│ + "node_modules/assertion-error": { │
│ + "version": "2.0.1", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/assertion-error/-/assertion-error-2.0.1.t │
│ gz", │
│ + "integrity": "sha512-Izi8RQcffqCeNVgF │
│ igKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQ │
│ gmTAnIDnbck1uxksT4dzN3PWBA==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "engines": { │
│ + "node": ">=12" │
│ + } │
│ + }, │
│ "node_modules/astro": { │
│ "version": "6.1.9", │
│ "resolved": "https://registry.npmjs.o │
│ rg/astro/-/astro-6.1.9.tgz", │
│ @@ -2206,6 +2365,16 @@ │
│ "url": "https://github.com/sponsors │
│ /wooorm" │
│ } │
│ }, │
│ + "node_modules/chai": { │
│ + "version": "6.2.2", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/chai/-/chai-6.2.2.tgz", │
│ + "integrity": "sha512-NUPRluOfOiTKBKvW │
│ PtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tW │
│ hR4WPjNeR9jXqdC7/UpSJTnlRg==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "engines": { │
│ + "node": ">=18" │
│ + } │
│ + }, │
│ "node_modules/character-entities": { │
│ "version": "2.0.2", │
│ "resolved": "https://registry.npmjs.o │
│ rg/character-entities/-/character-entities-2 │
│ .0.2.tgz", │
│ @@ -2303,6 +2472,13 @@ │
│ "node": ">= 18" │
│ } │
│ }, │
│ + "node_modules/convert-source-map": { │
│ + "version": "2.0.0", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/convert-source-map/-/convert-source-map-2 │
│ .0.0.tgz", │
│ + "integrity": "sha512-Kvp459HrV2FEJ1CA │
│ si1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61 │
│ m0xktWwt+1HSYf3JZsTms3aRJg==", │
│ + "dev": true, │
│ + "license": "MIT" │
│ + }, │
│ "node_modules/cookie": { │
│ "version": "1.1.1", │
│ "resolved": "https://registry.npmjs.o │
│ rg/cookie/-/cookie-1.1.1.tgz", │
│ @@ -2618,6 +2794,16 @@ │
│ "integrity": "sha512-mlsTRyGaPBjPedk6 │
│ Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS │
│ 85f93veegeCPkL8R4GLClnjLFw==", │
│ "license": "MIT" │
│ }, │
│ + "node_modules/expect-type": { │
│ + "version": "1.4.0", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/expect-type/-/expect-type-1.4.0.tgz", │
│ + "integrity": "sha512-KfYbmpRm0VbLjEvV │
│ a9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFI │
│ xlGsgBQizV9q5J9NHj4VG0n+pA==", │
│ + "dev": true, │
│ + "license": "Apache-2.0", │
│ + "engines": { │
│ + "node": ">=12.0.0" │
│ + } │
│ + }, │
│ "node_modules/extend": { │
│ "version": "3.0.2", │
│ "resolved": "https://registry.npmjs.o │
│ rg/extend/-/extend-3.0.2.tgz", │
│ @@ -4131,6 +4317,13 @@ │
│ "url": "https://github.com/inikulin │
│ /parse5?sponsor=1" │
│ } │
│ }, │
│ + "node_modules/pathe": { │
│ + "version": "2.0.3", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/pathe/-/pathe-2.0.3.tgz", │
│ + "integrity": "sha512-WUjGcAqP1gQacoQe │
│ +OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADw │
│ spS+AraAUePCKrSVtPLFj/F88w==", │
│ + "dev": true, │
│ + "license": "MIT" │
│ + }, │
│ "node_modules/piccolore": { │
│ "version": "0.1.3", │
│ "resolved": "https://registry.npmjs.o │
│ rg/piccolore/-/piccolore-0.1.3.tgz", │
│ @@ -4628,6 +4821,13 @@ │
│ "node": ">=20" │
│ } │
│ }, │
│ + "node_modules/siginfo": { │
│ + "version": "2.0.0", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/siginfo/-/siginfo-2.0.0.tgz", │
│ + "integrity": "sha512-ybx0WO1/8bSBLEWX │
│ ZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwP │
│ dAaOI7TtRRRJn/Es1zhrrCHu7g==", │
│ + "dev": true, │
│ + "license": "ISC" │
│ + }, │
│ "node_modules/sisteransi": { │
│ "version": "1.0.5", │
│ "resolved": "https://registry.npmjs.o │
│ rg/sisteransi/-/sisteransi-1.0.5.tgz", │
│ @@ -4665,6 +4865,20 @@ │
│ "url": "https://github.com/sponsors │
│ /wooorm" │
│ } │
│ }, │
│ + "node_modules/stackback": { │
│ + "version": "0.0.2", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/stackback/-/stackback-0.0.2.tgz", │
│ + "integrity": "sha512-1XMJE5fQo1jGH6Y/ │
│ 7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1 │
│ P3QhVwrYe+576+jkANtSS2mBbw==", │
│ + "dev": true, │
│ + "license": "MIT" │
│ + }, │
│ + "node_modules/std-env": { │
│ + "version": "4.2.0", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/std-env/-/std-env-4.2.0.tgz", │
│ + "integrity": "sha512-oCUKSupKTHX53Eyj │
│ DtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUG │
│ U9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", │
│ + "dev": true, │
│ + "license": "MIT" │
│ + }, │
│ "node_modules/stringify-entities": { │
│ "version": "4.0.4", │
│ "resolved": "https://registry.npmjs.o │
│ rg/stringify-entities/-/stringify-entities-4 │
│ .0.4.tgz", │
│ @@ -4710,6 +4924,13 @@ │
│ "integrity": "sha512-pkY1fj1cKHb2seWD │
│ y0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9 │
│ CF0yXv/QZFY7sEJmj24dK+Rrqw==", │
│ "license": "MIT" │
│ }, │
│ + "node_modules/tinybench": { │
│ + "version": "2.9.0", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/tinybench/-/tinybench-2.9.0.tgz", │
│ + "integrity": "sha512-0+DUvqWMValLmha6 │
│ lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxox │
│ QjTXAErryXVgUOfv2YqNllqGeg==", │
│ + "dev": true, │
│ + "license": "MIT" │
│ + }, │
│ "node_modules/tinyclip": { │
│ "version": "0.1.12", │
│ "resolved": "https://registry.npmjs.o │
│ rg/tinyclip/-/tinyclip-0.1.12.tgz", │
│ @@ -4744,6 +4965,16 @@ │
│ "url": "https://github.com/sponsors │
│ /SuperchupuDev" │
│ } │
│ }, │
│ + "node_modules/tinyrainbow": { │
│ + "version": "3.1.0", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/tinyrainbow/-/tinyrainbow-3.1.0.tgz", │
│ + "integrity": "sha512-Bf+ILmBgretUrdJx │
│ zXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kY │
│ DipTBcLrCVyUzAPz7QmArb0mmw==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "engines": { │
│ + "node": ">=14.0.0" │
│ + } │
│ + }, │
│ "node_modules/trim-lines": { │
│ "version": "3.0.1", │
│ "resolved": "https://registry.npmjs.o │
│ rg/trim-lines/-/trim-lines-3.0.1.tgz", │
│ @@ -5650,6 +5881,96 @@ │
│ } │
│ } │
│ }, │
│ + "node_modules/vitest": { │
│ + "version": "4.1.10", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/vitest/-/vitest-4.1.10.tgz", │
│ + "integrity": "sha512-R9jUTe5S4Qb0HCd4 │
│ TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9A │
│ bqSk6nt0PnOx4H4WNJYZ13GUPw==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "@vitest/expect": "4.1.10", │
│ + "@vitest/mocker": "4.1.10", │
│ + "@vitest/pretty-format": "4.1.10", │
│ + "@vitest/runner": "4.1.10", │
│ + "@vitest/snapshot": "4.1.10", │
│ + "@vitest/spy": "4.1.10", │
│ + "@vitest/utils": "4.1.10", │
│ + "es-module-lexer": "^2.0.0", │
│ + "expect-type": "^1.3.0", │
│ + "magic-string": "^0.30.21", │
│ + "obug": "^2.1.1", │
│ + "pathe": "^2.0.3", │
│ + "picomatch": "^4.0.3", │
│ + "std-env": "^4.0.0-rc.1", │
│ + "tinybench": "^2.9.0", │
│ + "tinyexec": "^1.0.2", │
│ + "tinyglobby": "^0.2.15", │
│ + "tinyrainbow": "^3.1.0", │
│ + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0 │
│ ", │
│ + "why-is-node-running": "^2.3.0" │
│ + }, │
│ + "bin": { │
│ + "vitest": "vitest.mjs" │
│ + }, │
│ + "engines": { │
│ + "node": "^20.0.0 || ^22.0.0 || >=24 │
│ .0.0" │
│ + }, │
│ + "funding": { │
│ + "url": "https://opencollective.com/ │
│ vitest" │
│ + }, │
│ + "peerDependencies": { │
│ + "@edge-runtime/vm": "*", │
│ + "@opentelemetry/api": "^1.9.0", │
│ + "@types/node": "^20.0.0 || ^22.0.0 │
│ || >=24.0.0", │
│ + "@vitest/browser-playwright": "4.1. │
│ 10", │
│ + "@vitest/browser-preview": "4.1.10" │
│ , │
│ + "@vitest/browser-webdriverio": "4.1 │
│ .10", │
│ + "@vitest/coverage-istanbul": "4.1.1 │
│ 0", │
│ + "@vitest/coverage-v8": "4.1.10", │
│ + "@vitest/ui": "4.1.10", │
│ + "happy-dom": "*", │
│ + "jsdom": "*", │
│ + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0 │
│ " │
│ + }, │
│ + "peerDependenciesMeta": { │
│ + "@edge-runtime/vm": { │
│ + "optional": true │
│ + }, │
│ + "@opentelemetry/api": { │
│ + "optional": true │
│ + }, │
│ + "@types/node": { │
│ + "optional": true │
│ + }, │
│ + "@vitest/browser-playwright": { │
│ + "optional": true │
│ + }, │
│ + "@vitest/browser-preview": { │
│ + "optional": true │
│ + }, │
│ + "@vitest/browser-webdriverio": { │
│ + "optional": true │
│ + }, │
│ + "@vitest/coverage-istanbul": { │
│ + "optional": true │
│ + }, │
│ + "@vitest/coverage-v8": { │
│ + "optional": true │
│ + }, │
│ + "@vitest/ui": { │
│ + "optional": true │
│ + }, │
│ + "happy-dom": { │
│ + "optional": true │
│ + }, │
│ + "jsdom": { │
│ + "optional": true │
│ + }, │
│ + "vite": { │
│ + "optional": false │
│ + } │
│ + } │
│ + }, │
│ "node_modules/web-namespaces": { │
│ "version": "2.0.1", │
│ "resolved": "https://registry.npmjs.o │
│ rg/web-namespaces/-/web-namespaces-2.0.1.tgz │
│ ", │
│ @@ -5669,6 +5990,23 @@ │
│ "node": ">=4" │
│ } │
│ }, │
│ + "node_modules/why-is-node-running": { │
│ + "version": "2.3.0", │
│ + "resolved": "https://registry.npmjs.o │
│ rg/why-is-node-running/-/why-is-node-running │
│ -2.3.0.tgz", │
│ + "integrity": "sha512-hUrmaWBdVDcxvYqn │
│ yh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7Xvlmtbw │
│ uTclUiIyN+CyXQD4Vmko8fNm8w==", │
│ + "dev": true, │
│ + "license": "MIT", │
│ + "dependencies": { │
│ + "siginfo": "^2.0.0", │
│ + "stackback": "0.0.2" │
│ + }, │
│ + "bin": { │
│ + "why-is-node-running": "cli.js" │
│ + }, │
│ + "engines": { │
│ + "node": ">=8" │
│ + } │
│ + }, │
│ "node_modules/xxhash-wasm": { │
│ "version": "1.1.0", │
│ "resolved": "https://registry.npmjs.o │
│ rg/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", │
└──────────────────────────────────────────────┘
┌─ ooknet-design/package.json ───────────────────────────────────────────────┐
│ diff --git a/ooknet-design/package.json b/ooknet-design/package.json │
│ index e845ae1..78b118d 100644 │
│ --- a/ooknet-design/package.json │
│ +++ b/ooknet-design/package.json │
│ @@ -9,7 +9,8 @@ │
│ "dev": "astro dev", │
│ "build": "astro build", │
│ "preview": "astro preview", │
│ - "astro": "astro" │
│ + "astro": "astro", │
│ + "test": "vitest run" │
│ }, │
│ "dependencies": { │
│ "@fontsource/cascadia-mono": "^5.2.3", │
│ @@ -18,6 +19,7 @@ │
│ "fuse.js": "^7.3.0" │
│ }, │
│ "devDependencies": { │
│ - "sass": "^1.99.0" │
│ + "sass": "^1.99.0", │
│ + "vitest": "^4.1.10" │
│ } │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ooknet-design/package.json ─────────┐
│ diff --git a/ooknet-design/package.json b/oo │
│ knet-design/package.json │
│ index e845ae1..78b118d 100644 │
│ --- a/ooknet-design/package.json │
│ +++ b/ooknet-design/package.json │
│ @@ -9,7 +9,8 @@ │
│ "dev": "astro dev", │
│ "build": "astro build", │
│ "preview": "astro preview", │
│ - "astro": "astro" │
│ + "astro": "astro", │
│ + "test": "vitest run" │
│ }, │
│ "dependencies": { │
│ "@fontsource/cascadia-mono": "^5.2.3", │
│ @@ -18,6 +19,7 @@ │
│ "fuse.js": "^7.3.0" │
│ }, │
│ "devDependencies": { │
│ - "sass": "^1.99.0" │
│ + "sass": "^1.99.0", │
│ + "vitest": "^4.1.10" │
│ } │
│ } │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/components/ArticleMeta/ArticleMeta.ts ──────────────────┐
│ diff --git a/ooknet-design/src/components/ArticleMeta/ArticleMeta.ts b/ooknet-desi │
│ gn/src/components/ArticleMeta/ArticleMeta.ts │
│ index fe2667d..8f60dc9 100644 │
│ --- a/ooknet-design/src/components/ArticleMeta/ArticleMeta.ts │
│ +++ b/ooknet-design/src/components/ArticleMeta/ArticleMeta.ts │
│ @@ -7,16 +7,28 @@ export interface Props { │
│ path: string; │
│ } │
│ │
│ +// The path can exceed the narrow frame; char-wrap it under the label. │
│ +function filedUnder(path: string, width: number): string[] { │
│ + const label = "[FILED UNDER]: "; │
│ + const avail = Math.max(1, width - len(label)); │
│ + const cps = [...`/${path}`]; │
│ + const out = [label + cps.slice(0, avail).join("")]; │
│ + for (let i = avail; i < cps.length; i += avail) { │
│ + out.push(" ".repeat(len(label)) + cps.slice(i, i + avail).join("")); │
│ + } │
│ + return out; │
│ +} │
│ + │
│ export function buildMeta(width: number, fm: NoteFrontmatter, path: string): stri │
│ ng { │
│ // Below ~60 cells the JSON-style META block can't fit (the tags array │
│ // alone is ~40 chars), so narrow frames drop it. │
│ const compact = width < 60; │
│ │
│ const lines: string[] = [ │
│ - `[FILED UNDER]: /${path}`, │
│ + ...filedUnder(path, width), │
│ rule(width), │
│ "", │
│ - kv("AUTHOR", fm.authors.join(", "), 12, width), │
│ + kv("AUTHOR", fm.authors.join(", ") || "UNATTRIBUTED", 12, width), │
│ kv("PUBLISHED", fm.published, 12, width), │
│ kv("STATUS", fm.status, 12, width), │
│ ]; │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...ts/ArticleMeta/ArticleMeta.ts ───┐
│ diff --git a/ooknet-design/src/components/Ar │
│ ticleMeta/ArticleMeta.ts b/ooknet-design/src │
│ /components/ArticleMeta/ArticleMeta.ts │
│ index fe2667d..8f60dc9 100644 │
│ --- a/ooknet-design/src/components/ArticleMe │
│ ta/ArticleMeta.ts │
│ +++ b/ooknet-design/src/components/ArticleMe │
│ ta/ArticleMeta.ts │
│ @@ -7,16 +7,28 @@ export interface Props { │
│ path: string; │
│ } │
│ │
│ +// The path can exceed the narrow frame; ch │
│ ar-wrap it under the label. │
│ +function filedUnder(path: string, width: nu │
│ mber): string[] { │
│ + const label = "[FILED UNDER]: "; │
│ + const avail = Math.max(1, width - len(lab │
│ el)); │
│ + const cps = [...`/${path}`]; │
│ + const out = [label + cps.slice(0, avail). │
│ join("")]; │
│ + for (let i = avail; i < cps.length; i += │
│ avail) { │
│ + out.push(" ".repeat(len(label)) + cps.s │
│ lice(i, i + avail).join("")); │
│ + } │
│ + return out; │
│ +} │
│ + │
│ export function buildMeta(width: number, fm │
│ : NoteFrontmatter, path: string): string { │
│ // Below ~60 cells the JSON-style META bl │
│ ock can't fit (the tags array │
│ // alone is ~40 chars), so narrow frames │
│ drop it. │
│ const compact = width < 60; │
│ │
│ const lines: string[] = [ │
│ - `[FILED UNDER]: /${path}`, │
│ + ...filedUnder(path, width), │
│ rule(width), │
│ "", │
│ - kv("AUTHOR", fm.authors.join(", "), │
│ 12, width), │
│ + kv("AUTHOR", fm.authors.join(", ") | │
│ | "UNATTRIBUTED", 12, width), │
│ kv("PUBLISHED", fm.published, │
│ 12, width), │
│ kv("STATUS", fm.status, │
│ 12, width), │
│ ]; │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/components/ArticleTags/ArticleTags.test.ts ─────────────┐
│ diff --git a/ooknet-design/src/components/ArticleTags/ArticleTags.test.ts b/ooknet │
│ -design/src/components/ArticleTags/ArticleTags.test.ts │
│ new file mode 100644 │
│ index 0000000..aa37711 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/components/ArticleTags/ArticleTags.test.ts │
│ @@ -0,0 +1,30 @@ │
│ +import { describe, expect, it } from "vitest"; │
│ +import { layoutTags, TAGS_LABEL_W, TAG_GAP } from "./ArticleTags"; │
│ +import { badgeWidth } from "../Badge/Badge"; │
│ + │
│ +describe("layoutTags", () => { │
│ + it("keeps tags on one row when they fit", () => { │
│ + expect(layoutTags(["A", "B"], 48)).toEqual([["A", "B"]]); │
│ + }); │
│ + │
│ + it("wraps to a new row when the frame width is exceeded", () => { │
│ + const rows = layoutTags(["DESIGN-SYSTEM", "REFERENCE", "TYPOGRAPHY"], 48); │
│ + expect(rows.length).toBeGreaterThan(1); │
│ + for (const row of rows) { │
│ + const used = row.reduce( │
│ + (s, t, i) => s + badgeWidth(t) + (i > 0 ? TAG_GAP : 0), │
│ + 0, │
│ + ); │
│ + expect(used).toBeLessThanOrEqual(48 - TAGS_LABEL_W); │
│ + } │
│ + }); │
│ + │
│ + it("emits an overlong single tag on its own row rather than dropping it", () => │
│ { │
│ + const huge = "X".repeat(60); │
│ + expect(layoutTags([huge, "OK"], 48)).toEqual([[huge], ["OK"]]); │
│ + }); │
│ + │
│ + it("returns no rows for no tags", () => { │
│ + expect(layoutTags([], 48)).toEqual([]); │
│ + }); │
│ +}); │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...ticleTags/ArticleTags.test.ts ───┐
│ diff --git a/ooknet-design/src/components/Ar │
│ ticleTags/ArticleTags.test.ts b/ooknet-desig │
│ n/src/components/ArticleTags/ArticleTags.tes │
│ t.ts │
│ new file mode 100644 │
│ index 0000000..aa37711 │
│ --- /dev/null │
│ +++ b/ooknet-design/src/components/ArticleTa │
│ gs/ArticleTags.test.ts │
│ @@ -0,0 +1,30 @@ │
│ +import { describe, expect, it } from "vites │
│ t"; │
│ +import { layoutTags, TAGS_LABEL_W, TAG_GAP │
│ } from "./ArticleTags"; │
│ +import { badgeWidth } from "../Badge/Badge" │
│ ; │
│ + │
│ +describe("layoutTags", () => { │
│ + it("keeps tags on one row when they fit", │
│ () => { │
│ + expect(layoutTags(["A", "B"], 48)).toEq │
│ ual([["A", "B"]]); │
│ + }); │
│ + │
│ + it("wraps to a new row when the frame wid │
│ th is exceeded", () => { │
│ + const rows = layoutTags(["DESIGN-SYSTEM │
│ ", "REFERENCE", "TYPOGRAPHY"], 48); │
│ + expect(rows.length).toBeGreaterThan(1); │
│ + for (const row of rows) { │
│ + const used = row.reduce( │
│ + (s, t, i) => s + badgeWidth(t) + (i │
│ > 0 ? TAG_GAP : 0), │
│ + 0, │
│ + ); │
│ + expect(used).toBeLessThanOrEqual(48 - │
│ TAGS_LABEL_W); │
│ + } │
│ + }); │
│ + │
│ + it("emits an overlong single tag on its o │
│ wn row rather than dropping it", () => { │
│ + const huge = "X".repeat(60); │
│ + expect(layoutTags([huge, "OK"], 48)).to │
│ Equal([[huge], ["OK"]]); │
│ + }); │
│ + │
│ + it("returns no rows for no tags", () => { │
│ + expect(layoutTags([], 48)).toEqual([]); │
│ + }); │
│ +}); │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/components/Frame/Frame.astro ───────────────────────────┐
│ diff --git a/ooknet-design/src/components/Frame/Frame.astro b/ooknet-design/src/co │
│ mponents/Frame/Frame.astro │
│ index 5e70b40..a9ae51b 100644 │
│ --- a/ooknet-design/src/components/Frame/Frame.astro │
│ +++ b/ooknet-design/src/components/Frame/Frame.astro │
│ @@ -1,7 +1,10 @@ │
│ --- │
│ // Frame - renders the same ASCII builder at both desktop (FRAME_W) and │
│ // mobile (MOBILE_FRAME_W) widths, then CSS toggles which one is visible. │
│ +// Every frame passes through checkGrid, so an overflowing line or an │
│ +// off-grid glyph fails the build instead of bending a border. │
│ import Pre from "../Pre/Pre.astro"; │
│ +import { checkGrid } from "../../lib/ascii"; │
│ import { FRAME_W, MOBILE_FRAME_W } from "../../lib/config"; │
│ │
│ interface Props { │
│ @@ -10,6 +13,7 @@ interface Props { │
│ } │
│ │
│ const { build, class: cls = "" } = Astro.props as Props; │
│ +const ctx = `Frame${cls ? `(${cls})` : ""} on ${Astro.url.pathname}`; │
│ --- │
│ -<Pre class={`frame-wide ${cls}`.trim()}>{build(FRAME_W)}</Pre> │
│ -<Pre class={`frame-narrow ${cls}`.trim()}>{build(MOBILE_FRAME_W)}</Pre> │
│ +<Pre class={`frame-wide ${cls}`.trim()}>{checkGrid(build(FRAME_W), FRAME_W, ctx)} │
│ </Pre> │
│ +<Pre class={`frame-narrow ${cls}`.trim()}>{checkGrid(build(MOBILE_FRAME_W), MOBIL │
│ E_FRAME_W, ctx)}</Pre> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ .../components/Frame/Frame.astro ───┐
│ diff --git a/ooknet-design/src/components/Fr │
│ ame/Frame.astro b/ooknet-design/src/componen │
│ ts/Frame/Frame.astro │
│ index 5e70b40..a9ae51b 100644 │
│ --- a/ooknet-design/src/components/Frame/Fra │
│ me.astro │
│ +++ b/ooknet-design/src/components/Frame/Fra │
│ me.astro │
│ @@ -1,7 +1,10 @@ │
│ --- │
│ // Frame - renders the same ASCII builder a │
│ t both desktop (FRAME_W) and │
│ // mobile (MOBILE_FRAME_W) widths, then CSS │
│ toggles which one is visible. │
│ +// Every frame passes through checkGrid, so │
│ an overflowing line or an │
│ +// off-grid glyph fails the build instead o │
│ f bending a border. │
│ import Pre from "../Pre/Pre.astro"; │
│ +import { checkGrid } from "../../lib/ascii" │
│ ; │
│ import { FRAME_W, MOBILE_FRAME_W } from ".. │
│ /../lib/config"; │
│ │
│ interface Props { │
│ @@ -10,6 +13,7 @@ interface Props { │
│ } │
│ │
│ const { build, class: cls = "" } = Astro.pr │
│ ops as Props; │
│ +const ctx = `Frame${cls ? `(${cls})` : ""} │
│ on ${Astro.url.pathname}`; │
│ --- │
│ -<Pre class={`frame-wide ${cls}`.trim()}>{bu │
│ ild(FRAME_W)}</Pre> │
│ -<Pre class={`frame-narrow ${cls}`.trim()}>{ │
│ build(MOBILE_FRAME_W)}</Pre> │
│ +<Pre class={`frame-wide ${cls}`.trim()}>{ch │
│ eckGrid(build(FRAME_W), FRAME_W, ctx)}</Pre> │
│ +<Pre class={`frame-narrow ${cls}`.trim()}>{ │
│ checkGrid(build(MOBILE_FRAME_W), MOBILE_FRAM │
│ E_W, ctx)}</Pre> │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/content.config.ts ──────────────────────────────────────┐
│ diff --git a/ooknet-design/src/content.config.ts b/ooknet-design/src/content.confi │
│ g.ts │
│ index f928ef0..ce664a6 100644 │
│ --- a/ooknet-design/src/content.config.ts │
│ +++ b/ooknet-design/src/content.config.ts │
│ @@ -1,23 +1,29 @@ │
│ import { defineCollection, z } from "astro:content"; │
│ import { glob } from "astro/loaders"; │
│ │
│ +// Only title, note, and published are required to author a note; the │
│ +// rest defaults so a bare draft can't crash the content sync. │
│ const notes = defineCollection({ │
│ loader: glob({ pattern: "**/*.md", base: "./src/content/notes" }), │
│ schema: z.object({ │
│ title: z.string(), │
│ note: z.string(), // e.g. "N217" │
│ + published: z.string(), // ISO date │
│ volume: z.string().optional(), │
│ date: z.string().optional(), │
│ - authors: z.array(z.string()), │
│ - published: z.string(), // ISO date │
│ - status: z.string(), │
│ - meta: z.object({ │
│ - source: z.string(), │
│ - category: z.string(), │
│ - tags: z.array(z.string()), │
│ - version: z.string(), │
│ - }), │
│ - filed: z.string(), │
│ + authors: z.array(z.string()).default([]), │
│ + status: z.string().default("DRAFT"), │
│ + meta: z │
│ + .object({ │
│ + source: z.string().default("ooknet"), │
│ + category: z.string().default("uncategorized"), │
│ + tags: z.array(z.string()).default([]), │
│ + version: z.string().default("v0.1"), │
│ + }) │
│ + // zod v4 uses defaults as-is without re-parsing, so the object │
│ + // default must be spelled out in full. │
│ + .default({ source: "ooknet", category: "uncategorized", tags: [], version: │
│ "v0.1" }), │
│ + filed: z.string().default("/UNFILED"), │
│ page: z.number().optional(), │
│ seeAlso: z.array(z.string()).optional(), │
│ draft: z.boolean().default(false), │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...-design/src/content.config.ts ───┐
│ diff --git a/ooknet-design/src/content.confi │
│ g.ts b/ooknet-design/src/content.config.ts │
│ index f928ef0..ce664a6 100644 │
│ --- a/ooknet-design/src/content.config.ts │
│ +++ b/ooknet-design/src/content.config.ts │
│ @@ -1,23 +1,29 @@ │
│ import { defineCollection, z } from "astro: │
│ content"; │
│ import { glob } from "astro/loaders"; │
│ │
│ +// Only title, note, and published are requ │
│ ired to author a note; the │
│ +// rest defaults so a bare draft can't cras │
│ h the content sync. │
│ const notes = defineCollection({ │
│ loader: glob({ pattern: "**/*.md", base: │
│ "./src/content/notes" }), │
│ schema: z.object({ │
│ title: z.string(), │
│ note: z.string(), │
│ // e.g. "N217" │
│ + published: z.string(), │
│ // ISO date │
│ volume: z.string().optional(), │
│ date: z.string().optional(), │
│ - authors: z.array(z.string()), │
│ - published: z.string(), │
│ // ISO date │
│ - status: z.string(), │
│ - meta: z.object({ │
│ - source: z.string(), │
│ - category: z.string(), │
│ - tags: z.array(z.string()), │
│ - version: z.string(), │
│ - }), │
│ - filed: z.string(), │
│ + authors: z.array(z.string()).default([] │
│ ), │
│ + status: z.string().default("DRAFT"), │
│ + meta: z │
│ + .object({ │
│ + source: z.string().default("ooknet" │
│ ), │
│ + category: z.string().default("uncat │
│ egorized"), │
│ + tags: z.array(z.string()).default([ │
│ ]), │
│ + version: z.string().default("v0.1") │
│ , │
│ + }) │
│ + // zod v4 uses defaults as-is without │
│ re-parsing, so the object │
│ + // default must be spelled out in ful │
│ l. │
│ + .default({ source: "ooknet", category │
│ : "uncategorized", tags: [], version: "v0.1" │
│ }), │
│ + filed: z.string().default("/UNFILED"), │
│ page: z.number().optional(), │
│ seeAlso: z.array(z.string()).optional() │
│ , │
│ draft: z.boolean().default(false), │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/ascii.test.ts ──────────────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/ascii.test.ts b/ooknet-design/src/lib/ascii.tes │
│ t.ts │
│ new file mode 100644 │
│ index 0000000..87c58fd │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/ascii.test.ts │
│ @@ -0,0 +1,103 @@ │
│ +import { describe, expect, it } from "vitest"; │
│ +import { G, box, center, checkGrid, kv, len, pad, row2, row3, rule, ruleD, wrap } │
│ from "./ascii"; │
│ + │
│ +describe("len", () => { │
│ + it("counts codepoints, not UTF-16 units", () => { │
│ + expect(len("abc")).toBe(3); │
│ + expect(len("?")).toBe(1); │
│ + expect(len("")).toBe(0); │
│ + expect(len(null)).toBe(0); │
│ + expect(len(undefined)).toBe(0); │
│ + }); │
│ +}); │
│ + │
│ +describe("pad", () => { │
│ + it("pads end by default", () => { │
│ + expect(pad("ab", 5)).toBe("ab "); │
│ + }); │
│ + it("pads start", () => { │
│ + expect(pad("ab", 5, "start")).toBe(" ab"); │
│ + }); │
│ + it("centers with the extra cell on the right", () => { │
│ + expect(pad("ab", 5, "center")).toBe(" ab "); │
│ + }); │
│ + it("truncates overlong input at codepoint boundaries", () => { │
│ + expect(pad("abcdef", 3)).toBe("abc"); │
│ + expect(pad("???", 2)).toBe("??"); │
│ + }); │
│ +}); │
│ + │
│ +describe("rules", () => { │
│ + it("rule and ruleD repeat to exact width", () => { │
│ + expect(rule(4)).toBe("────"); │
│ + expect(ruleD(3)).toBe("═══"); │
│ + }); │
│ +}); │
│ + │
│ +describe("box", () => { │
│ + it("emits every line at exactly the box width", () => { │
│ + const b = box({ width: 12, lines: ["hi", "a longer li"], title: "T" }); │
│ + for (const line of b.split("\n")) expect(len(line)).toBe(12); │
│ + }); │
│ + it("draws corners and title separator", () => { │
│ + const lines = box({ width: 8, lines: ["x"], title: "T" }).split("\n"); │
│ + expect(lines[0]).toBe(G.TL + "──────" + G.TR); │
│ + expect(lines[2]).toBe(G.LJ + "──────" + G.RJ); │
│ + expect(lines.at(-1)).toBe(G.BL + "──────" + G.BR); │
│ + }); │
│ + it("centers content when asked", () => { │
│ + const lines = box({ width: 8, lines: ["ab"], align: "center" }).split("\n"); │
│ + expect(lines[1]).toBe("│ ab │"); │
│ + }); │
│ +}); │
│ + │
│ +describe("rows", () => { │
│ + it("kv pads key and value to the total width", () => { │
│ + expect(kv("K", "V", 4, 10)).toBe("K V "); │
│ + }); │
│ + it("row2 fills the gap to exact width", () => { │
│ + const r = row2("LEFT", "RIGHT", 20); │
│ + expect(len(r)).toBe(20); │
│ + expect(r.startsWith("LEFT")).toBe(true); │
│ + expect(r.endsWith("RIGHT")).toBe(true); │
│ + }); │
│ + it("row3 places left, center, right in exact width", () => { │
│ + const r = row3("L", "CC", "R", 12); │
│ + expect(len(r)).toBe(12); │
│ + expect(r[0]).toBe("L"); │
│ + expect(r[11]).toBe("R"); │
│ + expect(r).toContain("CC"); │
│ + }); │
│ + it("center returns exact width", () => { │
│ + expect(center("ab", 6)).toBe(" ab "); │
│ + }); │
│ +}); │
│ + │
│ +describe("wrap", () => { │
│ + it("wraps greedily at word boundaries", () => { │
│ + expect(wrap("aa bb cc", 5)).toBe("aa bb\ncc"); │
│ + }); │
│ + it("leaves a too-long word on its own line rather than splitting it", () => { │
│ + expect(wrap("abcdefgh xy", 4)).toBe("abcdefgh\nxy"); │
│ + }); │
│ + it("returns single line when everything fits", () => { │
│ + expect(wrap("aa bb", 10)).toBe("aa bb"); │
│ + }); │
│ +}); │
│ + │
│ +describe("checkGrid", () => { │
│ + it("accepts frame glyphs, extras, latin-1, and ASCII", () => { │
│ + const ok = "│ ok caf? - ? - ↑↓ ╌ ═ ┼ │"; │
│ + expect(checkGrid(ok, 40, "t")).toBe(ok); │
│ + }); │
│ + it("throws on lines exceeding the width", () => { │
│ + expect(() => checkGrid("abcdef", 5, "t")).toThrow(/6 cells, max 5/); │
│ + }); │
│ + it("throws on off-grid characters with the codepoint named", () => { │
│ + expect(() => checkGrid("ok ?", 10, "t")).toThrow(/U\+1F44D/); │
│ + expect(() => checkGrid("??", 10, "t")).toThrow(/off-grid/); │
│ + }); │
│ + it("rejects glyphs the font subset lacks, naming the context", () => { │
│ + expect(() => checkGrid("a ? b", 10, "my-frame")).toThrow(/my-frame/); │
│ + }); │
│ +}); │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...-design/src/lib/ascii.test.ts ───┐
│ diff --git a/ooknet-design/src/lib/ascii.tes │
│ t.ts b/ooknet-design/src/lib/ascii.test.ts │
│ new file mode 100644 │
│ index 0000000..87c58fd │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/ascii.test.ts │
│ @@ -0,0 +1,103 @@ │
│ +import { describe, expect, it } from "vites │
│ t"; │
│ +import { G, box, center, checkGrid, kv, len │
│ , pad, row2, row3, rule, ruleD, wrap } from │
│ "./ascii"; │
│ + │
│ +describe("len", () => { │
│ + it("counts codepoints, not UTF-16 units", │
│ () => { │
│ + expect(len("abc")).toBe(3); │
│ + expect(len("?")).toBe(1); │
│ + expect(len("")).toBe(0); │
│ + expect(len(null)).toBe(0); │
│ + expect(len(undefined)).toBe(0); │
│ + }); │
│ +}); │
│ + │
│ +describe("pad", () => { │
│ + it("pads end by default", () => { │
│ + expect(pad("ab", 5)).toBe("ab "); │
│ + }); │
│ + it("pads start", () => { │
│ + expect(pad("ab", 5, "start")).toBe(" │
│ ab"); │
│ + }); │
│ + it("centers with the extra cell on the ri │
│ ght", () => { │
│ + expect(pad("ab", 5, "center")).toBe(" a │
│ b "); │
│ + }); │
│ + it("truncates overlong input at codepoint │
│ boundaries", () => { │
│ + expect(pad("abcdef", 3)).toBe("abc"); │
│ + expect(pad("???", 2)).toBe("??"); │
│ + }); │
│ +}); │
│ + │
│ +describe("rules", () => { │
│ + it("rule and ruleD repeat to exact width" │
│ , () => { │
│ + expect(rule(4)).toBe("────"); │
│ + expect(ruleD(3)).toBe("═══"); │
│ + }); │
│ +}); │
│ + │
│ +describe("box", () => { │
│ + it("emits every line at exactly the box w │
│ idth", () => { │
│ + const b = box({ width: 12, lines: ["hi" │
│ , "a longer li"], title: "T" }); │
│ + for (const line of b.split("\n")) expec │
│ t(len(line)).toBe(12); │
│ + }); │
│ + it("draws corners and title separator", ( │
│ ) => { │
│ + const lines = box({ width: 8, lines: [" │
│ x"], title: "T" }).split("\n"); │
│ + expect(lines[0]).toBe(G.TL + "──────" + │
│ G.TR); │
│ + expect(lines[2]).toBe(G.LJ + "──────" + │
│ G.RJ); │
│ + expect(lines.at(-1)).toBe(G.BL + "───── │
│ ─" + G.BR); │
│ + }); │
│ + it("centers content when asked", () => { │
│ + const lines = box({ width: 8, lines: [" │
│ ab"], align: "center" }).split("\n"); │
│ + expect(lines[1]).toBe("│ ab │"); │
│ + }); │
│ +}); │
│ + │
│ +describe("rows", () => { │
│ + it("kv pads key and value to the total wi │
│ dth", () => { │
│ + expect(kv("K", "V", 4, 10)).toBe("K V │
│ "); │
│ + }); │
│ + it("row2 fills the gap to exact width", ( │
│ ) => { │
│ + const r = row2("LEFT", "RIGHT", 20); │
│ + expect(len(r)).toBe(20); │
│ + expect(r.startsWith("LEFT")).toBe(true) │
│ ; │
│ + expect(r.endsWith("RIGHT")).toBe(true); │
│ + }); │
│ + it("row3 places left, center, right in ex │
│ act width", () => { │
│ + const r = row3("L", "CC", "R", 12); │
│ + expect(len(r)).toBe(12); │
│ + expect(r[0]).toBe("L"); │
│ + expect(r[11]).toBe("R"); │
│ + expect(r).toContain("CC"); │
│ + }); │
│ + it("center returns exact width", () => { │
│ + expect(center("ab", 6)).toBe(" ab "); │
│ + }); │
│ +}); │
│ + │
│ +describe("wrap", () => { │
│ + it("wraps greedily at word boundaries", ( │
│ ) => { │
│ + expect(wrap("aa bb cc", 5)).toBe("aa bb │
│ \ncc"); │
│ + }); │
│ + it("leaves a too-long word on its own lin │
│ e rather than splitting it", () => { │
│ + expect(wrap("abcdefgh xy", 4)).toBe("ab │
│ cdefgh\nxy"); │
│ + }); │
│ + it("returns single line when everything f │
│ its", () => { │
│ + expect(wrap("aa bb", 10)).toBe("aa bb") │
│ ; │
│ + }); │
│ +}); │
│ + │
│ +describe("checkGrid", () => { │
│ + it("accepts frame glyphs, extras, latin-1 │
│ , and ASCII", () => { │
│ + const ok = "│ ok caf? - ? - ↑↓ ╌ ═ ┼ │" │
│ ; │
│ + expect(checkGrid(ok, 40, "t")).toBe(ok) │
│ ; │
│ + }); │
│ + it("throws on lines exceeding the width", │
│ () => { │
│ + expect(() => checkGrid("abcdef", 5, "t" │
│ )).toThrow(/6 cells, max 5/); │
│ + }); │
│ + it("throws on off-grid characters with th │
│ e codepoint named", () => { │
│ + expect(() => checkGrid("ok ?", 10, "t") │
│ ).toThrow(/U\+1F44D/); │
│ + expect(() => checkGrid("??", 10, "t")). │
│ toThrow(/off-grid/); │
│ + }); │
│ + it("rejects glyphs the font subset lacks, │
│ naming the context", () => { │
│ + expect(() => checkGrid("a ? b", 10, "my │
│ -frame")).toThrow(/my-frame/); │
│ + }); │
│ +}); │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/ascii.ts ───────────────────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/ascii.ts b/ooknet-design/src/lib/ascii.ts │
│ index 466add5..afdd52d 100644 │
│ --- a/ooknet-design/src/lib/ascii.ts │
│ +++ b/ooknet-design/src/lib/ascii.ts │
│ @@ -11,6 +11,30 @@ export const G = { │
│ // pairs otherwise. │
│ export const len = (s: unknown): number => [...String(s ?? "")].length; │
│ │
│ +// Codepoints known to render exactly one cell wide in the frame font │
│ +// (fontsource Cascadia Mono, latin subset): printable ASCII + tab, │
│ +// Latin-1 supplement, box drawing, and the measured extras - - - ... ↑ ↓. │
│ +// Anything else - emoji, CJK, glyphs the subset lacks (?, ?, ?) - bends │
│ +// a frame border. Measure a new glyph against "0" in the browser before │
│ +// adding it here. │
│ +const ON_GRID = /^[\t\x20-\x7E\u00A0-\u00FF\u2013\u2014\u2022\u2026\u2122\u2191\u │
│ 2193\u2500-\u257F]$/u; │
│ + │
│ +/** Throws unless every line of `text` fits `maxW` cells and uses only │
│ + * on-grid glyphs. Returns `text` so call sites can wrap in place. */ │
│ +export function checkGrid(text: string, maxW: number, ctx: string): string { │
│ + for (const line of text.split("\n")) { │
│ + if (len(line) > maxW) { │
│ + throw new Error(`[ascii] ${ctx}: line is ${len(line)} cells, max ${maxW}: " │
│ ${[...line].slice(0, 40).join("")}..."`); │
│ + } │
│ + const bad = [...line].find((c) => !ON_GRID.test(c)); │
│ + if (bad !== undefined) { │
│ + const cp = bad.codePointAt(0)!.toString(16).toUpperCase().padStart(4, "0"); │
│ + throw new Error(`[ascii] ${ctx}: off-grid character "${bad}" (U+${cp}). If │
│ it measures one cell wide in the frame font, add it to ON_GRID in ascii.ts.`); │
│ + } │
│ + } │
│ + return text; │
│ +} │
│ + │
│ export type PadSide = "start" | "end" | "center"; │
│ export function pad(s: unknown, n: number, side: PadSide = "end"): string { │
│ const str = String(s ?? ""); │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/ascii.ts ─────┐
│ diff --git a/ooknet-design/src/lib/ascii.ts │
│ b/ooknet-design/src/lib/ascii.ts │
│ index 466add5..afdd52d 100644 │
│ --- a/ooknet-design/src/lib/ascii.ts │
│ +++ b/ooknet-design/src/lib/ascii.ts │
│ @@ -11,6 +11,30 @@ export const G = { │
│ // pairs otherwise. │
│ export const len = (s: unknown): number => │
│ [...String(s ?? "")].length; │
│ │
│ +// Codepoints known to render exactly one c │
│ ell wide in the frame font │
│ +// (fontsource Cascadia Mono, latin subset) │
│ : printable ASCII + tab, │
│ +// Latin-1 supplement, box drawing, and the │
│ measured extras - - - ... ↑ ↓. │
│ +// Anything else - emoji, CJK, glyphs the s │
│ ubset lacks (?, ?, ?) - bends │
│ +// a frame border. Measure a new glyph agai │
│ nst "0" in the browser before │
│ +// adding it here. │
│ +const ON_GRID = /^[\t\x20-\x7E\u00A0-\u00FF │
│ \u2013\u2014\u2022\u2026\u2122\u2191\u2193\u │
│ 2500-\u257F]$/u; │
│ + │
│ +/** Throws unless every line of `text` fits │
│ `maxW` cells and uses only │
│ + * on-grid glyphs. Returns `text` so call │
│ sites can wrap in place. */ │
│ +export function checkGrid(text: string, max │
│ W: number, ctx: string): string { │
│ + for (const line of text.split("\n")) { │
│ + if (len(line) > maxW) { │
│ + throw new Error(`[ascii] ${ctx}: line │
│ is ${len(line)} cells, max ${maxW}: "${[... │
│ line].slice(0, 40).join("")}..."`); │
│ + } │
│ + const bad = [...line].find((c) => !ON_G │
│ RID.test(c)); │
│ + if (bad !== undefined) { │
│ + const cp = bad.codePointAt(0)!.toStri │
│ ng(16).toUpperCase().padStart(4, "0"); │
│ + throw new Error(`[ascii] ${ctx}: off- │
│ grid character "${bad}" (U+${cp}). If it mea │
│ sures one cell wide in the frame font, add i │
│ t to ON_GRID in ascii.ts.`); │
│ + } │
│ + } │
│ + return text; │
│ +} │
│ + │
│ export type PadSide = "start" | "end" | "ce │
│ nter"; │
│ export function pad(s: unknown, n: number, │
│ side: PadSide = "end"): string { │
│ const str = String(s ?? ""); │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/notes.test.ts ──────────────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/notes.test.ts b/ooknet-design/src/lib/notes.tes │
│ t.ts │
│ new file mode 100644 │
│ index 0000000..cf0bcea │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/notes.test.ts │
│ @@ -0,0 +1,28 @@ │
│ +import { describe, expect, it } from "vitest"; │
│ +import { byNoteNum, noteNum, published } from "./notes"; │
│ + │
│ +describe("noteNum", () => { │
│ + it("extracts the number from a note id", () => { │
│ + expect(noteNum("N217")).toBe(217); │
│ + expect(noteNum("N0")).toBe(0); │
│ + expect(noteNum("VOLUME 04 / NOTE 218")).toBe(4218); │
│ + }); │
│ + it("falls back to 0 with no digits", () => { │
│ + expect(noteNum("DRAFT")).toBe(0); │
│ + }); │
│ +}); │
│ + │
│ +describe("byNoteNum", () => { │
│ + it("sorts numerically, not lexicographically", () => { │
│ + const notes = ["N9", "N217", "N0", "N1000"].map((n) => ({ data: { note: n } } │
│ )); │
│ + expect(notes.sort(byNoteNum).map((n) => n.data.note)).toEqual(["N0", "N9", "N │
│ 217", "N1000"]); │
│ + }); │
│ +}); │
│ + │
│ +describe("published", () => { │
│ + it("filters drafts", () => { │
│ + expect(published({ data: { draft: true } })).toBe(false); │
│ + expect(published({ data: { draft: false } })).toBe(true); │
│ + expect(published({ data: {} })).toBe(true); │
│ + }); │
│ +}); │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...-design/src/lib/notes.test.ts ───┐
│ diff --git a/ooknet-design/src/lib/notes.tes │
│ t.ts b/ooknet-design/src/lib/notes.test.ts │
│ new file mode 100644 │
│ index 0000000..cf0bcea │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/notes.test.ts │
│ @@ -0,0 +1,28 @@ │
│ +import { describe, expect, it } from "vites │
│ t"; │
│ +import { byNoteNum, noteNum, published } fr │
│ om "./notes"; │
│ + │
│ +describe("noteNum", () => { │
│ + it("extracts the number from a note id", │
│ () => { │
│ + expect(noteNum("N217")).toBe(217); │
│ + expect(noteNum("N0")).toBe(0); │
│ + expect(noteNum("VOLUME 04 / NOTE 218")) │
│ .toBe(4218); │
│ + }); │
│ + it("falls back to 0 with no digits", () = │
│ > { │
│ + expect(noteNum("DRAFT")).toBe(0); │
│ + }); │
│ +}); │
│ + │
│ +describe("byNoteNum", () => { │
│ + it("sorts numerically, not lexicographica │
│ lly", () => { │
│ + const notes = ["N9", "N217", "N0", "N10 │
│ 00"].map((n) => ({ data: { note: n } })); │
│ + expect(notes.sort(byNoteNum).map((n) => │
│ n.data.note)).toEqual(["N0", "N9", "N217", │
│ "N1000"]); │
│ + }); │
│ +}); │
│ + │
│ +describe("published", () => { │
│ + it("filters drafts", () => { │
│ + expect(published({ data: { draft: true │
│ } })).toBe(false); │
│ + expect(published({ data: { draft: false │
│ } })).toBe(true); │
│ + expect(published({ data: {} })).toBe(tr │
│ ue); │
│ + }); │
│ +}); │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/notes.ts ───────────────────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/notes.ts b/ooknet-design/src/lib/notes.ts │
│ new file mode 100644 │
│ index 0000000..8cb12cb │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/notes.ts │
│ @@ -0,0 +1,13 @@ │
│ +// Pure note helpers. astro:content stays in the pages so this module │
│ +// is unit-testable. │
│ + │
│ +export const published = ({ data }: { data: { draft?: boolean } }) => !data.draft │
│ ; │
│ + │
│ +export const noteNum = (note: string): number => │
│ + parseInt(note.replace(/\D/g, ""), 10) || 0; │
│ + │
│ +type HasNote = { data: { note: string } }; │
│ + │
│ +/** Ascending by note number: N0, N1, ..., N217. */ │
│ +export const byNoteNum = (a: HasNote, b: HasNote): number => │
│ + noteNum(a.data.note) - noteNum(b.data.note); │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/notes.ts ─────┐
│ diff --git a/ooknet-design/src/lib/notes.ts │
│ b/ooknet-design/src/lib/notes.ts │
│ new file mode 100644 │
│ index 0000000..8cb12cb │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/notes.ts │
│ @@ -0,0 +1,13 @@ │
│ +// Pure note helpers. astro:content stays i │
│ n the pages so this module │
│ +// is unit-testable. │
│ + │
│ +export const published = ({ data }: { data: │
│ { draft?: boolean } }) => !data.draft; │
│ + │
│ +export const noteNum = (note: string): numb │
│ er => │
│ + parseInt(note.replace(/\D/g, ""), 10) || │
│ 0; │
│ + │
│ +type HasNote = { data: { note: string } }; │
│ + │
│ +/** Ascending by note number: N0, N1, ..., │
│ N217. */ │
│ +export const byNoteNum = (a: HasNote, b: Ha │
│ sNote): number => │
│ + noteNum(a.data.note) - noteNum(b.data.not │
│ e); │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/rehype-ascii.test.ts ───────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/rehype-ascii.test.ts b/ooknet-design/src/lib/re │
│ hype-ascii.test.ts │
│ new file mode 100644 │
│ index 0000000..b06a99e │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/rehype-ascii.test.ts │
│ @@ -0,0 +1,53 @@ │
│ +import { describe, expect, it } from "vitest"; │
│ +import { buildAsciiTable, type Cell } from "./rehype-ascii"; │
│ +import { len } from "./ascii"; │
│ + │
│ +const cell = (text: string, align: Cell["align"] = "end"): Cell => ({ text, align │
│ }); │
│ + │
│ +const HEADER = [cell("NAME"), cell("VALUE")]; │
│ + │
│ +describe("buildAsciiTable", () => { │
│ + it("emits every line at the same width, within the target", () => { │
│ + const t = buildAsciiTable([HEADER, [cell("a"), cell("b")]], 1, 40); │
│ + const widths = new Set(t.split("\n").map(len)); │
│ + expect(widths.size).toBe(1); │
│ + expect([...widths][0]).toBeLessThanOrEqual(40); │
│ + }); │
│ + │
│ + it("keeps single-line tables compact (no row separators)", () => { │
│ + const t = buildAsciiTable([HEADER, [cell("a"), cell("b")], [cell("c"), cell(" │
│ d")]], 1, 40); │
│ + expect(t).not.toContain("╌"); │
│ + // one solid separator under the header only │
│ + expect(t.split("\n").filter((l) => l.startsWith("├")).length).toBe(1); │
│ + }); │
│ + │
│ + it("adds dashed separators between body rows once any cell wraps", () => { │
│ + const long = "a description long enough that it must wrap onto several lines" │
│ ; │
│ + const t = buildAsciiTable( │
│ + [HEADER, [cell("frame"), cell(long)], [cell("select"), cell("short")]], │
│ + 1, │
│ + 30, │
│ + ); │
│ + expect(t).toContain("╌"); │
│ + const dashed = t.split("\n").filter((l) => l.includes("╌")); │
│ + expect(dashed.length).toBe(1); // between the two body rows │
│ + }); │
│ + │
│ + it("right-aligns cells marked align start", () => { │
│ + const t = buildAsciiTable([HEADER, [cell("x"), cell("42", "start")]], 1, 40); │
│ + const row = t.split("\n").find((l) => l.includes("42"))!; │
│ + expect(row).toMatch(/ 42 │$/); │
│ + }); │
│ + │
│ + it("shrinks the widest column until the table fits", () => { │
│ + const wide = "word ".repeat(30).trim(); │
│ + const t = buildAsciiTable([HEADER, [cell("k"), cell(wide)]], 1, 48); │
│ + for (const line of t.split("\n")) expect(len(line)).toBeLessThanOrEqual(48); │
│ + }); │
│ + │
│ + it("pads ragged rows to the full column count", () => { │
│ + const t = buildAsciiTable([HEADER, [cell("only-one")]], 1, 40); │
│ + const widths = new Set(t.split("\n").map(len)); │
│ + expect(widths.size).toBe(1); │
│ + }); │
│ +}); │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ .../src/lib/rehype-ascii.test.ts ───┐
│ diff --git a/ooknet-design/src/lib/rehype-as │
│ cii.test.ts b/ooknet-design/src/lib/rehype-a │
│ scii.test.ts │
│ new file mode 100644 │
│ index 0000000..b06a99e │
│ --- /dev/null │
│ +++ b/ooknet-design/src/lib/rehype-ascii.tes │
│ t.ts │
│ @@ -0,0 +1,53 @@ │
│ +import { describe, expect, it } from "vites │
│ t"; │
│ +import { buildAsciiTable, type Cell } from │
│ "./rehype-ascii"; │
│ +import { len } from "./ascii"; │
│ + │
│ +const cell = (text: string, align: Cell["al │
│ ign"] = "end"): Cell => ({ text, align }); │
│ + │
│ +const HEADER = [cell("NAME"), cell("VALUE") │
│ ]; │
│ + │
│ +describe("buildAsciiTable", () => { │
│ + it("emits every line at the same width, w │
│ ithin the target", () => { │
│ + const t = buildAsciiTable([HEADER, [cel │
│ l("a"), cell("b")]], 1, 40); │
│ + const widths = new Set(t.split("\n").ma │
│ p(len)); │
│ + expect(widths.size).toBe(1); │
│ + expect([...widths][0]).toBeLessThanOrEq │
│ ual(40); │
│ + }); │
│ + │
│ + it("keeps single-line tables compact (no │
│ row separators)", () => { │
│ + const t = buildAsciiTable([HEADER, [cel │
│ l("a"), cell("b")], [cell("c"), cell("d")]], │
│ 1, 40); │
│ + expect(t).not.toContain("╌"); │
│ + // one solid separator under the header │
│ only │
│ + expect(t.split("\n").filter((l) => l.st │
│ artsWith("├")).length).toBe(1); │
│ + }); │
│ + │
│ + it("adds dashed separators between body r │
│ ows once any cell wraps", () => { │
│ + const long = "a description long enough │
│ that it must wrap onto several lines"; │
│ + const t = buildAsciiTable( │
│ + [HEADER, [cell("frame"), cell(long)], │
│ [cell("select"), cell("short")]], │
│ + 1, │
│ + 30, │
│ + ); │
│ + expect(t).toContain("╌"); │
│ + const dashed = t.split("\n").filter((l) │
│ => l.includes("╌")); │
│ + expect(dashed.length).toBe(1); // betwe │
│ en the two body rows │
│ + }); │
│ + │
│ + it("right-aligns cells marked align start │
│ ", () => { │
│ + const t = buildAsciiTable([HEADER, [cel │
│ l("x"), cell("42", "start")]], 1, 40); │
│ + const row = t.split("\n").find((l) => l │
│ .includes("42"))!; │
│ + expect(row).toMatch(/ 42 │$/); │
│ + }); │
│ + │
│ + it("shrinks the widest column until the t │
│ able fits", () => { │
│ + const wide = "word ".repeat(30).trim(); │
│ + const t = buildAsciiTable([HEADER, [cel │
│ l("k"), cell(wide)]], 1, 48); │
│ + for (const line of t.split("\n")) expec │
│ t(len(line)).toBeLessThanOrEqual(48); │
│ + }); │
│ + │
│ + it("pads ragged rows to the full column c │
│ ount", () => { │
│ + const t = buildAsciiTable([HEADER, [cel │
│ l("only-one")]], 1, 40); │
│ + const widths = new Set(t.split("\n").ma │
│ p(len)); │
│ + expect(widths.size).toBe(1); │
│ + }); │
│ +}); │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/rehype-ascii.ts ────────────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/rehype-ascii.ts b/ooknet-design/src/lib/rehype- │
│ ascii.ts │
│ index c9a4a9c..2a07ace 100644 │
│ --- a/ooknet-design/src/lib/rehype-ascii.ts │
│ +++ b/ooknet-design/src/lib/rehype-ascii.ts │
│ @@ -4,7 +4,7 @@ │
│ // gets a head-rule. Everything emits both the desktop and mobile │
│ // variants in the same tree; CSS toggles visibility per viewport. │
│ import type { Root, Element, ElementContent, RootContent, Properties } from "hast │
│ "; │
│ -import { G, len, pad, wrap, type PadSide } from "./ascii"; │
│ +import { G, checkGrid, len, pad, wrap, type PadSide } from "./ascii"; │
│ import { FRAME_W, MOBILE_FRAME_W } from "./config"; │
│ │
│ const FN_RE = /\b([a-zA-Z_]\w*)(?=\s*\()/g; │
│ @@ -79,6 +79,7 @@ function buildBoxChildren(text: string, lang: string, frameW: nu │
│ mber): ElementCo │
│ ]; │
│ │
│ for (const line of lines) { │
│ + checkGrid(line, inner, "code block"); │
│ const padLen = Math.max(0, inner - len(line)); │
│ children.push(frame(`${G.V} `)); │
│ let last = 0; │
│ @@ -120,7 +121,7 @@ function makeCodePre(text: string, lang: string, frameW: numbe │
│ r, extraClass: str │
│ │
│ // --------------------------------------------------------------- table │
│ │
│ -type Cell = { text: string; align: PadSide }; │
│ +export type Cell = { text: string; align: PadSide }; │
│ │
│ function tableCells(tbl: Element): { rows: Cell[][]; headerRows: number } { │
│ const rows: Cell[][] = []; │
│ @@ -144,7 +145,7 @@ function tableCells(tbl: Element): { rows: Cell[][]; headerRow │
│ s: number } { │
│ return { rows, headerRows }; │
│ } │
│ │
│ -function buildAsciiTable(rows: Cell[][], headerRows: number, width: number): stri │
│ ng { │
│ +export function buildAsciiTable(rows: Cell[][], headerRows: number, width: number │
│ ): string { │
│ const nCols = Math.max(...rows.map((r) => r.length)); │
│ const norm = rows.map((r) => { │
│ const out = [...r]; │
│ @@ -185,7 +186,7 @@ function buildAsciiTable(rows: Cell[][], headerRows: number, w │
│ idth: number): str │
│ if (ri === headerRows - 1) out.push(edge(G.LJ, G.X, G.RJ)); │
│ }); │
│ out.push(edge(G.BL, G.BJ, G.BR)); │
│ - return out.join("\n"); │
│ + return checkGrid(out.join("\n"), width, "table"); │
│ } │
│ │
│ function makeTablePre(text: string, extraClass: string): Element { │
│ @@ -225,7 +226,8 @@ function ruleSpan(frameW: number, variant: "frame-wide" | "fra │
│ me-narrow", cls = │
│ } │
│ │
│ function captionSpan(text: string, frameW: number, variant: string): Element { │
│ - return blockSpan("fig-caption", variant, [{ type: "text", value: wrap(text, fra │
│ meW) }]); │
│ + const wrapped = checkGrid(wrap(text, frameW), frameW, "figure caption"); │
│ + return blockSpan("fig-caption", variant, [{ type: "text", value: wrapped }]); │
│ } │
│ │
│ export function rehypeAscii() { │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...esign/src/lib/rehype-ascii.ts ───┐
│ diff --git a/ooknet-design/src/lib/rehype-as │
│ cii.ts b/ooknet-design/src/lib/rehype-ascii. │
│ ts │
│ index c9a4a9c..2a07ace 100644 │
│ --- a/ooknet-design/src/lib/rehype-ascii.ts │
│ +++ b/ooknet-design/src/lib/rehype-ascii.ts │
│ @@ -4,7 +4,7 @@ │
│ // gets a head-rule. Everything emits both │
│ the desktop and mobile │
│ // variants in the same tree; CSS toggles v │
│ isibility per viewport. │
│ import type { Root, Element, ElementContent │
│ , RootContent, Properties } from "hast"; │
│ -import { G, len, pad, wrap, type PadSide } │
│ from "./ascii"; │
│ +import { G, checkGrid, len, pad, wrap, type │
│ PadSide } from "./ascii"; │
│ import { FRAME_W, MOBILE_FRAME_W } from "./ │
│ config"; │
│ │
│ const FN_RE = /\b([a-zA-Z_]\w*)(?=\s*\()/g; │
│ @@ -79,6 +79,7 @@ function buildBoxChildren( │
│ text: string, lang: string, frameW: number): │
│ ElementCo │
│ ]; │
│ │
│ for (const line of lines) { │
│ + checkGrid(line, inner, "code block"); │
│ const padLen = Math.max(0, inner - len( │
│ line)); │
│ children.push(frame(`${G.V} `)); │
│ let last = 0; │
│ @@ -120,7 +121,7 @@ function makeCodePre(tex │
│ t: string, lang: string, frameW: number, ext │
│ raClass: str │
│ │
│ // ---------------------------------------- │
│ ----------------------- table │
│ │
│ -type Cell = { text: string; align: PadSide │
│ }; │
│ +export type Cell = { text: string; align: P │
│ adSide }; │
│ │
│ function tableCells(tbl: Element): { rows: │
│ Cell[][]; headerRows: number } { │
│ const rows: Cell[][] = []; │
│ @@ -144,7 +145,7 @@ function tableCells(tbl: │
│ Element): { rows: Cell[][]; headerRows: num │
│ ber } { │
│ return { rows, headerRows }; │
│ } │
│ │
│ -function buildAsciiTable(rows: Cell[][], he │
│ aderRows: number, width: number): string { │
│ +export function buildAsciiTable(rows: Cell[ │
│ ][], headerRows: number, width: number): str │
│ ing { │
│ const nCols = Math.max(...rows.map((r) => │
│ r.length)); │
│ const norm = rows.map((r) => { │
│ const out = [...r]; │
│ @@ -185,7 +186,7 @@ function buildAsciiTable │
│ (rows: Cell[][], headerRows: number, width: │
│ number): str │
│ if (ri === headerRows - 1) out.push(edg │
│ e(G.LJ, G.X, G.RJ)); │
│ }); │
│ out.push(edge(G.BL, G.BJ, G.BR)); │
│ - return out.join("\n"); │
│ + return checkGrid(out.join("\n"), width, " │
│ table"); │
│ } │
│ │
│ function makeTablePre(text: string, extraCl │
│ ass: string): Element { │
│ @@ -225,7 +226,8 @@ function ruleSpan(frameW │
│ : number, variant: "frame-wide" | "frame-nar │
│ row", cls = │
│ } │
│ │
│ function captionSpan(text: string, frameW: │
│ number, variant: string): Element { │
│ - return blockSpan("fig-caption", variant, │
│ [{ type: "text", value: wrap(text, frameW) } │
│ ]); │
│ + const wrapped = checkGrid(wrap(text, fram │
│ eW), frameW, "figure caption"); │
│ + return blockSpan("fig-caption", variant, │
│ [{ type: "text", value: wrapped }]); │
│ } │
│ │
│ export function rehypeAscii() { │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/types.ts ───────────────────────────────────────────┐
│ diff --git a/ooknet-design/src/lib/types.ts b/ooknet-design/src/lib/types.ts │
│ index 21db1c1..7ca6683 100644 │
│ --- a/ooknet-design/src/lib/types.ts │
│ +++ b/ooknet-design/src/lib/types.ts │
│ @@ -19,4 +19,5 @@ export interface NoteFrontmatter { │
│ filed: string; // e.g. "/INFO/GRAPH/ENTROPY/BOUND" │
│ page?: number; │
│ seeAlso?: string[]; │
│ + draft?: boolean; │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ooknet-design/src/lib/types.ts ─────┐
│ diff --git a/ooknet-design/src/lib/types.ts │
│ b/ooknet-design/src/lib/types.ts │
│ index 21db1c1..7ca6683 100644 │
│ --- a/ooknet-design/src/lib/types.ts │
│ +++ b/ooknet-design/src/lib/types.ts │
│ @@ -19,4 +19,5 @@ export interface NoteFront │
│ matter { │
│ filed: string; // e.g. "/INFO/G │
│ RAPH/ENTROPY/BOUND" │
│ page?: number; │
│ seeAlso?: string[]; │
│ + draft?: boolean; │
│ } │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/pages/index.astro ──────────────────────────────────────┐
│ diff --git a/ooknet-design/src/pages/index.astro b/ooknet-design/src/pages/index.a │
│ stro │
│ index e2ba75c..be49b27 100644 │
│ --- a/ooknet-design/src/pages/index.astro │
│ +++ b/ooknet-design/src/pages/index.astro │
│ @@ -4,10 +4,9 @@ import ArticleLayout from "../layouts/ArticleLayout/ArticleLayout │
│ .astro"; │
│ import TopHeader from "../components/TopHeader/TopHeader.astro"; │
│ import Frame from "../components/Frame/Frame.astro"; │
│ import { rule, ruleD, kv, row2 } from "../lib/ascii"; │
│ +import { published, byNoteNum } from "../lib/notes"; │
│ │
│ -const notes = await getCollection("notes", ({ data }) => !data.draft); │
│ -const noteNum = (note: string) => parseInt(note.replace(/\D/g, ""), 10) || 0; │
│ -notes.sort((a, b) => noteNum(b.data.note) - noteNum(a.data.note)); │
│ +const notes = (await getCollection("notes", published)).sort(byNoteNum).reverse() │
│ ; │
│ │
│ const buildIndexHeader = (w: number) => [ │
│ row2("INDEX OF NOTES", `${notes.length} entries`, w), │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...-design/src/pages/index.astro ───┐
│ diff --git a/ooknet-design/src/pages/index.a │
│ stro b/ooknet-design/src/pages/index.astro │
│ index e2ba75c..be49b27 100644 │
│ --- a/ooknet-design/src/pages/index.astro │
│ +++ b/ooknet-design/src/pages/index.astro │
│ @@ -4,10 +4,9 @@ import ArticleLayout from " │
│ ../layouts/ArticleLayout/ArticleLayout.astro │
│ "; │
│ import TopHeader from "../components/TopHea │
│ der/TopHeader.astro"; │
│ import Frame from "../components/Frame/Fram │
│ e.astro"; │
│ import { rule, ruleD, kv, row2 } from "../l │
│ ib/ascii"; │
│ +import { published, byNoteNum } from "../li │
│ b/notes"; │
│ │
│ -const notes = await getCollection("notes", │
│ ({ data }) => !data.draft); │
│ -const noteNum = (note: string) => parseInt( │
│ note.replace(/\D/g, ""), 10) || 0; │
│ -notes.sort((a, b) => noteNum(b.data.note) - │
│ noteNum(a.data.note)); │
│ +const notes = (await getCollection("notes", │
│ published)).sort(byNoteNum).reverse(); │
│ │
│ const buildIndexHeader = (w: number) => [ │
│ row2("INDEX OF NOTES", `${notes.length} e │
│ ntries`, w), │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/pages/notes/[slug].astro ───────────────────────────────┐
│ diff --git a/ooknet-design/src/pages/notes/[slug].astro b/ooknet-design/src/pages/ │
│ notes/[slug].astro │
│ index 343b055..0530758 100644 │
│ --- a/ooknet-design/src/pages/notes/[slug].astro │
│ +++ b/ooknet-design/src/pages/notes/[slug].astro │
│ @@ -12,11 +12,10 @@ import ArticleFooter from "../../components/ArticleFooter/Arti │
│ cleFooter.astro"; │
│ import Prose from "../../components/Prose/Prose.astro"; │
│ import Pre from "../../components/Pre/Pre.astro"; │
│ import type { PageLink } from "../../components/ArticlePagination/ArticlePaginati │
│ on"; │
│ +import { published, byNoteNum } from "../../lib/notes"; │
│ │
│ export async function getStaticPaths() { │
│ - const notes = await getCollection("notes", ({ data }) => !data.draft); │
│ - const num = (s: string) => parseInt(s.replace(/\D/g, ""), 10) || 0; │
│ - notes.sort((a, b) => num(a.data.note) - num(b.data.note)); │
│ + const notes = (await getCollection("notes", published)).sort(byNoteNum); │
│ const link = (n?: (typeof notes)[number]): PageLink | null => │
│ n ? { id: n.id, note: n.data.note } : null; │
│ return notes.map((n, i) => ({ │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ .../src/pages/notes/[slug].astro ───┐
│ diff --git a/ooknet-design/src/pages/notes/[ │
│ slug].astro b/ooknet-design/src/pages/notes/ │
│ [slug].astro │
│ index 343b055..0530758 100644 │
│ --- a/ooknet-design/src/pages/notes/[slug].a │
│ stro │
│ +++ b/ooknet-design/src/pages/notes/[slug].a │
│ stro │
│ @@ -12,11 +12,10 @@ import ArticleFooter fro │
│ m "../../components/ArticleFooter/ArticleFoo │
│ ter.astro"; │
│ import Prose from "../../components/Prose/P │
│ rose.astro"; │
│ import Pre from "../../components/Pre/Pre.a │
│ stro"; │
│ import type { PageLink } from "../../compon │
│ ents/ArticlePagination/ArticlePagination"; │
│ +import { published, byNoteNum } from "../.. │
│ /lib/notes"; │
│ │
│ export async function getStaticPaths() { │
│ - const notes = await getCollection("notes" │
│ , ({ data }) => !data.draft); │
│ - const num = (s: string) => parseInt(s.rep │
│ lace(/\D/g, ""), 10) || 0; │
│ - notes.sort((a, b) => num(a.data.note) - n │
│ um(b.data.note)); │
│ + const notes = (await getCollection("notes │
│ ", published)).sort(byNoteNum); │
│ const link = (n?: (typeof notes)[number]) │
│ : PageLink | null => │
│ n ? { id: n.id, note: n.data.note } : n │
│ ull; │
│ return notes.map((n, i) => ({ │
└──────────────────────────────────────────────┘
┌─ ooknet-design/src/pages/search.json.ts ───────────────────────────────────┐
│ diff --git a/ooknet-design/src/pages/search.json.ts b/ooknet-design/src/pages/sear │
│ ch.json.ts │
│ index 66cdaf3..5805975 100644 │
│ --- a/ooknet-design/src/pages/search.json.ts │
│ +++ b/ooknet-design/src/pages/search.json.ts │
│ @@ -1,9 +1,10 @@ │
│ // Static index consumed by the masthead fuzzy search. │
│ import type { APIRoute } from "astro"; │
│ import { getCollection } from "astro:content"; │
│ +import { published, byNoteNum } from "../lib/notes"; │
│ │
│ export const GET: APIRoute = async () => { │
│ - const notes = await getCollection("notes", ({ data }) => !data.draft); │
│ + const notes = (await getCollection("notes", published)).sort(byNoteNum).reverse │
│ (); │
│ const data = notes.map((n) => ({ │
│ id: n.id, │
│ note: n.data.note, │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...sign/src/pages/search.json.ts ───┐
│ diff --git a/ooknet-design/src/pages/search. │
│ json.ts b/ooknet-design/src/pages/search.jso │
│ n.ts │
│ index 66cdaf3..5805975 100644 │
│ --- a/ooknet-design/src/pages/search.json.ts │
│ +++ b/ooknet-design/src/pages/search.json.ts │
│ @@ -1,9 +1,10 @@ │
│ // Static index consumed by the masthead fu │
│ zzy search. │
│ import type { APIRoute } from "astro"; │
│ import { getCollection } from "astro:conten │
│ t"; │
│ +import { published, byNoteNum } from "../li │
│ b/notes"; │
│ │
│ export const GET: APIRoute = async () => { │
│ - const notes = await getCollection("notes" │
│ , ({ data }) => !data.draft); │
│ + const notes = (await getCollection("notes │
│ ", published)).sort(byNoteNum).reverse(); │
│ const data = notes.map((n) => ({ │
│ id: n.id, │
│ note: n.data.note, │
└──────────────────────────────────────────────┘
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET