From 7cfd5b4069df72129fe356f259ead203e2755549 Mon Sep 17 00:00:00 2001 From: vince Date: Thu, 18 Jun 2026 17:37:42 +0200 Subject: [PATCH 01/90] feat: define dashboard model schema --- bun.lock | 68 ++++++++ package.json | 11 +- src/lib/model/fixtures/dimensionlab.ts | 162 +++++++++++++++++ src/lib/model/fixtures/generic.ts | 91 ++++++++++ src/lib/model/fixtures/index.ts | 2 + src/lib/model/index.ts | 24 +++ src/lib/model/schema.test.ts | 63 +++++++ src/lib/model/schema.ts | 229 +++++++++++++++++++++++++ src/lib/model/validation.ts | 66 +++++++ 9 files changed, 714 insertions(+), 2 deletions(-) create mode 100644 src/lib/model/fixtures/dimensionlab.ts create mode 100644 src/lib/model/fixtures/generic.ts create mode 100644 src/lib/model/fixtures/index.ts create mode 100644 src/lib/model/index.ts create mode 100644 src/lib/model/schema.test.ts create mode 100644 src/lib/model/schema.ts create mode 100644 src/lib/model/validation.ts diff --git a/bun.lock b/bun.lock index 00016cd..267f7f4 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,11 @@ "workspaces": { "": { "name": "dimensionlab-website", + "dependencies": { + "@sinclair/typebox": "^0.34.49", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + }, "devDependencies": { "@sveltejs/adapter-node": "^5.5.4", "@sveltejs/kit": "^2.65.2", @@ -13,6 +18,7 @@ "svelte-check": "^4.6.0", "typescript": "^6.0.3", "vite": "^8.0.16", + "vitest": "^4.1.9", }, }, }, @@ -129,6 +135,8 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.0", "", { "os": "win32", "cpu": "x64" }, "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.10", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA=="], @@ -143,8 +151,12 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], @@ -153,18 +165,42 @@ "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.9", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A=="], + + "@vitest/runner": ["@vitest/runner@4.1.9", "", { "dependencies": { "@vitest/utils": "4.1.9", "pathe": "^2.0.3" } }, "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], + + "@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], + + "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], @@ -175,12 +211,20 @@ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], "esrap": ["esrap@2.2.11", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ=="], "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -195,6 +239,8 @@ "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -235,6 +281,8 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], @@ -243,6 +291,8 @@ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], "rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="], @@ -253,18 +303,30 @@ "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "svelte": ["svelte@5.56.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.11", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA=="], "svelte-check": ["svelte-check@4.6.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "0.1.1", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-KhVnDFDSid57mmZtHz8gfW8AAGylOZ0vPnOIzVmAL+urzwK8sBYXRss953gD8T0OdgAQ11mdWhE6uadmtOz8TQ=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -277,8 +339,14 @@ "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], + "vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "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" }, "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.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], "@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], + + "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], } } diff --git a/package.json b/package.json index a52402b..0cdc69e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,13 @@ "dev": "vite --host 0.0.0.0", "build": "vite build", "preview": "vite preview --host 0.0.0.0", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "test": "vitest run" + }, + "dependencies": { + "@sinclair/typebox": "^0.34.49", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1" }, "devDependencies": { "@sveltejs/adapter-node": "^5.5.4", @@ -17,6 +23,7 @@ "svelte": "^5.56.3", "svelte-check": "^4.6.0", "typescript": "^6.0.3", - "vite": "^8.0.16" + "vite": "^8.0.16", + "vitest": "^4.1.9" } } diff --git a/src/lib/model/fixtures/dimensionlab.ts b/src/lib/model/fixtures/dimensionlab.ts new file mode 100644 index 0000000..316e9a8 --- /dev/null +++ b/src/lib/model/fixtures/dimensionlab.ts @@ -0,0 +1,162 @@ +import { + DASHBOARD_SCHEMA_VERSION, + type DashboardDocument, +} from "../schema"; + +export const dimensionLabDashboardFixture: DashboardDocument = { + schemaVersion: DASHBOARD_SCHEMA_VERSION, + metadata: { + title: "System Overview", + subtitle: "Capacity, noise & top consumers", + description: "Dimension Lab operational dashboard seed data.", + timezone: "Europe/Amsterdam", + refreshIntervalSeconds: 15, + }, + layout: { + density: "dense", + telemetry: [ + "infra-ram", + "gpu-host-ram", + "network-ram", + "user-disk-peak", + "system-disk-peak", + "peak-cpu-busy", + "top-cpu-container", + "top-ram-container", + "gpu-3060-load", + "gpu-3060-vram", + "gpu-3060-temp", + "gpu-3060-fan", + "gpu-3090-load", + "gpu-3090-vram", + "gpu-3090-temp", + "gpu-3090-fan", + ], + serviceGroups: ["essentials", "monitoring", "ai-automation", "systems", "runtime-health"], + statusStrips: ["footer-status"], + modules: ["weather"], + }, + telemetry: [ + metric("infra-ram", "Infra RAM", "percent", 19, "linux-infra", "ok"), + metric("gpu-host-ram", "GPU Host RAM", "percent", 17, "linux", "ok"), + metric("network-ram", "Network RAM", "percent", 5, "network-core", "ok"), + metric("user-disk-peak", "User Disk Peak", "percent", 29, "linux /home", "ok"), + metric("system-disk-peak", "System Disk Peak", "percent", 49, "linux /boot", "ok"), + metric("peak-cpu-busy", "Peak CPU Busy", "percent", 3, "linux-infra", "ok"), + metric("top-cpu-container", "Top CPU Container", "percent", 7.9, "container placeholder", "ok"), + metric("top-ram-container", "Top RAM Container", "bytes", "5.2 GB", "container placeholder", "danger"), + metric("gpu-3060-load", "3060 GPU Load", "percent", 0, "RTX 3060", "ok"), + metric("gpu-3060-vram", "3060 VRAM", "percent", 0, "RTX 3060", "ok"), + metric("gpu-3060-temp", "3060 Temp", "temperature", 54, "RTX 3060", "ok"), + metric("gpu-3060-fan", "3060 Fan Spin", "percent", 0, "RTX 3060", "ok"), + metric("gpu-3090-load", "3090 GPU Load", "percent", 0, "RTX 3090", "ok"), + metric("gpu-3090-vram", "3090 VRAM", "percent", 74, "RTX 3090", "warning"), + metric("gpu-3090-temp", "3090 Temp", "temperature", 50, "RTX 3090", "ok"), + metric("gpu-3090-fan", "3090 Fan Spin", "percent", 0, "RTX 3090", "ok"), + ], + serviceGroups: [ + group("essentials", "Essentials", [ + service("vaultwarden", "Vaultwarden", "Password manager", "simple-icons:vaultwarden", "https://vault.dimensionlab.net"), + service("forgejo", "Forgejo", "Git repositories", "simple-icons:forgejo", "https://git.dimensionlab.net"), + service("wiki", "Wiki", "Internal documentation", "simple-icons:wikidotjs", "https://wiki.dimensionlab.net"), + service("aws-start", "AWS Start", "AWS access portal", "simple-icons:amazonaws", "https://dimensionlab.awsapps.com/start"), + ]), + group("monitoring", "Monitoring", [ + service("grafana", "Grafana", "Capacity and noise dashboard", "simple-icons:grafana", "https://grafana.dimensionlab.net"), + service("uptime-kuma", "Uptime Kuma", "Service uptime checks", "simple-icons:uptimekuma", "https://uptime.dimensionlab.net"), + service("prometheus", "Prometheus", "Metrics database", "simple-icons:prometheus", "https://prometheus.dimensionlab.net"), + service("backrest", "Backrest", "Restic backup manager", "mdi:backup-restore", "https://backups.dimensionlab.net"), + ]), + group("ai-automation", "AI & Automation", [ + service("n8n", "n8n", "Workflow automation", "simple-icons:n8n", "https://workflows.dimensionlab.net"), + service("open-webui", "Open WebUI", "Chat and model interface", "simple-icons:openwebui", "https://webui.dimensionlab.net"), + service("comfyui", "ComfyUI", "Image generation workflows", "mdi:image-edit-outline", "https://comfy.dimensionlab.net"), + service("models", "Models", "Local model management", "mdi:brain", "https://models.dimensionlab.net"), + ]), + group("systems", "Systems", [ + service("adminer", "Adminer", "PostgreSQL database browser", "simple-icons:adminer", "https://db.dimensionlab.net"), + service("assistant", "Assistant", "Personal AI agent gateway", "mdi:robot-outline", "https://assistant.dimensionlab.net"), + service("suna", "Suna", "AI command center", "mdi:account-hard-hat-outline", "https://suna.dimensionlab.net"), + service("cockpit-infra", "Cockpit Infra", "linux-infra server console", "simple-icons:cockpit", "https://infra-cockpit.dimensionlab.net"), + ]), + group("runtime-health", "Runtime Health", [ + service("forgejo-ssh-relay", "Forgejo SSH Relay", "Public Git SSH relay", "simple-icons:forgejo", "https://uptime.dimensionlab.net/status/dimensionlab"), + service("postgresql", "PostgreSQL", "Shared application database", "simple-icons:postgresql"), + service("ollama-api", "Ollama API", "Local model API", "simple-icons:ollama"), + service("node-exporter", "Node Exporter", "Host metrics exporter", "simple-icons:prometheus"), + ]), + ], + statusStrips: [ + { + id: "footer-status", + items: [ + { id: "system-status", label: "System Status", value: "All systems operational", severity: "ok" }, + { id: "last-sync", label: "Last Sync", value: "2 minutes ago", severity: "stale" }, + { id: "uptime", label: "Uptime", value: "28d 14h 32m", severity: "ok" }, + { id: "load-avg", label: "Load Avg", value: "0.47 0.53 0.59", severity: "neutral" }, + ], + }, + ], + modules: [ + { + id: "weather", + kind: "weather", + title: "Amsterdam", + value: "28.3 C", + detail: "Clear", + icon: "mdi:weather-sunny", + severity: "ok", + datasource: { type: "placeholder", reason: "weather adapter pending" }, + }, + ], +}; + +type ValueKind = "percent" | "bytes" | "temperature" | "latency" | "number" | "text"; +type Severity = "neutral" | "ok" | "warning" | "danger" | "stale" | "unavailable"; + +function metric( + id: string, + label: string, + kind: ValueKind, + value: number | string, + detail: string, + severity: Severity, +) { + return { + id, + label, + value: { kind, value }, + detail, + severity, + datasource: { type: "placeholder" as const, reason: "live datasource pending" }, + sparkline: [10, 18, 15, 28, 24, 32], + }; +} + +function service( + id: string, + label: string, + description: string, + icon: string, + href?: string, +) { + return { + id, + label, + description, + icon, + link: href ? { href, external: true } : undefined, + severity: "ok" as const, + detail: "ready", + datasource: { type: "placeholder" as const, reason: "service health adapter pending" }, + }; +} + +function group(id: string, title: string, services: ReturnType[]) { + return { + id, + title, + layout: "list" as const, + services, + }; +} diff --git a/src/lib/model/fixtures/generic.ts b/src/lib/model/fixtures/generic.ts new file mode 100644 index 0000000..ed4fa16 --- /dev/null +++ b/src/lib/model/fixtures/generic.ts @@ -0,0 +1,91 @@ +import { + DASHBOARD_SCHEMA_VERSION, + type DashboardDocument, +} from "../schema"; + +export const genericDashboardFixture: DashboardDocument = { + schemaVersion: DASHBOARD_SCHEMA_VERSION, + metadata: { + title: "Operations Console", + subtitle: "Generic environment", + description: "Portable fixture for component and renderer tests.", + timezone: "UTC", + refreshIntervalSeconds: 30, + }, + layout: { + density: "dense", + telemetry: ["service-uptime", "queue-depth"], + serviceGroups: ["core-services"], + statusStrips: ["runtime"], + modules: ["ambient"], + }, + telemetry: [ + { + id: "service-uptime", + label: "Service Uptime", + value: { kind: "percent", value: 99.9, precision: 1 }, + detail: "last 30 days", + severity: "ok", + datasource: { type: "static", label: "fixture" }, + sparkline: [99.7, 99.8, 99.9, 99.9], + }, + { + id: "queue-depth", + label: "Queue Depth", + value: { kind: "number", value: 18 }, + detail: "pending jobs", + severity: "warning", + thresholds: { warning: 15, danger: 50 }, + datasource: { type: "placeholder", reason: "adapter pending" }, + sparkline: [6, 11, 13, 18], + }, + ], + serviceGroups: [ + { + id: "core-services", + title: "Core Services", + layout: "list", + services: [ + { + id: "identity", + label: "Identity", + description: "Authentication and profile service", + icon: "mdi:account-key-outline", + severity: "ok", + detail: "ready", + datasource: { type: "static", label: "fixture" }, + }, + { + id: "scheduler", + label: "Scheduler", + description: "Background task coordinator", + icon: "mdi:calendar-clock", + severity: "warning", + detail: "delayed", + datasource: { type: "placeholder", reason: "health adapter pending" }, + }, + ], + }, + ], + statusStrips: [ + { + id: "runtime", + items: [ + { id: "status", label: "System Status", value: "Degraded", severity: "warning" }, + { id: "sync", label: "Last Sync", value: "2 minutes ago", severity: "stale" }, + ], + }, + ], + modules: [ + { + id: "ambient", + kind: "summary", + title: "Environment", + value: "Nominal", + detail: "static fixture", + icon: "mdi:radar", + severity: "ok", + datasource: { type: "static", label: "fixture" }, + }, + ], +}; diff --git a/src/lib/model/fixtures/index.ts b/src/lib/model/fixtures/index.ts new file mode 100644 index 0000000..a04278d --- /dev/null +++ b/src/lib/model/fixtures/index.ts @@ -0,0 +1,2 @@ +export { dimensionLabDashboardFixture } from "./dimensionlab"; +export { genericDashboardFixture } from "./generic"; diff --git a/src/lib/model/index.ts b/src/lib/model/index.ts new file mode 100644 index 0000000..1a028cc --- /dev/null +++ b/src/lib/model/index.ts @@ -0,0 +1,24 @@ +export { + DASHBOARD_SCHEMA_VERSION, + DashboardDocumentSchema, + dashboardDocumentJsonSchema, + type DashboardDocument, + type DashboardModule, + type DatasourceReference, + type MetricValue, + type ServiceEntry, + type ServiceGroup, + type Severity, + type StatusItem, + type StatusStrip, + type TelemetryCard, +} from "./schema"; +export { + assertDashboardDocument, + formatValidationErrors, + isDashboardDocument, + validateDashboardDocument, + type DashboardValidationFailure, + type DashboardValidationResult, + type DashboardValidationSuccess, +} from "./validation"; diff --git a/src/lib/model/schema.test.ts b/src/lib/model/schema.test.ts new file mode 100644 index 0000000..40f9baa --- /dev/null +++ b/src/lib/model/schema.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import { + DASHBOARD_SCHEMA_VERSION, + dashboardDocumentJsonSchema, + validateDashboardDocument, +} from "."; +import { + dimensionLabDashboardFixture, + genericDashboardFixture, +} from "./fixtures"; + +describe("dashboard model validation", () => { + it("accepts the generic dashboard fixture", () => { + const result = validateDashboardDocument(genericDashboardFixture); + + expect(result.valid).toBe(true); + if (result.valid) { + expect(result.data.schemaVersion).toBe(DASHBOARD_SCHEMA_VERSION); + } + }); + + it("accepts the Dimension Lab dashboard fixture", () => { + const result = validateDashboardDocument(dimensionLabDashboardFixture); + + expect(result.valid).toBe(true); + }); + + it("rejects documents with an unsupported schema version", () => { + const invalid = { + ...genericDashboardFixture, + schemaVersion: "dashboard.v0", + }; + + const result = validateDashboardDocument(invalid); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors.join(" ")).toContain("must be equal to constant"); + expect(result.details.length).toBeGreaterThan(0); + } + }); + + it("returns actionable field paths for invalid documents", () => { + const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); + delete invalid.metadata.title; + invalid.telemetry[0].severity = "fine"; + + const result = validateDashboardDocument(invalid); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors.some((error) => error.includes("/metadata"))).toBe(true); + expect(result.errors.some((error) => error.includes("/telemetry/0/severity"))).toBe(true); + } + }); + + it("exports JSON Schema for external tool contracts", () => { + expect(dashboardDocumentJsonSchema.$id).toContain("dashboard-document.v1"); + expect(dashboardDocumentJsonSchema.properties).toHaveProperty("schemaVersion"); + expect(dashboardDocumentJsonSchema.properties).toHaveProperty("telemetry"); + expect(dashboardDocumentJsonSchema.properties).toHaveProperty("serviceGroups"); + }); +}); diff --git a/src/lib/model/schema.ts b/src/lib/model/schema.ts new file mode 100644 index 0000000..388e921 --- /dev/null +++ b/src/lib/model/schema.ts @@ -0,0 +1,229 @@ +import { Type, type Static } from "@sinclair/typebox"; + +export const DASHBOARD_SCHEMA_VERSION = "dashboard.v1" as const; + +const IdentifierSchema = Type.String({ + minLength: 1, + pattern: "^[a-z0-9][a-z0-9-_.:]*$", +}); + +const SeveritySchema = Type.Union([ + Type.Literal("neutral"), + Type.Literal("ok"), + Type.Literal("warning"), + Type.Literal("danger"), + Type.Literal("stale"), + Type.Literal("unavailable"), +]); + +const IconReferenceSchema = Type.String({ minLength: 1 }); + +const LinkSchema = Type.Object( + { + href: Type.String({ format: "uri" }), + label: Type.Optional(Type.String({ minLength: 1 })), + external: Type.Optional(Type.Boolean()), + }, + { additionalProperties: false }, +); + +const StaticDatasourceSchema = Type.Object( + { + type: Type.Literal("static"), + label: Type.Optional(Type.String({ minLength: 1 })), + updatedAt: Type.Optional(Type.String({ format: "date-time" })), + }, + { additionalProperties: false }, +); + +const PlaceholderDatasourceSchema = Type.Object( + { + type: Type.Literal("placeholder"), + reason: Type.String({ minLength: 1 }), + }, + { additionalProperties: false }, +); + +const ExternalDatasourceSchema = Type.Object( + { + type: Type.Literal("external"), + adapter: Type.Union([ + Type.Literal("prometheus"), + Type.Literal("http-status"), + Type.Literal("weather"), + Type.Literal("custom"), + ]), + reference: Type.String({ minLength: 1 }), + }, + { additionalProperties: false }, +); + +const DatasourceReferenceSchema = Type.Union([ + StaticDatasourceSchema, + PlaceholderDatasourceSchema, + ExternalDatasourceSchema, +]); + +const MetricValueSchema = Type.Object( + { + kind: Type.Union([ + Type.Literal("percent"), + Type.Literal("bytes"), + Type.Literal("temperature"), + Type.Literal("latency"), + Type.Literal("number"), + Type.Literal("text"), + ]), + value: Type.Union([Type.Number(), Type.String()]), + unit: Type.Optional(Type.String({ minLength: 1 })), + precision: Type.Optional(Type.Integer({ minimum: 0, maximum: 4 })), + }, + { additionalProperties: false }, +); + +const ThresholdSchema = Type.Object( + { + warning: Type.Optional(Type.Number()), + danger: Type.Optional(Type.Number()), + }, + { additionalProperties: false }, +); + +export const TelemetryCardSchema = Type.Object( + { + id: IdentifierSchema, + label: Type.String({ minLength: 1 }), + description: Type.Optional(Type.String()), + icon: Type.Optional(IconReferenceSchema), + value: MetricValueSchema, + detail: Type.Optional(Type.String()), + severity: SeveritySchema, + thresholds: Type.Optional(ThresholdSchema), + datasource: Type.Optional(DatasourceReferenceSchema), + sparkline: Type.Optional(Type.Array(Type.Number())), + }, + { additionalProperties: false }, +); + +export const ServiceEntrySchema = Type.Object( + { + id: IdentifierSchema, + label: Type.String({ minLength: 1 }), + description: Type.String(), + icon: Type.Optional(IconReferenceSchema), + link: Type.Optional(LinkSchema), + severity: SeveritySchema, + detail: Type.Optional(Type.String()), + datasource: Type.Optional(DatasourceReferenceSchema), + }, + { additionalProperties: false }, +); + +export const ServiceGroupSchema = Type.Object( + { + id: IdentifierSchema, + title: Type.String({ minLength: 1 }), + layout: Type.Optional(Type.Union([Type.Literal("list"), Type.Literal("grid")])), + services: Type.Array(ServiceEntrySchema), + }, + { additionalProperties: false }, +); + +export const StatusItemSchema = Type.Object( + { + id: IdentifierSchema, + label: Type.String({ minLength: 1 }), + value: Type.String(), + severity: Type.Optional(SeveritySchema), + link: Type.Optional(LinkSchema), + }, + { additionalProperties: false }, +); + +export const StatusStripSchema = Type.Object( + { + id: IdentifierSchema, + title: Type.Optional(Type.String({ minLength: 1 })), + items: Type.Array(StatusItemSchema), + }, + { additionalProperties: false }, +); + +export const DashboardModuleSchema = Type.Object( + { + id: IdentifierSchema, + kind: Type.Union([ + Type.Literal("summary"), + Type.Literal("weather"), + Type.Literal("custom"), + ]), + title: Type.Optional(Type.String({ minLength: 1 })), + label: Type.Optional(Type.String()), + value: Type.Optional(Type.String()), + detail: Type.Optional(Type.String()), + icon: Type.Optional(IconReferenceSchema), + severity: Type.Optional(SeveritySchema), + datasource: Type.Optional(DatasourceReferenceSchema), + }, + { additionalProperties: false }, +); + +const LayoutSchema = Type.Object( + { + density: Type.Optional(Type.Union([Type.Literal("compact"), Type.Literal("dense")])), + telemetry: Type.Array(IdentifierSchema), + serviceGroups: Type.Array(IdentifierSchema), + statusStrips: Type.Array(IdentifierSchema), + modules: Type.Optional(Type.Array(IdentifierSchema)), + }, + { additionalProperties: false }, +); + +const MetadataSchema = Type.Object( + { + title: Type.String({ minLength: 1 }), + subtitle: Type.Optional(Type.String()), + description: Type.Optional(Type.String()), + timezone: Type.Optional(Type.String({ minLength: 1 })), + refreshIntervalSeconds: Type.Optional(Type.Integer({ minimum: 5 })), + }, + { additionalProperties: false }, +); + +export const DashboardDocumentSchema = Type.Object( + { + schemaVersion: Type.Literal(DASHBOARD_SCHEMA_VERSION), + metadata: MetadataSchema, + layout: LayoutSchema, + telemetry: Type.Array(TelemetryCardSchema), + serviceGroups: Type.Array(ServiceGroupSchema), + statusStrips: Type.Array(StatusStripSchema), + modules: Type.Optional(Type.Array(DashboardModuleSchema)), + migration: Type.Optional( + Type.Object( + { + previousVersion: Type.Optional(Type.String({ minLength: 1 })), + notes: Type.Optional(Type.String()), + }, + { additionalProperties: false }, + ), + ), + }, + { + $id: "https://dimensionlab.net/schemas/dashboard-document.v1.json", + additionalProperties: false, + }, +); + +export type Severity = Static; +export type DatasourceReference = Static; +export type MetricValue = Static; +export type TelemetryCard = Static; +export type ServiceEntry = Static; +export type ServiceGroup = Static; +export type StatusItem = Static; +export type StatusStrip = Static; +export type DashboardModule = Static; +export type DashboardDocument = Static; + +export const dashboardDocumentJsonSchema = DashboardDocumentSchema; diff --git a/src/lib/model/validation.ts b/src/lib/model/validation.ts new file mode 100644 index 0000000..2059f3c --- /dev/null +++ b/src/lib/model/validation.ts @@ -0,0 +1,66 @@ +import Ajv, { type ErrorObject } from "ajv"; +import addFormats from "ajv-formats"; +import { + DashboardDocumentSchema, + type DashboardDocument, +} from "./schema"; + +export interface DashboardValidationFailure { + valid: false; + errors: string[]; + details: ErrorObject[]; +} + +export interface DashboardValidationSuccess { + valid: true; + data: DashboardDocument; +} + +export type DashboardValidationResult = + | DashboardValidationFailure + | DashboardValidationSuccess; + +const ajv = addFormats( + new Ajv({ + allErrors: true, + strict: false, + }), +); + +const validateDashboard = ajv.compile(DashboardDocumentSchema); + +export function formatValidationErrors(errors: ErrorObject[] = []): string[] { + return errors.map((error) => { + const path = error.instancePath || "/"; + const message = error.message || "is invalid"; + return `${path} ${message}`; + }); +} + +export function validateDashboardDocument( + value: unknown, +): DashboardValidationResult { + if (validateDashboard(value)) { + return { valid: true, data: value }; + } + + const details = [...(validateDashboard.errors || [])]; + return { + valid: false, + errors: formatValidationErrors(details), + details, + }; +} + +export function assertDashboardDocument( + value: unknown, +): asserts value is DashboardDocument { + const result = validateDashboardDocument(value); + if (!result.valid) { + throw new Error(`Invalid dashboard document: ${result.errors.join("; ")}`); + } +} + +export function isDashboardDocument(value: unknown): value is DashboardDocument { + return validateDashboardDocument(value).valid; +} From 74651a49de423f8930d7aaca6f60517ce178cb7b Mon Sep 17 00:00:00 2001 From: vince Date: Thu, 18 Jun 2026 17:43:55 +0200 Subject: [PATCH 02/90] fix: harden dashboard model validation --- src/lib/model/fixtures/dimensionlab.ts | 8 +- src/lib/model/schema.test.ts | 71 +++++++++ src/lib/model/schema.ts | 21 ++- src/lib/model/validation.ts | 201 ++++++++++++++++++++++++- 4 files changed, 288 insertions(+), 13 deletions(-) diff --git a/src/lib/model/fixtures/dimensionlab.ts b/src/lib/model/fixtures/dimensionlab.ts index 316e9a8..572067f 100644 --- a/src/lib/model/fixtures/dimensionlab.ts +++ b/src/lib/model/fixtures/dimensionlab.ts @@ -44,7 +44,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { metric("system-disk-peak", "System Disk Peak", "percent", 49, "linux /boot", "ok"), metric("peak-cpu-busy", "Peak CPU Busy", "percent", 3, "linux-infra", "ok"), metric("top-cpu-container", "Top CPU Container", "percent", 7.9, "container placeholder", "ok"), - metric("top-ram-container", "Top RAM Container", "bytes", "5.2 GB", "container placeholder", "danger"), + metric("top-ram-container", "Top RAM Container", "bytes", 5.2 * 1024 ** 3, "container placeholder", "danger"), metric("gpu-3060-load", "3060 GPU Load", "percent", 0, "RTX 3060", "ok"), metric("gpu-3060-vram", "3060 VRAM", "percent", 0, "RTX 3060", "ok"), metric("gpu-3060-temp", "3060 Temp", "temperature", 54, "RTX 3060", "ok"), @@ -111,14 +111,14 @@ export const dimensionLabDashboardFixture: DashboardDocument = { ], }; -type ValueKind = "percent" | "bytes" | "temperature" | "latency" | "number" | "text"; +type NumericValueKind = "percent" | "bytes" | "temperature" | "latency" | "number"; type Severity = "neutral" | "ok" | "warning" | "danger" | "stale" | "unavailable"; function metric( id: string, label: string, - kind: ValueKind, - value: number | string, + kind: NumericValueKind, + value: number, detail: string, severity: Severity, ) { diff --git a/src/lib/model/schema.test.ts b/src/lib/model/schema.test.ts index 40f9baa..0f3153b 100644 --- a/src/lib/model/schema.test.ts +++ b/src/lib/model/schema.test.ts @@ -54,6 +54,77 @@ describe("dashboard model validation", () => { } }); + it("includes offending additional property names in errors", () => { + const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); + invalid.metadata.unexpected = true; + + const result = validateDashboardDocument(invalid); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors.join(" ")).toContain("unexpected"); + } + }); + + it("rejects non-finite numbers that cannot roundtrip through JSON", () => { + const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); + invalid.telemetry[0].value.value = Number.NaN; + + const result = validateDashboardDocument(invalid); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors.join(" ")).toContain("finite JSON number"); + } + }); + + it("rejects dangling layout references", () => { + const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); + invalid.layout.telemetry.push("missing-card"); + + const result = validateDashboardDocument(invalid); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors.join(" ")).toContain("missing-card"); + expect(result.errors.join(" ")).toContain("must reference an existing item"); + } + }); + + it("rejects duplicate IDs within collections", () => { + const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); + invalid.telemetry[1].id = invalid.telemetry[0].id; + + const result = validateDashboardDocument(invalid); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors.join(" ")).toContain("must be unique"); + expect(result.errors.join(" ")).toContain(invalid.telemetry[0].id); + } + }); + + it("rejects string values for numeric metric kinds", () => { + const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); + invalid.telemetry[0].value.value = "not a percent"; + + const result = validateDashboardDocument(invalid); + + expect(result.valid).toBe(false); + }); + + it("rejects contradictory warning and danger thresholds", () => { + const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); + invalid.telemetry[0].thresholds = { warning: 90, danger: 80 }; + + const result = validateDashboardDocument(invalid); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors.join(" ")).toContain("warning threshold"); + } + }); + it("exports JSON Schema for external tool contracts", () => { expect(dashboardDocumentJsonSchema.$id).toContain("dashboard-document.v1"); expect(dashboardDocumentJsonSchema.properties).toHaveProperty("schemaVersion"); diff --git a/src/lib/model/schema.ts b/src/lib/model/schema.ts index 388e921..d338468 100644 --- a/src/lib/model/schema.ts +++ b/src/lib/model/schema.ts @@ -64,7 +64,7 @@ const DatasourceReferenceSchema = Type.Union([ ExternalDatasourceSchema, ]); -const MetricValueSchema = Type.Object( +const NumericMetricValueSchema = Type.Object( { kind: Type.Union([ Type.Literal("percent"), @@ -72,21 +72,34 @@ const MetricValueSchema = Type.Object( Type.Literal("temperature"), Type.Literal("latency"), Type.Literal("number"), - Type.Literal("text"), ]), - value: Type.Union([Type.Number(), Type.String()]), + value: Type.Number(), unit: Type.Optional(Type.String({ minLength: 1 })), precision: Type.Optional(Type.Integer({ minimum: 0, maximum: 4 })), }, { additionalProperties: false }, ); +const TextMetricValueSchema = Type.Object( + { + kind: Type.Literal("text"), + value: Type.String(), + unit: Type.Optional(Type.String({ minLength: 1 })), + }, + { additionalProperties: false }, +); + +const MetricValueSchema = Type.Union([ + NumericMetricValueSchema, + TextMetricValueSchema, +]); + const ThresholdSchema = Type.Object( { warning: Type.Optional(Type.Number()), danger: Type.Optional(Type.Number()), }, - { additionalProperties: false }, + { additionalProperties: false, minProperties: 1 }, ); export const TelemetryCardSchema = Type.Object( diff --git a/src/lib/model/validation.ts b/src/lib/model/validation.ts index 2059f3c..3ed22ab 100644 --- a/src/lib/model/validation.ts +++ b/src/lib/model/validation.ts @@ -5,10 +5,20 @@ import { type DashboardDocument, } from "./schema"; +type DashboardValidationIssue = ErrorObject | SemanticValidationIssue; + +interface SemanticValidationIssue { + instancePath: string; + schemaPath: string; + keyword: "semantic"; + params: Record; + message: string; +} + export interface DashboardValidationFailure { valid: false; errors: string[]; - details: ErrorObject[]; + details: DashboardValidationIssue[]; } export interface DashboardValidationSuccess { @@ -24,27 +34,46 @@ const ajv = addFormats( new Ajv({ allErrors: true, strict: false, + strictNumbers: true, }), ); const validateDashboard = ajv.compile(DashboardDocumentSchema); -export function formatValidationErrors(errors: ErrorObject[] = []): string[] { +export function formatValidationErrors( + errors: DashboardValidationIssue[] = [], +): string[] { return errors.map((error) => { const path = error.instancePath || "/"; + const suffix = formatErrorParams(error); const message = error.message || "is invalid"; - return `${path} ${message}`; + return `${path} ${message}${suffix}`; }); } export function validateDashboardDocument( value: unknown, ): DashboardValidationResult { + const finiteNumberIssues: SemanticValidationIssue[] = []; + collectFiniteNumberIssues(value, "", finiteNumberIssues); + if (validateDashboard(value)) { - return { valid: true, data: value }; + const semanticIssues = [ + ...finiteNumberIssues, + ...validateSemanticRules(value), + ]; + if (semanticIssues.length === 0) { + return { valid: true, data: value }; + } + + return { + valid: false, + errors: formatValidationErrors(semanticIssues), + details: semanticIssues, + }; } - const details = [...(validateDashboard.errors || [])]; + const details = [...(validateDashboard.errors || []), ...finiteNumberIssues]; return { valid: false, errors: formatValidationErrors(details), @@ -64,3 +93,165 @@ export function assertDashboardDocument( export function isDashboardDocument(value: unknown): value is DashboardDocument { return validateDashboardDocument(value).valid; } + +function formatErrorParams(error: DashboardValidationIssue): string { + if (error.keyword === "additionalProperties") { + const additionalProperty = error.params.additionalProperty; + return typeof additionalProperty === "string" + ? `: ${additionalProperty}` + : ""; + } + + if (error.keyword === "semantic") { + const id = error.params.id; + const ref = error.params.ref; + if (typeof id === "string") return `: ${id}`; + if (typeof ref === "string") return `: ${ref}`; + } + + return ""; +} + +function validateSemanticRules(document: DashboardDocument): SemanticValidationIssue[] { + const issues: SemanticValidationIssue[] = []; + + collectDuplicateIdIssues("telemetry", document.telemetry, issues); + collectDuplicateIdIssues("serviceGroups", document.serviceGroups, issues); + collectDuplicateIdIssues("statusStrips", document.statusStrips, issues); + collectDuplicateIdIssues("modules", document.modules || [], issues); + + document.serviceGroups.forEach((group, groupIndex) => { + collectDuplicateIdIssues( + `serviceGroups/${groupIndex}/services`, + group.services, + issues, + ); + }); + + collectMissingReferenceIssues( + "layout/telemetry", + document.layout.telemetry, + new Set(document.telemetry.map((item) => item.id)), + issues, + ); + collectMissingReferenceIssues( + "layout/serviceGroups", + document.layout.serviceGroups, + new Set(document.serviceGroups.map((item) => item.id)), + issues, + ); + collectMissingReferenceIssues( + "layout/statusStrips", + document.layout.statusStrips, + new Set(document.statusStrips.map((item) => item.id)), + issues, + ); + collectMissingReferenceIssues( + "layout/modules", + document.layout.modules || [], + new Set((document.modules || []).map((item) => item.id)), + issues, + ); + + document.telemetry.forEach((card, index) => { + const warning = card.thresholds?.warning; + const danger = card.thresholds?.danger; + if (warning !== undefined && danger !== undefined && warning > danger) { + issues.push( + semanticIssue( + `/telemetry/${index}/thresholds`, + "warning threshold must be less than or equal to danger threshold", + { id: card.id }, + ), + ); + } + }); + + return issues; +} + +function collectFiniteNumberIssues( + value: unknown, + path: string, + issues: SemanticValidationIssue[], +) { + if (typeof value === "number") { + if (!Number.isFinite(value)) { + issues.push( + semanticIssue(path || "/", "must be a finite JSON number", {}), + ); + } + return; + } + + if (Array.isArray(value)) { + value.forEach((item, index) => { + collectFiniteNumberIssues(item, `${path}/${index}`, issues); + }); + return; + } + + if (value && typeof value === "object") { + Object.entries(value).forEach(([key, item]) => { + collectFiniteNumberIssues(item, `${path}/${escapeJsonPointer(key)}`, issues); + }); + } +} + +function collectDuplicateIdIssues( + collectionPath: string, + items: Array<{ id: string }>, + issues: SemanticValidationIssue[], +) { + const seen = new Set(); + items.forEach((item, index) => { + if (seen.has(item.id)) { + issues.push( + semanticIssue( + `/${collectionPath}/${index}/id`, + "must be unique within its collection", + { id: item.id }, + ), + ); + return; + } + seen.add(item.id); + }); +} + +function collectMissingReferenceIssues( + layoutPath: string, + refs: string[], + validIds: Set, + issues: SemanticValidationIssue[], +) { + refs.forEach((ref, index) => { + if (!validIds.has(ref)) { + issues.push( + semanticIssue( + `/${layoutPath}/${index}`, + "must reference an existing item", + { ref }, + ), + ); + } + }); +} + +function semanticIssue( + instancePath: string, + message: string, + params: Record, +): SemanticValidationIssue { + return { + instancePath, + schemaPath: "", + keyword: "semantic", + params, + message, + }; +} + +function escapeJsonPointer(value: string): string { + return value.replaceAll("~", "~0").replaceAll("/", "~1"); +} From b7c673bbd745f4f5bc08538361d779d95a0cfb7a Mon Sep 17 00:00:00 2001 From: vince Date: Thu, 18 Jun 2026 17:49:18 +0200 Subject: [PATCH 03/90] fix: reject non-json model values --- src/lib/model/fixtures/dimensionlab.ts | 5 +-- src/lib/model/schema.test.ts | 25 ++++++++++++++ src/lib/model/validation.ts | 45 +++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/lib/model/fixtures/dimensionlab.ts b/src/lib/model/fixtures/dimensionlab.ts index 572067f..be23b02 100644 --- a/src/lib/model/fixtures/dimensionlab.ts +++ b/src/lib/model/fixtures/dimensionlab.ts @@ -140,16 +140,17 @@ function service( icon: string, href?: string, ) { - return { + const entry = { id, label, description, icon, - link: href ? { href, external: true } : undefined, severity: "ok" as const, detail: "ready", datasource: { type: "placeholder" as const, reason: "service health adapter pending" }, }; + + return href ? { ...entry, link: { href, external: true } } : entry; } function group(id: string, title: string, services: ReturnType[]) { diff --git a/src/lib/model/schema.test.ts b/src/lib/model/schema.test.ts index 0f3153b..ab9e27a 100644 --- a/src/lib/model/schema.test.ts +++ b/src/lib/model/schema.test.ts @@ -125,6 +125,31 @@ describe("dashboard model validation", () => { } }); + it("rejects thresholds on text metric values", () => { + const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); + invalid.telemetry[0].value = { kind: "text", value: "available" }; + invalid.telemetry[0].thresholds = { warning: 10 }; + + const result = validateDashboardDocument(invalid); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors.join(" ")).toContain("text metric values"); + } + }); + + it("rejects undefined properties because they are not JSON values", () => { + const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); + invalid.serviceGroups[0].services[0].link = undefined; + + const result = validateDashboardDocument(invalid); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors.join(" ")).toContain("must be omitted instead of undefined"); + } + }); + it("exports JSON Schema for external tool contracts", () => { expect(dashboardDocumentJsonSchema.$id).toContain("dashboard-document.v1"); expect(dashboardDocumentJsonSchema.properties).toHaveProperty("schemaVersion"); diff --git a/src/lib/model/validation.ts b/src/lib/model/validation.ts index 3ed22ab..7a79b54 100644 --- a/src/lib/model/validation.ts +++ b/src/lib/model/validation.ts @@ -55,11 +55,14 @@ export function validateDashboardDocument( value: unknown, ): DashboardValidationResult { const finiteNumberIssues: SemanticValidationIssue[] = []; + const undefinedIssues: SemanticValidationIssue[] = []; collectFiniteNumberIssues(value, "", finiteNumberIssues); + collectUndefinedIssues(value, "", undefinedIssues); if (validateDashboard(value)) { const semanticIssues = [ ...finiteNumberIssues, + ...undefinedIssues, ...validateSemanticRules(value), ]; if (semanticIssues.length === 0) { @@ -73,7 +76,11 @@ export function validateDashboardDocument( }; } - const details = [...(validateDashboard.errors || []), ...finiteNumberIssues]; + const details = [ + ...(validateDashboard.errors || []), + ...finiteNumberIssues, + ...undefinedIssues, + ]; return { valid: false, errors: formatValidationErrors(details), @@ -154,6 +161,16 @@ function validateSemanticRules(document: DashboardDocument): SemanticValidationI ); document.telemetry.forEach((card, index) => { + if (card.value.kind === "text" && card.thresholds !== undefined) { + issues.push( + semanticIssue( + `/telemetry/${index}/thresholds`, + "must not be set for text metric values", + { id: card.id }, + ), + ); + } + const warning = card.thresholds?.warning; const danger = card.thresholds?.danger; if (warning !== undefined && danger !== undefined && warning > danger) { @@ -170,6 +187,32 @@ function validateSemanticRules(document: DashboardDocument): SemanticValidationI return issues; } +function collectUndefinedIssues( + value: unknown, + path: string, + issues: SemanticValidationIssue[], +) { + if (value === undefined) { + issues.push( + semanticIssue(path || "/", "must be omitted instead of undefined", {}), + ); + return; + } + + if (Array.isArray(value)) { + value.forEach((item, index) => { + collectUndefinedIssues(item, `${path}/${index}`, issues); + }); + return; + } + + if (value && typeof value === "object") { + Object.entries(value).forEach(([key, item]) => { + collectUndefinedIssues(item, `${path}/${escapeJsonPointer(key)}`, issues); + }); + } +} + function collectFiniteNumberIssues( value: unknown, path: string, From 31604ea935465578bc6c270451771a95c3ba7614 Mon Sep 17 00:00:00 2001 From: vince Date: Thu, 18 Jun 2026 17:53:52 +0200 Subject: [PATCH 04/90] fix: constrain dashboard metric validation --- src/lib/model/schema.test.ts | 45 ++++++++++++++++++++++++++++++++++++ src/lib/model/schema.ts | 22 +++++++++++++----- src/lib/model/validation.ts | 22 ++++++++++++++++++ 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/src/lib/model/schema.test.ts b/src/lib/model/schema.test.ts index ab9e27a..7eeed74 100644 --- a/src/lib/model/schema.test.ts +++ b/src/lib/model/schema.test.ts @@ -104,6 +104,21 @@ describe("dashboard model validation", () => { } }); + it("rejects duplicate status strip item IDs", () => { + const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); + invalid.statusStrips[0].items.push({ + ...invalid.statusStrips[0].items[0], + }); + + const result = validateDashboardDocument(invalid); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors.join(" ")).toContain("must be unique"); + expect(result.errors.join(" ")).toContain(invalid.statusStrips[0].items[0].id); + } + }); + it("rejects string values for numeric metric kinds", () => { const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); invalid.telemetry[0].value.value = "not a percent"; @@ -113,6 +128,24 @@ describe("dashboard model validation", () => { expect(result.valid).toBe(false); }); + it("rejects percent values outside 0 to 100", () => { + const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); + invalid.telemetry[0].value.value = 150; + + const result = validateDashboardDocument(invalid); + + expect(result.valid).toBe(false); + }); + + it("rejects negative values for nonnegative metric kinds", () => { + const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); + invalid.telemetry[0].value = { kind: "latency", value: -20 }; + + const result = validateDashboardDocument(invalid); + + expect(result.valid).toBe(false); + }); + it("rejects contradictory warning and danger thresholds", () => { const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); invalid.telemetry[0].thresholds = { warning: 90, danger: 80 }; @@ -125,6 +158,18 @@ describe("dashboard model validation", () => { } }); + it("rejects percent thresholds above 100", () => { + const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); + invalid.telemetry[0].thresholds = { warning: 99, danger: 999 }; + + const result = validateDashboardDocument(invalid); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors.join(" ")).toContain("percent thresholds"); + } + }); + it("rejects thresholds on text metric values", () => { const invalid = JSON.parse(JSON.stringify(genericDashboardFixture)); invalid.telemetry[0].value = { kind: "text", value: "available" }; diff --git a/src/lib/model/schema.ts b/src/lib/model/schema.ts index d338468..e0215a4 100644 --- a/src/lib/model/schema.ts +++ b/src/lib/model/schema.ts @@ -64,16 +64,25 @@ const DatasourceReferenceSchema = Type.Union([ ExternalDatasourceSchema, ]); -const NumericMetricValueSchema = Type.Object( +const PercentMetricValueSchema = Type.Object( + { + kind: Type.Literal("percent"), + value: Type.Number({ minimum: 0, maximum: 100 }), + unit: Type.Optional(Type.String({ minLength: 1 })), + precision: Type.Optional(Type.Integer({ minimum: 0, maximum: 4 })), + }, + { additionalProperties: false }, +); + +const NonNegativeMetricValueSchema = Type.Object( { kind: Type.Union([ - Type.Literal("percent"), Type.Literal("bytes"), Type.Literal("temperature"), Type.Literal("latency"), Type.Literal("number"), ]), - value: Type.Number(), + value: Type.Number({ minimum: 0 }), unit: Type.Optional(Type.String({ minLength: 1 })), precision: Type.Optional(Type.Integer({ minimum: 0, maximum: 4 })), }, @@ -90,14 +99,15 @@ const TextMetricValueSchema = Type.Object( ); const MetricValueSchema = Type.Union([ - NumericMetricValueSchema, + PercentMetricValueSchema, + NonNegativeMetricValueSchema, TextMetricValueSchema, ]); const ThresholdSchema = Type.Object( { - warning: Type.Optional(Type.Number()), - danger: Type.Optional(Type.Number()), + warning: Type.Optional(Type.Number({ minimum: 0 })), + danger: Type.Optional(Type.Number({ minimum: 0 })), }, { additionalProperties: false, minProperties: 1 }, ); diff --git a/src/lib/model/validation.ts b/src/lib/model/validation.ts index 7a79b54..f70b0cd 100644 --- a/src/lib/model/validation.ts +++ b/src/lib/model/validation.ts @@ -135,6 +135,14 @@ function validateSemanticRules(document: DashboardDocument): SemanticValidationI ); }); + document.statusStrips.forEach((strip, stripIndex) => { + collectDuplicateIdIssues( + `statusStrips/${stripIndex}/items`, + strip.items, + issues, + ); + }); + collectMissingReferenceIssues( "layout/telemetry", document.layout.telemetry, @@ -171,6 +179,20 @@ function validateSemanticRules(document: DashboardDocument): SemanticValidationI ); } + if ( + card.value.kind === "percent" && + ((card.thresholds?.warning !== undefined && card.thresholds.warning > 100) || + (card.thresholds?.danger !== undefined && card.thresholds.danger > 100)) + ) { + issues.push( + semanticIssue( + `/telemetry/${index}/thresholds`, + "percent thresholds must be between 0 and 100", + { id: card.id }, + ), + ); + } + const warning = card.thresholds?.warning; const danger = card.thresholds?.danger; if (warning !== undefined && danger !== undefined && warning > danger) { From 5e42ee88690ecc67bb7d4f3700590244d7a72d2c Mon Sep 17 00:00:00 2001 From: vince Date: Thu, 18 Jun 2026 18:09:17 +0200 Subject: [PATCH 05/90] feat: add dashboard persistence store --- README.md | 47 ++-- bun.lock | 183 +++++++++++++++ drizzle.config.ts | 10 + drizzle/0000_dashboard_persistence.sql | 20 ++ drizzle/meta/0000_snapshot.json | 138 +++++++++++ drizzle/meta/_journal.json | 13 + package.json | 9 +- src/lib/server/dashboard.test.ts | 42 ++++ src/lib/server/dashboard.ts | 34 +++ src/lib/server/db/connection.ts | 47 ++++ src/lib/server/db/dashboard-store.test.ts | 205 ++++++++++++++++ src/lib/server/db/dashboard-store.ts | 261 +++++++++++++++++++++ src/lib/server/db/migrations.ts | 52 ++++ src/lib/server/db/model-migrations.test.ts | 31 +++ src/lib/server/db/model-migrations.ts | 62 +++++ src/lib/server/db/schema.ts | 39 +++ src/routes/+page.server.ts | 4 +- tsconfig.json | 3 +- 18 files changed, 1181 insertions(+), 19 deletions(-) create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_dashboard_persistence.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 src/lib/server/dashboard.test.ts create mode 100644 src/lib/server/db/connection.ts create mode 100644 src/lib/server/db/dashboard-store.test.ts create mode 100644 src/lib/server/db/dashboard-store.ts create mode 100644 src/lib/server/db/migrations.ts create mode 100644 src/lib/server/db/model-migrations.test.ts create mode 100644 src/lib/server/db/model-migrations.ts create mode 100644 src/lib/server/db/schema.ts diff --git a/README.md b/README.md index 352d1a3..3dadcbd 100644 --- a/README.md +++ b/README.md @@ -9,27 +9,46 @@ in validated dashboard model state. ## Development -```sh -npm install -npm run dev -``` - -The current workspace has Bun available, so local verification can also use: - ```sh bun install bun run dev ``` +This MVP uses Drizzle with Bun SQLite for local file-backed persistence. + ## Scripts -- `npm run dev`: start the local development server. -- `npm run check`: run Svelte and TypeScript checks. -- `npm run build`: build the production app. -- `npm run preview`: preview the production build. +- `bun run dev`: start the local development server. +- `bun run check`: run Svelte and TypeScript checks. +- `bun run test`: run Vitest. +- `bun run build`: build the production app. +- `bun run preview`: preview the production build. +- `bun run db:generate`: generate Drizzle migrations from the server schema. +- `bun run db:check`: validate migration consistency. + +## Persistence + +Dashboard documents are stored in SQLite through Drizzle. The default database +URL is: + +```sh +DATABASE_URL=file:./data/dimensionlab.sqlite +``` + +SQLite files under `data/` are ignored. Drizzle schema lives in +`src/lib/server/db/schema.ts`; tracked migrations live in `drizzle/`. Runtime +startup applies the checked-in dashboard migrations before reads or writes. If +the app is launched from outside the repo tree, set `DASHBOARD_MIGRATIONS_DIR` +to the tracked migrations directory. The current driver is `bun:sqlite`, which +keeps this repo installable in the Bun workflow. The store boundary is isolated +so a later Postgres driver can replace the SQLite connection without changing +the dashboard model or renderer. +Stored dashboard documents pass through a version migration boundary before +reads or writes; the MVP supports `dashboard.v1` and fails unsupported versions +with an explicit migration error. ## Runtime Shape -The first deployment target is a Node/container-friendly SvelteKit build. Later -issues add the versioned dashboard model, SQLite persistence, component system, -Storybook, seed data, and rendering pipeline. +The SvelteKit build uses the Node adapter, and the current persistence runtime +is Bun because the MVP SQLite driver is `bun:sqlite`. Later issues add the +component system, Storybook, seed data expansion, and rendering pipeline. diff --git a/bun.lock b/bun.lock index 267f7f4..6bf41c5 100644 --- a/bun.lock +++ b/bun.lock @@ -8,12 +8,15 @@ "@sinclair/typebox": "^0.34.49", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", + "drizzle-orm": "^0.45.2", }, "devDependencies": { "@sveltejs/adapter-node": "^5.5.4", "@sveltejs/kit": "^2.65.2", "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@types/bun": "^1.3.14", "@types/node": "^25.9.3", + "drizzle-kit": "^0.31.10", "svelte": "^5.56.3", "svelte-check": "^4.6.0", "typescript": "^6.0.3", @@ -23,12 +26,70 @@ }, }, "packages": { + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -151,6 +212,8 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], @@ -191,6 +254,10 @@ "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], @@ -209,10 +276,16 @@ "devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="], + "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], + + "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], "esrap": ["esrap@2.2.11", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ=="], @@ -231,6 +304,8 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], @@ -295,6 +370,8 @@ "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="], "rollup": ["rollup@4.62.0", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.0", "@rollup/rollup-android-arm64": "4.62.0", "@rollup/rollup-darwin-arm64": "4.62.0", "@rollup/rollup-darwin-x64": "4.62.0", "@rollup/rollup-freebsd-arm64": "4.62.0", "@rollup/rollup-freebsd-x64": "4.62.0", "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", "@rollup/rollup-linux-arm-musleabihf": "4.62.0", "@rollup/rollup-linux-arm64-gnu": "4.62.0", "@rollup/rollup-linux-arm64-musl": "4.62.0", "@rollup/rollup-linux-loong64-gnu": "4.62.0", "@rollup/rollup-linux-loong64-musl": "4.62.0", "@rollup/rollup-linux-ppc64-gnu": "4.62.0", "@rollup/rollup-linux-ppc64-musl": "4.62.0", "@rollup/rollup-linux-riscv64-gnu": "4.62.0", "@rollup/rollup-linux-riscv64-musl": "4.62.0", "@rollup/rollup-linux-s390x-gnu": "4.62.0", "@rollup/rollup-linux-x64-gnu": "4.62.0", "@rollup/rollup-linux-x64-musl": "4.62.0", "@rollup/rollup-openbsd-x64": "4.62.0", "@rollup/rollup-openharmony-arm64": "4.62.0", "@rollup/rollup-win32-arm64-msvc": "4.62.0", "@rollup/rollup-win32-ia32-msvc": "4.62.0", "@rollup/rollup-win32-x64-gnu": "4.62.0", "@rollup/rollup-win32-x64-msvc": "4.62.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA=="], @@ -307,8 +384,12 @@ "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], @@ -331,6 +412,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsx": ["tsx@4.22.4", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], @@ -345,8 +428,108 @@ "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "tsx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], } } diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..2f23f47 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/lib/server/db/schema.ts", + out: "./drizzle", + dialect: "sqlite", + dbCredentials: { + url: process.env.DATABASE_URL?.replace(/^file:/, "") || "./data/dimensionlab.sqlite", + }, +}); diff --git a/drizzle/0000_dashboard_persistence.sql b/drizzle/0000_dashboard_persistence.sql new file mode 100644 index 0000000..fb321c5 --- /dev/null +++ b/drizzle/0000_dashboard_persistence.sql @@ -0,0 +1,20 @@ +CREATE TABLE `dashboard_documents` ( + `id` text PRIMARY KEY NOT NULL, + `current_revision_id` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE `dashboard_revisions` ( + `id` text PRIMARY KEY NOT NULL, + `dashboard_id` text NOT NULL, + `schema_version` text NOT NULL, + `document` text NOT NULL, + `actor` text NOT NULL, + `message` text, + `operation` text NOT NULL, + `source_revision_id` text, + `created_at` integer NOT NULL +); +--> statement-breakpoint +CREATE INDEX `idx_dashboard_revisions_dashboard_created` ON `dashboard_revisions` (`dashboard_id`,`created_at`); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..e184f8e --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,138 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "00000000-0000-0000-0000-dashboard0000", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "dashboard_documents": { + "name": "dashboard_documents", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "current_revision_id": { + "name": "current_revision_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_revisions": { + "name": "dashboard_revisions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "dashboard_id": { + "name": "dashboard_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema_version": { + "name": "schema_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "document": { + "name": "document", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor": { + "name": "actor", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_revision_id": { + "name": "source_revision_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_dashboard_revisions_dashboard_created": { + "name": "idx_dashboard_revisions_dashboard_created", + "columns": [ + "dashboard_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "enums": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..d19b757 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1781805600000, + "tag": "0000_dashboard_persistence", + "breakpoints": true + } + ] +} diff --git a/package.json b/package.json index 0cdc69e..fcf2270 100644 --- a/package.json +++ b/package.json @@ -8,18 +8,23 @@ "build": "vite build", "preview": "vite preview --host 0.0.0.0", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "test": "vitest run" + "test": "vitest run", + "db:generate": "drizzle-kit generate", + "db:check": "drizzle-kit check" }, "dependencies": { "@sinclair/typebox": "^0.34.49", "ajv": "^8.20.0", - "ajv-formats": "^3.0.1" + "ajv-formats": "^3.0.1", + "drizzle-orm": "^0.45.2" }, "devDependencies": { "@sveltejs/adapter-node": "^5.5.4", "@sveltejs/kit": "^2.65.2", "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@types/bun": "^1.3.14", "@types/node": "^25.9.3", + "drizzle-kit": "^0.31.10", "svelte": "^5.56.3", "svelte-check": "^4.6.0", "typescript": "^6.0.3", diff --git a/src/lib/server/dashboard.test.ts b/src/lib/server/dashboard.test.ts new file mode 100644 index 0000000..91a5ffb --- /dev/null +++ b/src/lib/server/dashboard.test.ts @@ -0,0 +1,42 @@ +import { rmSync } from "node:fs"; +import { mkdtemp } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, test } from "vitest"; +import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; +import { createDashboardStore, type DashboardStore } from "./db/dashboard-store"; +import { loadDashboardRuntime } from "./dashboard"; + +const stores: DashboardStore[] = []; +const tempRoots: string[] = []; + +afterEach(() => { + stores.splice(0).forEach((store) => store.close()); + tempRoots.splice(0).forEach((path) => rmSync(path, { force: true, recursive: true })); +}); + +describe("dashboard runtime loader", () => { + test("seeds and loads the active dashboard from sqlite", async () => { + const store = await createTestStore(); + + const runtime = loadDashboardRuntime(store); + + expect(runtime.title).toBe(dimensionLabDashboardFixture.metadata.title); + expect(runtime.subtitle).toBe(dimensionLabDashboardFixture.metadata.subtitle); + expect(runtime.status).toBe("building"); + expect(runtime.schemaVersion).toBe(dimensionLabDashboardFixture.schemaVersion); + expect(runtime.currentRevisionId).toBe(store.getActiveDashboard()?.currentRevisionId); + expect(store.listRevisions()).toHaveLength(1); + }); +}); + +async function createTestStore() { + const root = await mkdtemp(join(tmpdir(), "dimensionlab-dashboard-runtime-")); + tempRoots.push(root); + const store = createDashboardStore({ + databaseUrl: `file:${join(root, "dashboard.sqlite")}`, + }); + stores.push(store); + + return store; +} diff --git a/src/lib/server/dashboard.ts b/src/lib/server/dashboard.ts index 0dcd533..244e97b 100644 --- a/src/lib/server/dashboard.ts +++ b/src/lib/server/dashboard.ts @@ -1,3 +1,9 @@ +import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; +import { + createDashboardStore, + type DashboardStore, +} from "$lib/server/db/dashboard-store"; + export type PlaceholderStatus = "ready" | "building"; export interface PlaceholderDashboard { @@ -5,6 +11,8 @@ export interface PlaceholderDashboard { subtitle: string; status: PlaceholderStatus; message: string; + schemaVersion?: string; + currentRevisionId?: string; } export function loadPlaceholderDashboard(): PlaceholderDashboard { @@ -16,3 +24,29 @@ export function loadPlaceholderDashboard(): PlaceholderDashboard { "SvelteKit is running. Later issues will replace this placeholder with the validated dashboard model.", }; } + +export function loadDashboardRuntime(store?: DashboardStore): PlaceholderDashboard { + const dashboardStore = store || createDashboardStore(); + + try { + const seeded = dashboardStore.seedDashboardIfEmpty(dimensionLabDashboardFixture, { + actor: "dimensionlab-seed", + message: "load initial dashboard seed", + }); + const active = dashboardStore.getActiveDashboard(); + const document = active?.document || seeded.document; + const currentRevisionId = active?.currentRevisionId || seeded.id; + + return { + title: document.metadata.title, + subtitle: document.metadata.subtitle || "", + status: "building", + message: + "Active dashboard document loaded from SQLite. Later issues will replace this placeholder with model-driven rendering.", + schemaVersion: document.schemaVersion, + currentRevisionId, + }; + } finally { + if (!store) dashboardStore.close(); + } +} diff --git a/src/lib/server/db/connection.ts b/src/lib/server/db/connection.ts new file mode 100644 index 0000000..188da4c --- /dev/null +++ b/src/lib/server/db/connection.ts @@ -0,0 +1,47 @@ +import { mkdirSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { Database } from "bun:sqlite"; +import { drizzle, type BunSQLiteDatabase } from "drizzle-orm/bun-sqlite"; +import { dashboardDbSchema } from "./schema"; +import { applyDashboardMigrations } from "./migrations"; + +export const DEFAULT_DATABASE_URL = "file:./data/dimensionlab.sqlite"; + +export type DashboardDatabase = BunSQLiteDatabase & { + $client: Database; +}; + +export interface DashboardDatabaseConnection { + db: DashboardDatabase; + sqlite: Database; + filename: string; + close(): void; +} + +export function openDashboardDatabase( + databaseUrl = process.env.DATABASE_URL || DEFAULT_DATABASE_URL, +): DashboardDatabaseConnection { + const filename = resolveSqliteFilename(databaseUrl); + mkdirSync(dirname(filename), { recursive: true }); + + const sqlite = new Database(filename, { create: true, readwrite: true }); + const db = drizzle(sqlite, { schema: dashboardDbSchema }) as DashboardDatabase; + applyDashboardMigrations(db); + + return { + db, + sqlite, + filename, + close() { + sqlite.close(); + }, + }; +} + +export function resolveSqliteFilename(databaseUrl: string): string { + if (!databaseUrl.startsWith("file:")) { + throw new Error(`Only file: SQLite DATABASE_URL values are supported: ${databaseUrl}`); + } + + return resolve(databaseUrl.slice("file:".length)); +} diff --git a/src/lib/server/db/dashboard-store.test.ts b/src/lib/server/db/dashboard-store.test.ts new file mode 100644 index 0000000..ecb80f3 --- /dev/null +++ b/src/lib/server/db/dashboard-store.test.ts @@ -0,0 +1,205 @@ +import { existsSync, rmSync } from "node:fs"; +import { mkdtemp } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { genericDashboardFixture } from "$lib/model/fixtures/generic"; +import type { DashboardDocument } from "$lib/model"; +import { + DashboardPersistenceValidationError, + createDashboardStore, + type DashboardStore, +} from "./dashboard-store"; + +const stores: DashboardStore[] = []; +const tempRoots: string[] = []; +const originalCwd = process.cwd(); + +afterEach(() => { + vi.useRealTimers(); + process.chdir(originalCwd); + stores.splice(0).forEach((store) => store.close()); + tempRoots.splice(0).forEach((path) => rmSync(path, { force: true, recursive: true })); +}); + +describe("dashboard persistence store", () => { + test("seeds the first active dashboard and creates the sqlite file", async () => { + const { dbPath, store } = await createTestStore(); + + const seed = store.seedDashboardIfEmpty(genericDashboardFixture, { + actor: "test-seed", + message: "initial fixture", + }); + + expect(existsSync(dbPath)).toBe(true); + expect(seed.operation).toBe("seed"); + expect(seed.actor).toBe("test-seed"); + expect(seed.schemaVersion).toBe(genericDashboardFixture.schemaVersion); + expect(seed.document.metadata.title).toBe("Operations Console"); + + const active = store.getActiveDashboard(); + expect(active?.currentRevisionId).toBe(seed.id); + expect(active?.document.metadata.title).toBe("Operations Console"); + expect(store.listRevisions()).toHaveLength(1); + }); + + test("commits validated updates and records revision metadata", async () => { + const { store } = await createTestStore(); + const seed = store.seedDashboardIfEmpty(genericDashboardFixture, { + actor: "test-seed", + }); + const updated = cloneDashboard({ + ...genericDashboardFixture, + metadata: { + ...genericDashboardFixture.metadata, + title: "Operations Console Updated", + }, + }); + + const revision = store.commitDashboard(updated, { + actor: "agent", + message: "rename dashboard", + }); + + expect(revision.operation).toBe("commit"); + expect(revision.actor).toBe("agent"); + expect(revision.message).toBe("rename dashboard"); + expect(revision.document.metadata.title).toBe("Operations Console Updated"); + expect(store.getActiveDashboard()?.currentRevisionId).toBe(revision.id); + expect(store.getRevision(seed.id)?.document.metadata.title).toBe("Operations Console"); + expect(store.listRevisions().map((item) => item.id)).toEqual([ + revision.id, + seed.id, + ]); + }); + + test("rejects invalid writes without changing the active revision", async () => { + const { store } = await createTestStore(); + const seed = store.seedDashboardIfEmpty(genericDashboardFixture, { + actor: "test-seed", + }); + const invalid = cloneDashboard({ + ...genericDashboardFixture, + layout: { + ...genericDashboardFixture.layout, + telemetry: ["missing-card"], + }, + }); + + expect(() => + store.commitDashboard(invalid, { + actor: "agent", + message: "invalid update", + }), + ).toThrow(DashboardPersistenceValidationError); + + const active = store.getActiveDashboard(); + expect(active?.currentRevisionId).toBe(seed.id); + expect(active?.document.layout.telemetry).toEqual(genericDashboardFixture.layout.telemetry); + expect(store.listRevisions()).toHaveLength(1); + }); + + test("loads specific revisions and rolls back to a prior valid document", async () => { + const { store } = await createTestStore(); + const seed = store.seedDashboardIfEmpty(genericDashboardFixture, { + actor: "test-seed", + }); + const updated = cloneDashboard({ + ...genericDashboardFixture, + metadata: { + ...genericDashboardFixture.metadata, + title: "Changed Dashboard", + }, + }); + const change = store.commitDashboard(updated, { + actor: "agent", + message: "change title", + }); + + const rollback = store.rollbackToRevision(seed.id, { + actor: "operator", + message: "restore seed", + }); + + expect(store.getRevision(change.id)?.document.metadata.title).toBe("Changed Dashboard"); + expect(rollback.operation).toBe("rollback"); + expect(rollback.sourceRevisionId).toBe(seed.id); + expect(rollback.document.metadata.title).toBe("Operations Console"); + expect(store.getActiveDashboard()?.currentRevisionId).toBe(rollback.id); + expect(store.getActiveDashboard()?.document.metadata.title).toBe("Operations Console"); + expect(store.listRevisions().map((item) => item.operation)).toEqual([ + "rollback", + "commit", + "seed", + ]); + }); + + test("assigns monotonic revision timestamps for stable history ordering", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-06-18T12:00:00.000Z")); + const { store } = await createTestStore(); + const seed = store.seedDashboardIfEmpty(genericDashboardFixture, { + actor: "test-seed", + }); + const updated = cloneDashboard({ + ...genericDashboardFixture, + metadata: { + ...genericDashboardFixture.metadata, + title: "Changed Dashboard", + }, + }); + + const commit = store.commitDashboard(updated, { actor: "agent" }); + const rollback = store.rollbackToRevision(seed.id, { actor: "operator" }); + + expect([ + seed.createdAt.toISOString(), + commit.createdAt.toISOString(), + rollback.createdAt.toISOString(), + ]).toEqual([ + "2026-06-18T12:00:00.000Z", + "2026-06-18T12:00:00.001Z", + "2026-06-18T12:00:00.002Z", + ]); + expect(store.listRevisions().map((item) => item.id)).toEqual([ + rollback.id, + commit.id, + seed.id, + ]); + }); + + test("applies migrations when launched outside the repository root", async () => { + const root = await mkdtemp(join(tmpdir(), "dimensionlab-dashboard-cwd-")); + tempRoots.push(root); + process.chdir(root); + vi.resetModules(); + const { createDashboardStore: createStore } = await import("./dashboard-store"); + const store = createStore({ + databaseUrl: `file:${join(root, "dashboard.sqlite")}`, + }); + stores.push(store); + + const seed = store.seedDashboardIfEmpty(genericDashboardFixture, { + actor: "test-seed", + }); + + expect(seed.document.metadata.title).toBe("Operations Console"); + expect(store.getActiveDashboard()?.currentRevisionId).toBe(seed.id); + }); +}); + +async function createTestStore() { + const root = await mkdtemp(join(tmpdir(), "dimensionlab-dashboard-store-")); + tempRoots.push(root); + const dbPath = join(root, "nested", "dashboard.sqlite"); + const store = createDashboardStore({ + databaseUrl: `file:${dbPath}`, + }); + stores.push(store); + + return { dbPath, store }; +} + +function cloneDashboard(document: DashboardDocument): DashboardDocument { + return structuredClone(document); +} diff --git a/src/lib/server/db/dashboard-store.ts b/src/lib/server/db/dashboard-store.ts new file mode 100644 index 0000000..92c7a73 --- /dev/null +++ b/src/lib/server/db/dashboard-store.ts @@ -0,0 +1,261 @@ +import { randomUUID } from "node:crypto"; +import { desc, eq } from "drizzle-orm"; +import { + type DashboardDocument, + type DashboardValidationFailure, +} from "$lib/model"; +import { + type DashboardDatabaseConnection, + openDashboardDatabase, +} from "./connection"; +import { + dashboardDocuments, + dashboardRevisions, + type DashboardRevisionOperation, +} from "./schema"; +import { migrateDashboardDocumentForPersistence } from "./model-migrations"; + +const DEFAULT_DASHBOARD_ID = "primary"; +const DEFAULT_ACTOR = "system"; + +export interface DashboardRevision { + id: string; + dashboardId: string; + schemaVersion: string; + document: DashboardDocument; + actor: string; + message: string | null; + operation: DashboardRevisionOperation; + sourceRevisionId: string | null; + createdAt: Date; +} + +export interface ActiveDashboard { + dashboardId: string; + currentRevisionId: string; + document: DashboardDocument; + revision: DashboardRevision; + createdAt: Date; + updatedAt: Date; +} + +export interface DashboardWriteMetadata { + actor?: string; + message?: string; +} + +export interface DashboardStoreOptions { + databaseUrl?: string; + dashboardId?: string; +} + +export interface DashboardStore { + getActiveDashboard(): ActiveDashboard | null; + getRevision(revisionId: string): DashboardRevision | null; + listRevisions(limit?: number): DashboardRevision[]; + seedDashboardIfEmpty( + document: DashboardDocument, + metadata?: DashboardWriteMetadata, + ): DashboardRevision; + commitDashboard( + document: DashboardDocument, + metadata?: DashboardWriteMetadata, + ): DashboardRevision; + rollbackToRevision( + revisionId: string, + metadata?: DashboardWriteMetadata, + ): DashboardRevision; + close(): void; +} + +export class DashboardPersistenceValidationError extends Error { + readonly failure: DashboardValidationFailure; + + constructor(failure: DashboardValidationFailure) { + super(`Invalid dashboard document: ${failure.errors.join("; ")}`); + this.name = "DashboardPersistenceValidationError"; + this.failure = failure; + } +} + +export class DashboardRevisionNotFoundError extends Error { + constructor(revisionId: string) { + super(`Dashboard revision not found: ${revisionId}`); + this.name = "DashboardRevisionNotFoundError"; + } +} + +export function createDashboardStore( + options: DashboardStoreOptions = {}, +): DashboardStore { + const connection = openDashboardDatabase(options.databaseUrl); + return new SqliteDashboardStore(connection, options.dashboardId || DEFAULT_DASHBOARD_ID); +} + +class SqliteDashboardStore implements DashboardStore { + constructor( + private readonly connection: DashboardDatabaseConnection, + private readonly dashboardId: string, + ) {} + + getActiveDashboard(): ActiveDashboard | null { + const dashboard = this.connection.db + .select() + .from(dashboardDocuments) + .where(eq(dashboardDocuments.id, this.dashboardId)) + .get(); + + if (!dashboard?.currentRevisionId) return null; + + const revision = this.getRevision(dashboard.currentRevisionId); + if (!revision) return null; + + return { + dashboardId: dashboard.id, + currentRevisionId: dashboard.currentRevisionId, + document: revision.document, + revision, + createdAt: dashboard.createdAt, + updatedAt: dashboard.updatedAt, + }; + } + + getRevision(revisionId: string): DashboardRevision | null { + const row = this.connection.db + .select() + .from(dashboardRevisions) + .where(eq(dashboardRevisions.id, revisionId)) + .get(); + + return row ? toRevision(row) : null; + } + + listRevisions(limit = 50): DashboardRevision[] { + return this.connection.db + .select() + .from(dashboardRevisions) + .where(eq(dashboardRevisions.dashboardId, this.dashboardId)) + .orderBy(desc(dashboardRevisions.createdAt)) + .limit(limit) + .all() + .map(toRevision); + } + + seedDashboardIfEmpty( + document: DashboardDocument, + metadata: DashboardWriteMetadata = {}, + ): DashboardRevision { + const active = this.getActiveDashboard(); + if (active) return active.revision; + + return this.writeRevision(document, "seed", metadata); + } + + commitDashboard( + document: DashboardDocument, + metadata: DashboardWriteMetadata = {}, + ): DashboardRevision { + return this.writeRevision(document, "commit", metadata); + } + + rollbackToRevision( + revisionId: string, + metadata: DashboardWriteMetadata = {}, + ): DashboardRevision { + const revision = this.getRevision(revisionId); + if (!revision || revision.dashboardId !== this.dashboardId) { + throw new DashboardRevisionNotFoundError(revisionId); + } + + return this.writeRevision(revision.document, "rollback", metadata, revision.id); + } + + close() { + this.connection.close(); + } + + private writeRevision( + document: DashboardDocument, + operation: DashboardRevisionOperation, + metadata: DashboardWriteMetadata, + sourceRevisionId: string | null = null, + ): DashboardRevision { + const migration = migrateDashboardDocumentForPersistence(document); + if (!migration.valid) { + throw new DashboardPersistenceValidationError(migration.failure); + } + + const now = this.nextRevisionTimestamp(); + const revision: DashboardRevision = { + id: randomUUID(), + dashboardId: this.dashboardId, + schemaVersion: migration.document.schemaVersion, + document: structuredClone(migration.document), + actor: metadata.actor || DEFAULT_ACTOR, + message: metadata.message || null, + operation, + sourceRevisionId, + createdAt: now, + }; + + this.connection.db.transaction((tx) => { + tx.insert(dashboardRevisions).values({ + id: revision.id, + dashboardId: revision.dashboardId, + schemaVersion: revision.schemaVersion, + document: revision.document, + actor: revision.actor, + message: revision.message, + operation: revision.operation, + sourceRevisionId: revision.sourceRevisionId, + createdAt: revision.createdAt, + }).run(); + + tx.insert(dashboardDocuments) + .values({ + id: this.dashboardId, + currentRevisionId: revision.id, + createdAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: dashboardDocuments.id, + set: { + currentRevisionId: revision.id, + updatedAt: now, + }, + }) + .run(); + }); + + return revision; + } + + private nextRevisionTimestamp(): Date { + const active = this.getActiveDashboard(); + const activeUpdatedAtMs = active?.updatedAt.getTime() || 0; + + return new Date(Math.max(Date.now(), activeUpdatedAtMs + 1)); + } +} + +type DashboardRevisionRow = typeof dashboardRevisions.$inferSelect; + +function toRevision(row: DashboardRevisionRow): DashboardRevision { + const migration = migrateDashboardDocumentForPersistence(row.document); + if (!migration.valid) { + throw new DashboardPersistenceValidationError(migration.failure); + } + + return { + id: row.id, + dashboardId: row.dashboardId, + schemaVersion: row.schemaVersion, + document: migration.document, + actor: row.actor, + message: row.message, + operation: row.operation as DashboardRevisionOperation, + sourceRevisionId: row.sourceRevisionId, + createdAt: row.createdAt, + }; +} diff --git a/src/lib/server/db/migrations.ts b/src/lib/server/db/migrations.ts new file mode 100644 index 0000000..9e34ba0 --- /dev/null +++ b/src/lib/server/db/migrations.ts @@ -0,0 +1,52 @@ +import { existsSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { migrate } from "drizzle-orm/bun-sqlite/migrator"; +import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite"; + +const MIGRATION_JOURNAL_PATH = join("meta", "_journal.json"); + +export function applyDashboardMigrations( + database: BunSQLiteDatabase>, + migrationsFolder = resolveDashboardMigrationsFolder(), +) { + migrate(database, { migrationsFolder }); +} + +export function resolveDashboardMigrationsFolder( + explicitFolder = process.env.DASHBOARD_MIGRATIONS_DIR, +): string { + if (explicitFolder) return assertMigrationFolder(resolve(explicitFolder)); + + const cwdFolder = findMigrationFolder(process.cwd()); + if (cwdFolder) return cwdFolder; + + const moduleFolder = findMigrationFolder(dirname(fileURLToPath(import.meta.url))); + if (moduleFolder) return moduleFolder; + + throw new Error( + "Unable to locate dashboard Drizzle migrations. Set DASHBOARD_MIGRATIONS_DIR.", + ); +} + +function findMigrationFolder(startPath: string): string | null { + let currentPath = resolve(startPath); + + while (true) { + const candidate = join(currentPath, "drizzle"); + if (hasMigrationJournal(candidate)) return candidate; + + const parentPath = dirname(currentPath); + if (parentPath === currentPath) return null; + currentPath = parentPath; + } +} + +function assertMigrationFolder(folder: string): string { + if (hasMigrationJournal(folder)) return folder; + throw new Error(`Dashboard migrations not found in ${folder}`); +} + +function hasMigrationJournal(folder: string): boolean { + return existsSync(join(folder, MIGRATION_JOURNAL_PATH)); +} diff --git a/src/lib/server/db/model-migrations.test.ts b/src/lib/server/db/model-migrations.test.ts new file mode 100644 index 0000000..eb16b8f --- /dev/null +++ b/src/lib/server/db/model-migrations.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from "vitest"; +import { DASHBOARD_SCHEMA_VERSION } from "$lib/model"; +import { genericDashboardFixture } from "$lib/model/fixtures/generic"; +import { + UnsupportedDashboardModelVersionError, + migrateDashboardDocumentForPersistence, +} from "./model-migrations"; + +describe("dashboard model migrations", () => { + test("accepts the current dashboard model version without migration", () => { + const result = migrateDashboardDocumentForPersistence(genericDashboardFixture); + + expect(result.valid).toBe(true); + if (!result.valid) throw new Error("expected current fixture to be valid"); + expect(result.document).toEqual(genericDashboardFixture); + expect(result.fromVersion).toBe(DASHBOARD_SCHEMA_VERSION); + expect(result.toVersion).toBe(DASHBOARD_SCHEMA_VERSION); + expect(result.migrated).toBe(false); + }); + + test("fails unsupported model versions with an explicit migration error", () => { + const previousVersion = { + ...genericDashboardFixture, + schemaVersion: "dashboard.v0", + }; + + expect(() => migrateDashboardDocumentForPersistence(previousVersion)).toThrow( + UnsupportedDashboardModelVersionError, + ); + }); +}); diff --git a/src/lib/server/db/model-migrations.ts b/src/lib/server/db/model-migrations.ts new file mode 100644 index 0000000..92550e6 --- /dev/null +++ b/src/lib/server/db/model-migrations.ts @@ -0,0 +1,62 @@ +import { + DASHBOARD_SCHEMA_VERSION, + validateDashboardDocument, + type DashboardDocument, + type DashboardValidationFailure, +} from "$lib/model"; + +export interface DashboardModelMigrationSuccess { + valid: true; + document: DashboardDocument; + fromVersion: string; + toVersion: typeof DASHBOARD_SCHEMA_VERSION; + migrated: boolean; +} + +export interface DashboardModelMigrationFailure { + valid: false; + failure: DashboardValidationFailure; +} + +export type DashboardModelMigrationResult = + | DashboardModelMigrationFailure + | DashboardModelMigrationSuccess; + +export class UnsupportedDashboardModelVersionError extends Error { + constructor( + readonly fromVersion: string, + readonly toVersion: string, + ) { + super(`No dashboard model migration from ${fromVersion} to ${toVersion}`); + this.name = "UnsupportedDashboardModelVersionError"; + } +} + +export function migrateDashboardDocumentForPersistence( + value: unknown, +): DashboardModelMigrationResult { + const version = readSchemaVersion(value); + if (version && version !== DASHBOARD_SCHEMA_VERSION) { + throw new UnsupportedDashboardModelVersionError(version, DASHBOARD_SCHEMA_VERSION); + } + + const validation = validateDashboardDocument(value); + if (!validation.valid) { + return { valid: false, failure: validation }; + } + + return { + valid: true, + document: validation.data, + fromVersion: DASHBOARD_SCHEMA_VERSION, + toVersion: DASHBOARD_SCHEMA_VERSION, + migrated: false, + }; +} + +function readSchemaVersion(value: unknown): string | null { + if (!value || typeof value !== "object") return null; + + const schemaVersion = (value as { schemaVersion?: unknown }).schemaVersion; + return typeof schemaVersion === "string" ? schemaVersion : null; +} diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts new file mode 100644 index 0000000..c148035 --- /dev/null +++ b/src/lib/server/db/schema.ts @@ -0,0 +1,39 @@ +import type { DashboardDocument } from "$lib/model"; +import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const dashboardDocuments = sqliteTable("dashboard_documents", { + id: text("id").primaryKey(), + currentRevisionId: text("current_revision_id"), + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(), +}); + +export const dashboardRevisions = sqliteTable( + "dashboard_revisions", + { + id: text("id").primaryKey(), + dashboardId: text("dashboard_id").notNull(), + schemaVersion: text("schema_version").notNull(), + document: text("document", { mode: "json" }).$type().notNull(), + actor: text("actor").notNull(), + message: text("message"), + operation: text("operation", { + enum: ["seed", "commit", "rollback"], + }).notNull(), + sourceRevisionId: text("source_revision_id"), + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), + }, + (table) => [ + index("idx_dashboard_revisions_dashboard_created").on( + table.dashboardId, + table.createdAt, + ), + ], +); + +export const dashboardDbSchema = { + dashboardDocuments, + dashboardRevisions, +}; + +export type DashboardRevisionOperation = "seed" | "commit" | "rollback"; diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 2a54c0c..114bfe5 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,7 +1,7 @@ -import { loadPlaceholderDashboard } from "$lib/server/dashboard"; +import { loadDashboardRuntime } from "$lib/server/dashboard"; export function load() { return { - dashboard: loadPlaceholderDashboard(), + dashboard: loadDashboardRuntime(), }; } diff --git a/tsconfig.json b/tsconfig.json index 4344710..c08a57f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "skipLibCheck": true, "sourceMap": true, "strict": true, - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "types": ["node", "bun-types"] } } From fb266cf17aa5a58f6d2991ef64a6a4720960d6e1 Mon Sep 17 00:00:00 2001 From: vince Date: Thu, 18 Jun 2026 18:27:54 +0200 Subject: [PATCH 06/90] feat: build dashboard design system --- bun.lock | 5 + package.json | 1 + src/app.css | 31 +--- src/lib/ui/components/DashboardFrame.svelte | 116 ++++++++++++++ src/lib/ui/components/FooterCell.svelte | 89 +++++++++++ src/lib/ui/components/IconGlyph.svelte | 53 +++++++ src/lib/ui/components/ModuleCard.svelte | 86 +++++++++++ src/lib/ui/components/Panel.svelte | 81 ++++++++++ src/lib/ui/components/ServicePanel.svelte | 17 +++ src/lib/ui/components/ServiceRow.svelte | 106 +++++++++++++ src/lib/ui/components/StatusBadge.svelte | 52 +++++++ src/lib/ui/components/StatusStrip.svelte | 32 ++++ src/lib/ui/components/TelemetryCard.svelte | 158 ++++++++++++++++++++ src/lib/ui/components/TelemetryGrid.svelte | 22 +++ src/lib/ui/components/render.test.ts | 97 ++++++++++++ src/lib/ui/content-boundary.test.ts | 49 ++++++ src/lib/ui/fixtures.ts | 130 ++++++++++++++++ src/lib/ui/format.ts | 41 +++++ src/lib/ui/index.ts | 23 +++ src/lib/ui/tokens.css | 81 ++++++++++ src/lib/ui/types.ts | 86 +++++++++++ src/routes/+page.svelte | 149 +++++++++++------- src/routes/page.test.ts | 28 ++++ 23 files changed, 1449 insertions(+), 84 deletions(-) create mode 100644 src/lib/ui/components/DashboardFrame.svelte create mode 100644 src/lib/ui/components/FooterCell.svelte create mode 100644 src/lib/ui/components/IconGlyph.svelte create mode 100644 src/lib/ui/components/ModuleCard.svelte create mode 100644 src/lib/ui/components/Panel.svelte create mode 100644 src/lib/ui/components/ServicePanel.svelte create mode 100644 src/lib/ui/components/ServiceRow.svelte create mode 100644 src/lib/ui/components/StatusBadge.svelte create mode 100644 src/lib/ui/components/StatusStrip.svelte create mode 100644 src/lib/ui/components/TelemetryCard.svelte create mode 100644 src/lib/ui/components/TelemetryGrid.svelte create mode 100644 src/lib/ui/components/render.test.ts create mode 100644 src/lib/ui/content-boundary.test.ts create mode 100644 src/lib/ui/fixtures.ts create mode 100644 src/lib/ui/format.ts create mode 100644 src/lib/ui/index.ts create mode 100644 src/lib/ui/tokens.css create mode 100644 src/lib/ui/types.ts create mode 100644 src/routes/page.test.ts diff --git a/bun.lock b/bun.lock index 6bf41c5..335ff14 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "dimensionlab-website", "dependencies": { + "@iconify/svelte": "^5.2.2", "@sinclair/typebox": "^0.34.49", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", @@ -90,6 +91,10 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@iconify/svelte": ["@iconify/svelte@5.2.2", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "svelte": ">5.0.0" } }, "sha512-XMXxD3nzH7yB68C3K4sNwzrJ1TBscODR0ZqCeNf0KGuEMCTSuCo0jHNDZ0o0iRXTylO41NSz/DWam/4atLG1yw=="], + + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], diff --git a/package.json b/package.json index fcf2270..2bef9c3 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "db:check": "drizzle-kit check" }, "dependencies": { + "@iconify/svelte": "^5.2.2", "@sinclair/typebox": "^0.34.49", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", diff --git a/src/app.css b/src/app.css index ea0c565..4fa34dd 100644 --- a/src/app.css +++ b/src/app.css @@ -1,30 +1 @@ -:root { - color-scheme: dark; - font-family: - "IBM Plex Mono", "Roboto Mono", "SFMono-Regular", Consolas, monospace; - background: #050505; - color: #f4f4f4; - text-rendering: geometricPrecision; -} - -body { - min-width: 320px; - min-height: 100vh; - margin: 0; - background: - linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px), - linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px), - #050505; - background-size: 48px 48px; -} - -button, -input, -textarea, -select { - font: inherit; -} - -a { - color: inherit; -} +@import "./lib/ui/tokens.css"; diff --git a/src/lib/ui/components/DashboardFrame.svelte b/src/lib/ui/components/DashboardFrame.svelte new file mode 100644 index 0000000..0a76f67 --- /dev/null +++ b/src/lib/ui/components/DashboardFrame.svelte @@ -0,0 +1,116 @@ + + +
+
+
+ {#if dashboard.eyebrow} +

{dashboard.eyebrow}

+ {/if} +

{dashboard.title}

+ {#if dashboard.subtitle} + {dashboard.subtitle} + {/if} +
+ {#if dashboard.modules.length} +
+ {#each dashboard.modules as module (module.id)} + + {/each} +
+ {/if} +
+ + + +
+ {#each dashboard.serviceGroups as group (group.id)} + + {/each} +
+ + +
+ + diff --git a/src/lib/ui/components/FooterCell.svelte b/src/lib/ui/components/FooterCell.svelte new file mode 100644 index 0000000..d4261fe --- /dev/null +++ b/src/lib/ui/components/FooterCell.svelte @@ -0,0 +1,89 @@ + + +{#snippet cellContent()} + {item.label} + {item.value} +{/snippet} + +{#if item.link} + + {@render cellContent()} + +{:else} + +{/if} + + diff --git a/src/lib/ui/components/IconGlyph.svelte b/src/lib/ui/components/IconGlyph.svelte new file mode 100644 index 0000000..81318d6 --- /dev/null +++ b/src/lib/ui/components/IconGlyph.svelte @@ -0,0 +1,53 @@ + + + + {#if name} + + {/if} + + + diff --git a/src/lib/ui/components/ModuleCard.svelte b/src/lib/ui/components/ModuleCard.svelte new file mode 100644 index 0000000..21a2854 --- /dev/null +++ b/src/lib/ui/components/ModuleCard.svelte @@ -0,0 +1,86 @@ + + + + + diff --git a/src/lib/ui/components/Panel.svelte b/src/lib/ui/components/Panel.svelte new file mode 100644 index 0000000..cbc68e3 --- /dev/null +++ b/src/lib/ui/components/Panel.svelte @@ -0,0 +1,81 @@ + + +
+ {#if title} +
+

{title}

+
+ {/if} +
+ {@render children?.()} +
+
+ + diff --git a/src/lib/ui/components/ServicePanel.svelte b/src/lib/ui/components/ServicePanel.svelte new file mode 100644 index 0000000..c292535 --- /dev/null +++ b/src/lib/ui/components/ServicePanel.svelte @@ -0,0 +1,17 @@ + + + + {#if group.summary?.length} + + {/if} + {#each group.services as service (service.id)} + + {/each} + diff --git a/src/lib/ui/components/ServiceRow.svelte b/src/lib/ui/components/ServiceRow.svelte new file mode 100644 index 0000000..0021424 --- /dev/null +++ b/src/lib/ui/components/ServiceRow.svelte @@ -0,0 +1,106 @@ + + +{#snippet rowContent()} + +
+

{service.label}

+

{service.description}

+
+ {#if service.detail} + + {/if} +{/snippet} + +{#if service.link} + + {@render rowContent()} + +{:else} +
+ {@render rowContent()} +
+{/if} + + diff --git a/src/lib/ui/components/StatusBadge.svelte b/src/lib/ui/components/StatusBadge.svelte new file mode 100644 index 0000000..a0fa058 --- /dev/null +++ b/src/lib/ui/components/StatusBadge.svelte @@ -0,0 +1,52 @@ + + +{label} + + diff --git a/src/lib/ui/components/StatusStrip.svelte b/src/lib/ui/components/StatusStrip.svelte new file mode 100644 index 0000000..6a2bcf1 --- /dev/null +++ b/src/lib/ui/components/StatusStrip.svelte @@ -0,0 +1,32 @@ + + +
+ {#each items as item (item.id)} + + {/each} +
+ + diff --git a/src/lib/ui/components/TelemetryCard.svelte b/src/lib/ui/components/TelemetryCard.svelte new file mode 100644 index 0000000..960aeaa --- /dev/null +++ b/src/lib/ui/components/TelemetryCard.svelte @@ -0,0 +1,158 @@ + + +
+
+ {#if card.icon} + + {/if} +

{card.label}

+
+ {value} + {#if progress !== null} + + {/if} + {#if card.sparkline?.length} + + {/if} + {#if card.detail || card.description} +

{card.detail || card.description}

+ {/if} +
+ + + + diff --git a/src/lib/ui/components/TelemetryGrid.svelte b/src/lib/ui/components/TelemetryGrid.svelte new file mode 100644 index 0000000..aead537 --- /dev/null +++ b/src/lib/ui/components/TelemetryGrid.svelte @@ -0,0 +1,22 @@ + + +
+ {#each cards as card (card.id)} + + {/each} +
+ + diff --git a/src/lib/ui/components/render.test.ts b/src/lib/ui/components/render.test.ts new file mode 100644 index 0000000..dd26b81 --- /dev/null +++ b/src/lib/ui/components/render.test.ts @@ -0,0 +1,97 @@ +import { render } from "svelte/server"; +import { describe, expect, test } from "vitest"; +import DashboardFrame from "./DashboardFrame.svelte"; +import FooterCell from "./FooterCell.svelte"; +import ServiceRow from "./ServiceRow.svelte"; +import TelemetryCard from "./TelemetryCard.svelte"; +import { dashboardPreviewFixtures } from "../fixtures"; + +describe("dashboard UI components", () => { + test("renders the primary generic dashboard fixture", () => { + const { body } = render(DashboardFrame, { + props: { + dashboard: dashboardPreviewFixtures.primary, + }, + }); + + expect(body).toContain("Operations Console"); + expect(body).toContain("Core Throughput"); + expect(body).toContain("Queue Workers"); + expect(body).toContain("data-icon-name=\"mdi:server-network\""); + expect(body).toContain("data-severity=\"warning\""); + expect(body).not.toContain("dashboard-title"); + }); + + test("renders another generic dashboard fixture through the same component", () => { + const { body } = render(DashboardFrame, { + props: { + dashboard: dashboardPreviewFixtures.secondary, + }, + }); + + expect(body).toContain("Support Desk"); + expect(body).toContain("Response Window"); + expect(body).toContain("Regional Nodes"); + expect(body).toContain("data-icon-name=\"mdi:headset\""); + expect(body).toContain("data-severity=\"loading\""); + }); + + test("renders optional service and status links as focusable anchors", () => { + const service = render(ServiceRow, { + props: { + service: { + id: "linked-service", + label: "Linked Service", + description: "Generic linked destination", + severity: "ok", + detail: "ready", + link: { href: "https://example.test/service", external: true }, + }, + }, + }); + const footer = render(FooterCell, { + props: { + item: { + id: "linked-status", + label: "Linked Status", + value: "open", + severity: "neutral", + link: { href: "https://example.test/status" }, + }, + }, + }); + + expect(service.body).toContain(" { + const withoutProgress = render(TelemetryCard, { + props: { + card: { + id: "bytes", + label: "Bytes Metric", + value: { kind: "bytes", value: 2048 }, + severity: "neutral", + }, + }, + }); + const withProgress = render(TelemetryCard, { + props: { + card: { + id: "bytes-with-progress", + label: "Bytes With Progress", + value: { kind: "bytes", value: 2048 }, + progress: 42, + severity: "neutral", + }, + }, + }); + + expect(withoutProgress.body).not.toContain("telemetry-card__bar"); + expect(withProgress.body).toContain("--metric-progress: 42%"); + }); +}); diff --git a/src/lib/ui/content-boundary.test.ts b/src/lib/ui/content-boundary.test.ts new file mode 100644 index 0000000..6613644 --- /dev/null +++ b/src/lib/ui/content-boundary.test.ts @@ -0,0 +1,49 @@ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; + +const forbiddenTerms = [ + "dimensionlab", + "dimension lab", + "vaultwarden", + "forgejo", + "grafana", + "uptime kuma", + "prometheus", + "backrest", + "open webui", + "comfyui", + "adminer", + "cockpit", + "ollama", +]; + +describe("UI content boundary", () => { + test("keeps environment-specific content out of reusable UI source", () => { + const source = readUiSource(join(process.cwd(), "src", "lib", "ui")); + const normalized = source.toLowerCase(); + + expect( + forbiddenTerms.filter((term) => normalized.includes(term)), + ).toEqual([]); + }); + + test("keeps icon rendering driven by icon identifiers", () => { + const source = readUiSource(join(process.cwd(), "src", "lib", "ui")); + + expect(source).not.toContain("@iconify-json/"); + expect(source).not.toContain("/icons/"); + }); +}); + +function readUiSource(path: string): string { + const stats = statSync(path); + if (stats.isFile()) { + if (path.endsWith(".test.ts")) return ""; + return readFileSync(path, "utf8"); + } + + return readdirSync(path) + .map((entry) => readUiSource(join(path, entry))) + .join("\n"); +} diff --git a/src/lib/ui/fixtures.ts b/src/lib/ui/fixtures.ts new file mode 100644 index 0000000..54f8b48 --- /dev/null +++ b/src/lib/ui/fixtures.ts @@ -0,0 +1,130 @@ +import type { UiDashboardPreview } from "./types"; + +export const dashboardPreviewFixtures: Record = { + primary: { + eyebrow: "Command Surface", + title: "Operations Console", + subtitle: "Capacity, latency, and queue health", + telemetry: [ + metric("core-throughput", "Core Throughput", "mdi:server-network", 82, "ok"), + metric("edge-cache", "Edge Cache", "mdi:database-clock", 67, "neutral"), + metric("queue-depth", "Queue Depth", "mdi:tray-full", 74, "warning"), + metric("error-budget", "Error Budget", "mdi:chart-timeline-variant", 18, "danger"), + metric("build-latency", "Build Latency", "mdi:timer-sand", 23, "stale"), + metric("worker-load", "Worker Load", "mdi:cpu-64-bit", 55, "ok"), + ], + modules: [ + { + id: "ambient-conditions", + title: "Local Node", + value: "21.4 C", + detail: "clear window", + icon: "mdi:weather-partly-cloudy", + severity: "ok", + }, + ], + serviceGroups: [ + { + id: "queue-workers", + title: "Queue Workers", + services: [ + service("ingest", "Ingest", "Event intake pipeline", "mdi:arrow-collapse-down", "ok", "1.252 ms"), + service("scheduler", "Scheduler", "Timed job coordinator", "mdi:calendar-clock", "warning", "1.487 ms"), + service("archive", "Archive", "Cold storage transfer", "mdi:archive-arrow-down", "ok", "1.301 ms"), + ], + }, + { + id: "regional-nodes", + title: "Regional Nodes", + services: [ + service("north", "North", "Primary traffic cell", "mdi:access-point", "ok", "899 ms"), + service("west", "West", "Replica traffic cell", "mdi:access-point-network", "neutral", "1.118 ms"), + service("south", "South", "Maintenance window", "mdi:wrench-clock", "stale", "paused"), + ], + }, + { + id: "control-plane", + title: "Control Plane", + services: [ + service("identity", "Identity", "Access token exchange", "mdi:account-key", "ok", "721 ms"), + service("policy", "Policy", "Rules evaluation", "mdi:shield-check", "ok", "812 ms"), + service("audit", "Audit", "Event ledger writer", "mdi:text-box-search", "warning", "1.442 ms"), + ], + }, + ], + statusItems: [ + { id: "system", label: "System", value: "Nominal", severity: "ok" }, + { id: "sync", label: "Last Sync", value: "2 minutes ago", severity: "stale" }, + { id: "uptime", label: "Uptime", value: "14d 08h", severity: "neutral" }, + { id: "refresh", label: "Refresh", value: "15s", severity: "neutral" }, + ], + }, + secondary: { + eyebrow: "Support Surface", + title: "Support Desk", + subtitle: "Response, routing, and regional health", + telemetry: [ + metric("response-window", "Response Window", "mdi:headset", 41, "ok"), + metric("ticket-load", "Ticket Load", "mdi:ticket-confirmation", 68, "warning"), + metric("handoff-drift", "Handoff Drift", "mdi:swap-horizontal", 11, "neutral"), + metric("coverage-gap", "Coverage Gap", "mdi:map-marker-alert", 7, "danger"), + metric("routing-warmup", "Routing Warmup", "mdi:progress-clock", 0, "loading"), + ], + modules: [ + { + id: "shift-state", + title: "Shift State", + value: "covered", + detail: "static fixture", + icon: "mdi:clipboard-check-outline", + severity: "ok", + }, + ], + serviceGroups: [ + { + id: "regional-nodes", + title: "Regional Nodes", + services: [ + service("desk-a", "Desk A", "Frontline queue", "mdi:monitor-dashboard", "ok", "843 ms"), + service("desk-b", "Desk B", "Escalation queue", "mdi:monitor-star", "warning", "1.204 ms"), + service("desk-c", "Desk C", "Overflow queue", "mdi:monitor-off", "unavailable", "offline"), + ], + }, + ], + statusItems: [ + { id: "desk", label: "Desk", value: "Covered", severity: "ok" }, + { id: "handoff", label: "Handoff", value: "Pending", severity: "warning" }, + { id: "routing", label: "Routing", value: "Manual", severity: "neutral" }, + ], + }, +}; + +function metric( + id: string, + label: string, + icon: string, + progress: number, + severity: UiDashboardPreview["telemetry"][number]["severity"], +) { + return { + id, + label, + icon, + progress, + severity, + value: { kind: "percent" as const, value: progress }, + detail: "static fixture", + sparkline: [12, 18, 15, 28, 24, progress], + }; +} + +function service( + id: string, + label: string, + description: string, + icon: string, + severity: UiDashboardPreview["serviceGroups"][number]["services"][number]["severity"], + detail: string, +) { + return { id, label, description, icon, severity, detail }; +} diff --git a/src/lib/ui/format.ts b/src/lib/ui/format.ts new file mode 100644 index 0000000..4a8a5f1 --- /dev/null +++ b/src/lib/ui/format.ts @@ -0,0 +1,41 @@ +import type { UiMetricValue } from "./types"; + +const byteUnits = ["B", "KB", "MB", "GB", "TB"]; + +export function formatMetricValue(metric: UiMetricValue): string { + if (metric.kind === "text") return String(metric.value); + + const value = typeof metric.value === "number" ? metric.value : Number(metric.value); + const precision = metric.precision ?? inferPrecision(value); + const unit = metric.unit ?? defaultUnit(metric.kind); + + if (metric.kind === "bytes") return formatBytes(value, precision); + return `${value.toFixed(precision)}${unit ? ` ${unit}` : ""}`; +} + +export function clampPercent(value = 0): number { + if (!Number.isFinite(value)) return 0; + return Math.max(0, Math.min(100, value)); +} + +function defaultUnit(kind: UiMetricValue["kind"]): string { + if (kind === "percent") return "%"; + if (kind === "temperature") return "C"; + if (kind === "latency") return "ms"; + return ""; +} + +function inferPrecision(value: number): number { + return Number.isInteger(value) ? 0 : 1; +} + +function formatBytes(value: number, precision: number): string { + let size = value; + let index = 0; + while (size >= 1024 && index < byteUnits.length - 1) { + size /= 1024; + index += 1; + } + + return `${size.toFixed(precision)} ${byteUnits[index]}`; +} diff --git a/src/lib/ui/index.ts b/src/lib/ui/index.ts new file mode 100644 index 0000000..df2a02c --- /dev/null +++ b/src/lib/ui/index.ts @@ -0,0 +1,23 @@ +export { default as DashboardFrame } from "./components/DashboardFrame.svelte"; +export { default as FooterCell } from "./components/FooterCell.svelte"; +export { default as IconGlyph } from "./components/IconGlyph.svelte"; +export { default as ModuleCard } from "./components/ModuleCard.svelte"; +export { default as Panel } from "./components/Panel.svelte"; +export { default as ServicePanel } from "./components/ServicePanel.svelte"; +export { default as ServiceRow } from "./components/ServiceRow.svelte"; +export { default as StatusBadge } from "./components/StatusBadge.svelte"; +export { default as StatusStrip } from "./components/StatusStrip.svelte"; +export { default as TelemetryCard } from "./components/TelemetryCard.svelte"; +export { default as TelemetryGrid } from "./components/TelemetryGrid.svelte"; +export { dashboardPreviewFixtures } from "./fixtures"; +export type { + UiDashboardPreview, + UiLink, + UiMetricValue, + UiModuleBlock, + UiServiceGroup, + UiServiceRow, + UiSeverity, + UiStatusItem, + UiTelemetryCard, +} from "./types"; diff --git a/src/lib/ui/tokens.css b/src/lib/ui/tokens.css new file mode 100644 index 0000000..56c2f7e --- /dev/null +++ b/src/lib/ui/tokens.css @@ -0,0 +1,81 @@ +:root { + color-scheme: dark; + --ui-color-canvas: #020302; + --ui-color-surface: #060706; + --ui-color-surface-raised: #0b0d0c; + --ui-color-line: rgba(244, 244, 244, 0.16); + --ui-color-line-strong: rgba(244, 244, 244, 0.32); + --ui-color-text: #f3f4ed; + --ui-color-muted: #8d948c; + --ui-color-dim: #555b55; + --ui-color-accent: #d7ff00; + --ui-color-ok: #bfff00; + --ui-color-warning: #ffb020; + --ui-color-danger: #ff1744; + --ui-color-stale: #82909a; + --ui-color-unavailable: #65707a; + --ui-font-mono: "IBM Plex Mono", "Roboto Mono", "SFMono-Regular", Consolas, monospace; + --ui-font-display: "Teko", "IBM Plex Mono", "Roboto Mono", monospace; + --ui-space-1: 0.25rem; + --ui-space-2: 0.5rem; + --ui-space-3: 0.75rem; + --ui-space-4: 1rem; + --ui-space-5: 1.25rem; + --ui-space-6: 1.5rem; + --ui-radius-none: 0; + --ui-border: 1px solid var(--ui-color-line); + --ui-border-strong: 1px solid var(--ui-color-line-strong); + --ui-shadow-hard: 0 0 0 1px rgba(215, 255, 0, 0.12) inset; + --ui-z-base: 0; + --ui-z-panel: 1; + --ui-z-overlay: 10; + --ui-density-card-min: 9.75rem; + --ui-focus-ring: 0 0 0 2px var(--ui-color-canvas), 0 0 0 4px var(--ui-color-accent); +} + +html { + background: var(--ui-color-canvas); +} + +body { + min-width: 320px; + min-height: 100vh; + margin: 0; + background: + linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px), + var(--ui-color-canvas); + background-size: 48px 48px, 48px 48px, auto; + color: var(--ui-color-text); + font-family: var(--ui-font-mono); + text-rendering: geometricPrecision; +} + +button, +input, +textarea, +select { + font: inherit; +} + +button:focus-visible, +a:focus-visible, +[tabindex]:focus-visible { + outline: 0; + box-shadow: var(--ui-focus-ring); +} + +a { + color: inherit; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} diff --git a/src/lib/ui/types.ts b/src/lib/ui/types.ts new file mode 100644 index 0000000..1844491 --- /dev/null +++ b/src/lib/ui/types.ts @@ -0,0 +1,86 @@ +export type UiSeverity = + | "neutral" + | "ok" + | "warning" + | "danger" + | "stale" + | "unavailable" + | "loading"; + +export type UiMetricKind = + | "bytes" + | "latency" + | "number" + | "percent" + | "temperature" + | "text"; + +export interface UiMetricValue { + kind: UiMetricKind; + value: number | string; + unit?: string; + precision?: number; +} + +export interface UiLink { + href: string; + label?: string; + external?: boolean; +} + +export interface UiTelemetryCard { + id: string; + label: string; + value: UiMetricValue; + detail?: string; + description?: string; + icon?: string; + severity: UiSeverity; + progress?: number; + sparkline?: number[]; +} + +export interface UiServiceRow { + id: string; + label: string; + description: string; + icon?: string; + severity: UiSeverity; + detail?: string; + link?: UiLink; +} + +export interface UiServiceGroup { + id: string; + title: string; + services: UiServiceRow[]; + summary?: UiStatusItem[]; +} + +export interface UiStatusItem { + id: string; + label: string; + value: string; + severity?: UiSeverity; + link?: UiLink; +} + +export interface UiModuleBlock { + id: string; + title?: string; + label?: string; + value?: string; + detail?: string; + icon?: string; + severity?: UiSeverity; +} + +export interface UiDashboardPreview { + title: string; + subtitle?: string; + eyebrow?: string; + telemetry: UiTelemetryCard[]; + serviceGroups: UiServiceGroup[]; + modules: UiModuleBlock[]; + statusItems: UiStatusItem[]; +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 68a3c90..5e495a8 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,83 +1,124 @@ - {data.dashboard.title} | Dimension Lab + {dashboard.title} -
-
-

{data.dashboard.subtitle}

-

{data.dashboard.title}

-

{data.dashboard.message}

-
- Runtime - {data.dashboard.status} -
-
-
+
+
+ {#each fixtureKeys as key} + + {/each} +
+ + +
diff --git a/src/routes/page.test.ts b/src/routes/page.test.ts new file mode 100644 index 0000000..5c9d0b7 --- /dev/null +++ b/src/routes/page.test.ts @@ -0,0 +1,28 @@ +import { render } from "svelte/server"; +import { describe, expect, test } from "vitest"; +import Page from "./+page.svelte"; + +describe("home page design-system preview", () => { + test("renders the runtime dashboard data from the server load", () => { + const { body } = render(Page, { + props: { + data: { + dashboard: { + title: "Runtime Surface", + subtitle: "Loaded from persistence", + status: "building", + message: "Runtime model is available.", + schemaVersion: "dashboard.v1", + currentRevisionId: "revision-1234567890", + }, + }, + }, + }); + + expect(body).toContain("Runtime Surface"); + expect(body).toContain("Loaded from persistence"); + expect(body).toContain("revision"); + expect(body).toContain("primary"); + expect(body).toContain("secondary"); + }); +}); From d95f064189984b74ba6c824c5eda4814ae3dd55f Mon Sep 17 00:00:00 2001 From: vince Date: Thu, 18 Jun 2026 18:53:48 +0200 Subject: [PATCH 07/90] feat: add Storybook component explorer --- .gitignore | 1 + .storybook/main.ts | 19 + .storybook/preview.ts | 23 ++ README.md | 10 +- bun.lock | 337 +++++++++++++++++- package.json | 7 + src/lib/ui/components/Badge.svelte | 14 + src/lib/ui/components/Button.svelte | 80 +++++ src/lib/ui/components/DashboardHeader.svelte | 71 ++++ src/lib/ui/components/FooterStatusCell.svelte | 8 + src/lib/ui/components/GridFrame.svelte | 30 ++ src/lib/ui/components/IconButton.svelte | 44 +++ src/lib/ui/components/ProgressMeter.svelte | 58 +++ src/lib/ui/components/Separator.svelte | 39 ++ .../ui/components/ServiceGroupPanel.svelte | 8 + src/lib/ui/components/Sparkline.svelte | 58 +++ src/lib/ui/components/SystemState.svelte | 67 ++++ src/lib/ui/components/TelemetryStrip.svelte | 8 + src/lib/ui/components/WeatherModule.svelte | 8 + src/lib/ui/index.ts | 13 + src/lib/ui/stories/Badge.stories.svelte | 21 ++ src/lib/ui/stories/Button.stories.svelte | 19 + .../ui/stories/DashboardFrame.stories.svelte | 15 + .../ui/stories/DashboardHeader.stories.svelte | 31 ++ .../stories/DashboardOnePager.stories.svelte | 18 + src/lib/ui/stories/FooterCell.stories.svelte | 16 + .../stories/FooterStatusCell.stories.svelte | 16 + src/lib/ui/stories/GridFrame.stories.svelte | 37 ++ src/lib/ui/stories/IconButton.stories.svelte | 17 + src/lib/ui/stories/IconGlyph.stories.svelte | 21 ++ src/lib/ui/stories/ModuleCard.stories.svelte | 15 + src/lib/ui/stories/Panel.stories.svelte | 42 +++ .../ui/stories/ProgressMeter.stories.svelte | 16 + src/lib/ui/stories/Separator.stories.svelte | 21 ++ .../stories/ServiceGroupPanel.stories.svelte | 16 + .../ui/stories/ServicePanel.stories.svelte | 16 + src/lib/ui/stories/ServiceRow.stories.svelte | 19 + src/lib/ui/stories/Sparkline.stories.svelte | 16 + src/lib/ui/stories/StatusBadge.stories.svelte | 18 + src/lib/ui/stories/StatusStrip.stories.svelte | 27 ++ src/lib/ui/stories/SystemState.stories.svelte | 16 + .../ui/stories/TelemetryCard.stories.svelte | 21 ++ .../ui/stories/TelemetryGrid.stories.svelte | 15 + .../ui/stories/TelemetryStrip.stories.svelte | 15 + .../ui/stories/WeatherModule.stories.svelte | 16 + src/lib/ui/stories/story-data.ts | 258 ++++++++++++++ src/lib/ui/storybook.test.ts | 77 ++++ 47 files changed, 1730 insertions(+), 8 deletions(-) create mode 100644 .storybook/main.ts create mode 100644 .storybook/preview.ts create mode 100644 src/lib/ui/components/Badge.svelte create mode 100644 src/lib/ui/components/Button.svelte create mode 100644 src/lib/ui/components/DashboardHeader.svelte create mode 100644 src/lib/ui/components/FooterStatusCell.svelte create mode 100644 src/lib/ui/components/GridFrame.svelte create mode 100644 src/lib/ui/components/IconButton.svelte create mode 100644 src/lib/ui/components/ProgressMeter.svelte create mode 100644 src/lib/ui/components/Separator.svelte create mode 100644 src/lib/ui/components/ServiceGroupPanel.svelte create mode 100644 src/lib/ui/components/Sparkline.svelte create mode 100644 src/lib/ui/components/SystemState.svelte create mode 100644 src/lib/ui/components/TelemetryStrip.svelte create mode 100644 src/lib/ui/components/WeatherModule.svelte create mode 100644 src/lib/ui/stories/Badge.stories.svelte create mode 100644 src/lib/ui/stories/Button.stories.svelte create mode 100644 src/lib/ui/stories/DashboardFrame.stories.svelte create mode 100644 src/lib/ui/stories/DashboardHeader.stories.svelte create mode 100644 src/lib/ui/stories/DashboardOnePager.stories.svelte create mode 100644 src/lib/ui/stories/FooterCell.stories.svelte create mode 100644 src/lib/ui/stories/FooterStatusCell.stories.svelte create mode 100644 src/lib/ui/stories/GridFrame.stories.svelte create mode 100644 src/lib/ui/stories/IconButton.stories.svelte create mode 100644 src/lib/ui/stories/IconGlyph.stories.svelte create mode 100644 src/lib/ui/stories/ModuleCard.stories.svelte create mode 100644 src/lib/ui/stories/Panel.stories.svelte create mode 100644 src/lib/ui/stories/ProgressMeter.stories.svelte create mode 100644 src/lib/ui/stories/Separator.stories.svelte create mode 100644 src/lib/ui/stories/ServiceGroupPanel.stories.svelte create mode 100644 src/lib/ui/stories/ServicePanel.stories.svelte create mode 100644 src/lib/ui/stories/ServiceRow.stories.svelte create mode 100644 src/lib/ui/stories/Sparkline.stories.svelte create mode 100644 src/lib/ui/stories/StatusBadge.stories.svelte create mode 100644 src/lib/ui/stories/StatusStrip.stories.svelte create mode 100644 src/lib/ui/stories/SystemState.stories.svelte create mode 100644 src/lib/ui/stories/TelemetryCard.stories.svelte create mode 100644 src/lib/ui/stories/TelemetryGrid.stories.svelte create mode 100644 src/lib/ui/stories/TelemetryStrip.stories.svelte create mode 100644 src/lib/ui/stories/WeatherModule.stories.svelte create mode 100644 src/lib/ui/stories/story-data.ts create mode 100644 src/lib/ui/storybook.test.ts diff --git a/.gitignore b/.gitignore index 7752ebb..7d8d6c4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ data/*.sqlite-* coverage/ playwright-report/ test-results/ +storybook-static/ diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..89968a2 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,19 @@ +import type { StorybookConfig } from "@storybook/sveltekit"; + +const config: StorybookConfig = { + stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|ts|svelte)"], + addons: [ + "@storybook/addon-svelte-csf", + "@storybook/addon-a11y", + "@storybook/addon-vitest", + ], + framework: { + name: "@storybook/sveltekit", + options: {}, + }, + docs: { + autodocs: "tag", + }, +}; + +export default config; diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 0000000..33f01f6 --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1,23 @@ +import "../src/app.css"; +import type { Preview } from "@storybook/sveltekit"; + +const preview: Preview = { + parameters: { + backgrounds: { + default: "canvas", + values: [ + { name: "canvas", value: "#020302" }, + { name: "raised", value: "#0b0d0c" }, + ], + }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + layout: "fullscreen", + }, +}; + +export default preview; diff --git a/README.md b/README.md index 3dadcbd..9981182 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ This MVP uses Drizzle with Bun SQLite for local file-backed persistence. - `bun run test`: run Vitest. - `bun run build`: build the production app. - `bun run preview`: preview the production build. +- `bun run storybook`: start the component explorer on port 6006. +- `bun run build-storybook`: build the static Storybook review artifact. - `bun run db:generate`: generate Drizzle migrations from the server schema. - `bun run db:check`: validate migration consistency. @@ -51,4 +53,10 @@ with an explicit migration error. The SvelteKit build uses the Node adapter, and the current persistence runtime is Bun because the MVP SQLite driver is `bun:sqlite`. Later issues add the -component system, Storybook, seed data expansion, and rendering pipeline. +seed data expansion and rendering pipeline. + +## Storybook + +Storybook covers the reusable UI components with generic fixtures only. Stories +must not import environment-specific dashboard content; the presentation layer +accepts labels, values, icons, status, and links through typed props. diff --git a/bun.lock b/bun.lock index 335ff14..db8e635 100644 --- a/bun.lock +++ b/bun.lock @@ -12,12 +12,17 @@ "drizzle-orm": "^0.45.2", }, "devDependencies": { + "@storybook/addon-a11y": "^10.4.6", + "@storybook/addon-svelte-csf": "^5.1.2", + "@storybook/addon-vitest": "^10.4.6", + "@storybook/sveltekit": "^10.4.6", "@sveltejs/adapter-node": "^5.5.4", "@sveltejs/kit": "^2.65.2", "@sveltejs/vite-plugin-svelte": "^7.1.2", "@types/bun": "^1.3.14", "@types/node": "^25.9.3", "drizzle-kit": "^0.31.10", + "storybook": "^10.4.6", "svelte": "^5.56.3", "svelte-check": "^4.6.0", "typescript": "^6.0.3", @@ -27,11 +32,19 @@ }, }, "packages": { + "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], - "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], - "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], @@ -107,7 +120,85 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], - "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.127.0", "", { "os": "android", "cpu": "arm" }, "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ=="], + + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.127.0", "", { "os": "android", "cpu": "arm64" }, "sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg=="], + + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.127.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg=="], + + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.127.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw=="], + + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.127.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA=="], + + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ=="], + + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g=="], + + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ=="], + + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA=="], + + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.127.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ=="], + + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ=="], + + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g=="], + + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.127.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q=="], + + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ=="], + + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg=="], + + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.127.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ=="], + + "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.127.0", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ=="], + + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.127.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw=="], + + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.127.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw=="], + + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.127.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w=="], + + "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + + "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.21.3", "", { "os": "android", "cpu": "arm" }, "sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg=="], + + "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.21.3", "", { "os": "android", "cpu": "arm64" }, "sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw=="], + + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.21.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg=="], + + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.21.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg=="], + + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.21.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw=="], + + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3", "", { "os": "linux", "cpu": "arm" }, "sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ=="], + + "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.21.3", "", { "os": "linux", "cpu": "arm" }, "sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ=="], + + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.21.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ=="], + + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.21.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw=="], + + "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.21.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A=="], + + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.21.3", "", { "os": "linux", "cpu": "none" }, "sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og=="], + + "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.21.3", "", { "os": "linux", "cpu": "none" }, "sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw=="], + + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.21.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q=="], + + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.21.3", "", { "os": "linux", "cpu": "x64" }, "sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q=="], + + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.21.3", "", { "os": "linux", "cpu": "x64" }, "sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA=="], + + "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.21.3", "", { "os": "none", "cpu": "arm64" }, "sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg=="], + + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.21.3", "", { "dependencies": { "@emnapi/core": "1.11.0", "@emnapi/runtime": "1.11.0", "@napi-rs/wasm-runtime": "^1.1.5" }, "cpu": "none" }, "sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA=="], + + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.21.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg=="], + + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.21.3", "", { "os": "win32", "cpu": "x64" }, "sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], @@ -205,6 +296,28 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@storybook/addon-a11y": ["@storybook/addon-a11y@10.4.6", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.4.6" } }, "sha512-XCJy+f0DFOiCgUU9knRDlLDxVFI+AAQ3/wE/NF85zB9iDPPS2DwkSN+mas3zDgHt66zhN8Cq3/UiyCDUweV9Zw=="], + + "@storybook/addon-svelte-csf": ["@storybook/addon-svelte-csf@5.1.2", "", { "dependencies": { "@storybook/csf": "^0.1.13", "dedent": "^1.5.3", "es-toolkit": "^1.26.1", "esrap": "^1.2.2", "magic-string": "^0.30.12", "svelte-ast-print": "^0.4.0", "zimmerframe": "^1.1.2" }, "peerDependencies": { "@storybook/svelte": "^0.0.0-0 || ^8.2.0 || ^9.0.0 || ^9.1.0-0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0", "@sveltejs/vite-plugin-svelte": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "storybook": "^0.0.0-0 || ^8.2.0 || ^9.0.0 || ^9.1.0-0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0", "svelte": "^5.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-NpImknEb48J7yr/ArTYpvhDSvGUrgm5Nuybu9PCicjSKTACsXX7cln2R19572ORtns399RTE+t20BBOKxSPm2g=="], + + "@storybook/addon-vitest": ["@storybook/addon-vitest@10.4.6", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2" }, "peerDependencies": { "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", "storybook": "^10.4.6", "vitest": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/browser-playwright", "@vitest/runner", "vitest"] }, "sha512-VvskHge0GZy86LG6kcY5Ww34z8rDV8JBxqSdUpcJVsWfIvyX6MfAbqI76LlereSyBIJGZJZsqaLwRXsQoVY+0Q=="], + + "@storybook/builder-vite": ["@storybook/builder-vite@10.4.6", "", { "dependencies": { "@storybook/csf-plugin": "10.4.6", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.4.6", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-BHBtD81HiXUiDQz/CaFynLtWmm7AFUQn8VnXuHipZ8KlnUANopa4yqdVuy/Gwz8ub254uFI5NMZsW/KlgWNgNg=="], + + "@storybook/csf": ["@storybook/csf@0.1.13", "", { "dependencies": { "type-fest": "^2.19.0" } }, "sha512-7xOOwCLGB3ebM87eemep89MYRFTko+D8qE7EdAAq74lgdqRR5cOUtYWJLjO2dLtP94nqoOdHJo6MdLLKzg412Q=="], + + "@storybook/csf-plugin": ["@storybook/csf-plugin@10.4.6", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.4.6", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-NILLxDqpA/JR/AazGWpsz+4fadJwRU4uhHephGtYpVOWnQA/DkJfKT6zpcJVq8+QA8A2zKMLX3GVKsXIrxjuDA=="], + + "@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="], + + "@storybook/icons": ["@storybook/icons@2.0.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-KZBCpXsshAIjczYNXR/rlxEtCUX/eAbpFNwKi8bcOomrLA4t/SyPz5RF+lVPO2oZBUE4sAkt43mfJUevQDSEEw=="], + + "@storybook/svelte": ["@storybook/svelte@10.4.6", "", { "dependencies": { "ts-dedent": "^2.0.0", "type-fest": "^5.6.0" }, "peerDependencies": { "storybook": "^10.4.6", "svelte": "^5.0.0" } }, "sha512-U73tDy/2vgY83Zjjy7Q5Lz0ElpZZjGQHZHZd1rrD0gv1+Zas/7aTnRjRqIUrBNqHBx1S8VA5Tzp5YI0FkM1IOw=="], + + "@storybook/svelte-vite": ["@storybook/svelte-vite@10.4.6", "", { "dependencies": { "@storybook/builder-vite": "10.4.6", "@storybook/svelte": "10.4.6", "magic-string": "^0.30.0", "svelte2tsx": "^0.7.44", "typescript": "^4.9.4 || ^5.0.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "storybook": "^10.4.6", "svelte": "^5.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-zyFg12tksWb0gLK29ucyZO5oNh4veu4biMeJgAEPEALjHw8HysP7mTA4547JYprk3JzORpwSqQ7Y9TPPB0ZwFA=="], + + "@storybook/sveltekit": ["@storybook/sveltekit@10.4.6", "", { "dependencies": { "@storybook/builder-vite": "10.4.6", "@storybook/svelte": "10.4.6", "@storybook/svelte-vite": "10.4.6" }, "peerDependencies": { "storybook": "^10.4.6", "svelte": "^5.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-DAbNFUr3B/XiUUUZak2TcjxCxB7eRCCgt4P0MSm9LdFTyePBdU13jybuArUfHqp6lgwqNgbFIjv1nazPjHq51g=="], + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.10", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA=="], "@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.5.4", "", { "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.59.0" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ=="], @@ -215,8 +328,16 @@ "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@7.1.2", "", { "dependencies": { "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.2" }, "peerDependencies": { "svelte": "^5.46.4", "vite": "^8.0.0-beta.7 || ^8.0.0" } }, "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA=="], + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], + + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], + + "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -233,7 +354,7 @@ "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], - "@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], + "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], "@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], @@ -243,27 +364,41 @@ "@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], - "@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], + "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], + "@webcontainer/env": ["@webcontainer/env@1.1.1", "", {}, "sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng=="], + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], + + "axe-core": ["axe-core@4.12.1", "", {}, "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA=="], + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], @@ -275,12 +410,30 @@ "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + + "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], + + "dedent-js": ["dedent-js@1.0.1", "", {}, "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="], + "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], @@ -289,11 +442,15 @@ "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], + "es-toolkit": ["es-toolkit@1.47.1", "", {}, "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], - "esrap": ["esrap@2.2.11", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ=="], + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esrap": ["esrap@1.4.9", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g=="], "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], @@ -313,12 +470,22 @@ "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], @@ -349,8 +516,14 @@ "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], @@ -359,18 +532,38 @@ "obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], + "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + + "oxc-parser": ["oxc-parser@0.127.0", "", { "dependencies": { "@oxc-project/types": "^0.127.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.127.0", "@oxc-parser/binding-android-arm64": "0.127.0", "@oxc-parser/binding-darwin-arm64": "0.127.0", "@oxc-parser/binding-darwin-x64": "0.127.0", "@oxc-parser/binding-freebsd-x64": "0.127.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.127.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.127.0", "@oxc-parser/binding-linux-arm64-gnu": "0.127.0", "@oxc-parser/binding-linux-arm64-musl": "0.127.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-musl": "0.127.0", "@oxc-parser/binding-linux-s390x-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-musl": "0.127.0", "@oxc-parser/binding-openharmony-arm64": "0.127.0", "@oxc-parser/binding-wasm32-wasi": "0.127.0", "@oxc-parser/binding-win32-arm64-msvc": "0.127.0", "@oxc-parser/binding-win32-ia32-msvc": "0.127.0", "@oxc-parser/binding-win32-x64-msvc": "0.127.0" } }, "sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA=="], + + "oxc-resolver": ["oxc-resolver@11.21.3", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.21.3", "@oxc-resolver/binding-android-arm64": "11.21.3", "@oxc-resolver/binding-darwin-arm64": "11.21.3", "@oxc-resolver/binding-darwin-x64": "11.21.3", "@oxc-resolver/binding-freebsd-x64": "11.21.3", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.3", "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.3", "@oxc-resolver/binding-linux-arm64-gnu": "11.21.3", "@oxc-resolver/binding-linux-arm64-musl": "11.21.3", "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.3", "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.3", "@oxc-resolver/binding-linux-riscv64-musl": "11.21.3", "@oxc-resolver/binding-linux-s390x-gnu": "11.21.3", "@oxc-resolver/binding-linux-x64-gnu": "11.21.3", "@oxc-resolver/binding-linux-x64-musl": "11.21.3", "@oxc-resolver/binding-openharmony-arm64": "11.21.3", "@oxc-resolver/binding-wasm32-wasi": "11.21.3", "@oxc-resolver/binding-win32-arm64-msvc": "11.21.3", "@oxc-resolver/binding-win32-x64-msvc": "11.21.3" } }, "sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + + "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], + + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], @@ -381,8 +574,16 @@ "rollup": ["rollup@4.62.0", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.0", "@rollup/rollup-android-arm64": "4.62.0", "@rollup/rollup-darwin-arm64": "4.62.0", "@rollup/rollup-darwin-x64": "4.62.0", "@rollup/rollup-freebsd-arm64": "4.62.0", "@rollup/rollup-freebsd-x64": "4.62.0", "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", "@rollup/rollup-linux-arm-musleabihf": "4.62.0", "@rollup/rollup-linux-arm64-gnu": "4.62.0", "@rollup/rollup-linux-arm64-musl": "4.62.0", "@rollup/rollup-linux-loong64-gnu": "4.62.0", "@rollup/rollup-linux-loong64-musl": "4.62.0", "@rollup/rollup-linux-ppc64-gnu": "4.62.0", "@rollup/rollup-linux-ppc64-musl": "4.62.0", "@rollup/rollup-linux-riscv64-gnu": "4.62.0", "@rollup/rollup-linux-riscv64-musl": "4.62.0", "@rollup/rollup-linux-s390x-gnu": "4.62.0", "@rollup/rollup-linux-x64-gnu": "4.62.0", "@rollup/rollup-linux-x64-musl": "4.62.0", "@rollup/rollup-openbsd-x64": "4.62.0", "@rollup/rollup-openharmony-arm64": "4.62.0", "@rollup/rollup-win32-arm64-msvc": "4.62.0", "@rollup/rollup-win32-ia32-msvc": "4.62.0", "@rollup/rollup-win32-x64-gnu": "4.62.0", "@rollup/rollup-win32-x64-msvc": "4.62.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA=="], + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="], + + "semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="], + "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], @@ -399,12 +600,24 @@ "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + "storybook": ["storybook@10.4.6", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-6wkA6LxfDSSilloITsrFOJfsnw0mDUP2h8Ls+lRt8oRsudtz2RWFhLv+Toiwg6NW7hUpdTDc2hzR7DztJid6+A=="], + + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "svelte": ["svelte@5.56.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.11", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA=="], + "svelte-ast-print": ["svelte-ast-print@0.4.2", "", { "dependencies": { "esrap": "1.2.2", "zimmerframe": "1.1.2" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-hRHHufbJoArFmDYQKCpCvc0xUuIEfwYksvyLYEQyH+1xb5LD5sM/IthfooCdXZQtOIqXz6xm7NmaqdfwG4kh6w=="], + "svelte-check": ["svelte-check@4.6.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "0.1.1", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-KhVnDFDSid57mmZtHz8gfW8AAGylOZ0vPnOIzVmAL+urzwK8sBYXRss953gD8T0OdgAQ11mdWhE6uadmtOz8TQ=="], + "svelte2tsx": ["svelte2tsx@0.7.56", "", { "dependencies": { "dedent-js": "^1.0.1", "scule": "^1.3.0" }, "peerDependencies": { "svelte": "^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0", "typescript": "^4.9.4 || ^5.0.0 || ^6.0.0" } }, "sha512-NTvqqL+goYlW8gWNajk81L07+uu7jw5V2m1Az5MZbYm3GEydcHXh+uTrLHM9SuGuaqCtF90vlMXkOVBotfH94g=="], + + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], @@ -413,34 +626,86 @@ "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + "ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tsx": ["tsx@4.22.4", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg=="], + "type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="], "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], "vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "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" }, "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.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + + "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "@oxc-resolver/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="], + + "@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], + + "@rolldown/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], + "@storybook/svelte/type-fest": ["type-fest@5.7.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg=="], + + "@storybook/svelte-vite/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + + "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + + "@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + + "@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "@vitest/mocker/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], + "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "rolldown/@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], + + "storybook/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "svelte/esrap": ["esrap@2.2.11", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ=="], + + "svelte-ast-print/esrap": ["esrap@1.2.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@types/estree": "^1.0.1" } }, "sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw=="], + + "svelte-ast-print/zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="], + "tsx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + "vitest/@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], + + "vitest/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -485,6 +750,62 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + + "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + + "storybook/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "storybook/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "storybook/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "storybook/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "storybook/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "storybook/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "storybook/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "storybook/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "storybook/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "storybook/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "storybook/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "storybook/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "storybook/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "storybook/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "storybook/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "storybook/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "storybook/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "storybook/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "storybook/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "storybook/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "storybook/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "storybook/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "storybook/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "storybook/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "storybook/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "storybook/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], @@ -536,5 +857,7 @@ "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], } } diff --git a/package.json b/package.json index 2bef9c3..e8a81a7 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "dev": "vite --host 0.0.0.0", "build": "vite build", "preview": "vite preview --host 0.0.0.0", + "storybook": "storybook dev -p 6006 --host 0.0.0.0", + "build-storybook": "storybook build", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "test": "vitest run", "db:generate": "drizzle-kit generate", @@ -20,12 +22,17 @@ "drizzle-orm": "^0.45.2" }, "devDependencies": { + "@storybook/addon-a11y": "^10.4.6", + "@storybook/addon-svelte-csf": "^5.1.2", + "@storybook/addon-vitest": "^10.4.6", + "@storybook/sveltekit": "^10.4.6", "@sveltejs/adapter-node": "^5.5.4", "@sveltejs/kit": "^2.65.2", "@sveltejs/vite-plugin-svelte": "^7.1.2", "@types/bun": "^1.3.14", "@types/node": "^25.9.3", "drizzle-kit": "^0.31.10", + "storybook": "^10.4.6", "svelte": "^5.56.3", "svelte-check": "^4.6.0", "typescript": "^6.0.3", diff --git a/src/lib/ui/components/Badge.svelte b/src/lib/ui/components/Badge.svelte new file mode 100644 index 0000000..2e9fc8d --- /dev/null +++ b/src/lib/ui/components/Badge.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/lib/ui/components/Button.svelte b/src/lib/ui/components/Button.svelte new file mode 100644 index 0000000..f35817a --- /dev/null +++ b/src/lib/ui/components/Button.svelte @@ -0,0 +1,80 @@ + + + + + diff --git a/src/lib/ui/components/DashboardHeader.svelte b/src/lib/ui/components/DashboardHeader.svelte new file mode 100644 index 0000000..e8d13f2 --- /dev/null +++ b/src/lib/ui/components/DashboardHeader.svelte @@ -0,0 +1,71 @@ + + +
+
+ {#if eyebrow} +

{eyebrow}

+ {/if} +

{title}

+ {#if subtitle} + {subtitle} + {/if} +
+ {#if module} + + {/if} +
+ + diff --git a/src/lib/ui/components/FooterStatusCell.svelte b/src/lib/ui/components/FooterStatusCell.svelte new file mode 100644 index 0000000..997ae3e --- /dev/null +++ b/src/lib/ui/components/FooterStatusCell.svelte @@ -0,0 +1,8 @@ + + + diff --git a/src/lib/ui/components/GridFrame.svelte b/src/lib/ui/components/GridFrame.svelte new file mode 100644 index 0000000..1368dda --- /dev/null +++ b/src/lib/ui/components/GridFrame.svelte @@ -0,0 +1,30 @@ + + +
+ {@render children?.()} +
+ + diff --git a/src/lib/ui/components/IconButton.svelte b/src/lib/ui/components/IconButton.svelte new file mode 100644 index 0000000..83f2a59 --- /dev/null +++ b/src/lib/ui/components/IconButton.svelte @@ -0,0 +1,44 @@ + + + + + diff --git a/src/lib/ui/components/ProgressMeter.svelte b/src/lib/ui/components/ProgressMeter.svelte new file mode 100644 index 0000000..501d8c7 --- /dev/null +++ b/src/lib/ui/components/ProgressMeter.svelte @@ -0,0 +1,58 @@ + + +
+ {#if progress !== null} + + {/if} +
+ + diff --git a/src/lib/ui/components/Separator.svelte b/src/lib/ui/components/Separator.svelte new file mode 100644 index 0000000..f2ca968 --- /dev/null +++ b/src/lib/ui/components/Separator.svelte @@ -0,0 +1,39 @@ + + + + + diff --git a/src/lib/ui/components/ServiceGroupPanel.svelte b/src/lib/ui/components/ServiceGroupPanel.svelte new file mode 100644 index 0000000..56ccdbb --- /dev/null +++ b/src/lib/ui/components/ServiceGroupPanel.svelte @@ -0,0 +1,8 @@ + + + diff --git a/src/lib/ui/components/Sparkline.svelte b/src/lib/ui/components/Sparkline.svelte new file mode 100644 index 0000000..78b96dd --- /dev/null +++ b/src/lib/ui/components/Sparkline.svelte @@ -0,0 +1,58 @@ + + + + + + + diff --git a/src/lib/ui/components/SystemState.svelte b/src/lib/ui/components/SystemState.svelte new file mode 100644 index 0000000..4395618 --- /dev/null +++ b/src/lib/ui/components/SystemState.svelte @@ -0,0 +1,67 @@ + + +
+ +
+

{title}

+ {#if detail} +

{detail}

+ {/if} +
+
+ + diff --git a/src/lib/ui/components/TelemetryStrip.svelte b/src/lib/ui/components/TelemetryStrip.svelte new file mode 100644 index 0000000..a8beee3 --- /dev/null +++ b/src/lib/ui/components/TelemetryStrip.svelte @@ -0,0 +1,8 @@ + + + diff --git a/src/lib/ui/components/WeatherModule.svelte b/src/lib/ui/components/WeatherModule.svelte new file mode 100644 index 0000000..295f195 --- /dev/null +++ b/src/lib/ui/components/WeatherModule.svelte @@ -0,0 +1,8 @@ + + + diff --git a/src/lib/ui/index.ts b/src/lib/ui/index.ts index df2a02c..aefd856 100644 --- a/src/lib/ui/index.ts +++ b/src/lib/ui/index.ts @@ -1,14 +1,27 @@ +export { default as Badge } from "./components/Badge.svelte"; +export { default as Button } from "./components/Button.svelte"; +export { default as DashboardHeader } from "./components/DashboardHeader.svelte"; export { default as DashboardFrame } from "./components/DashboardFrame.svelte"; export { default as FooterCell } from "./components/FooterCell.svelte"; +export { default as FooterStatusCell } from "./components/FooterStatusCell.svelte"; +export { default as GridFrame } from "./components/GridFrame.svelte"; export { default as IconGlyph } from "./components/IconGlyph.svelte"; +export { default as IconButton } from "./components/IconButton.svelte"; export { default as ModuleCard } from "./components/ModuleCard.svelte"; export { default as Panel } from "./components/Panel.svelte"; +export { default as ProgressMeter } from "./components/ProgressMeter.svelte"; +export { default as Separator } from "./components/Separator.svelte"; +export { default as ServiceGroupPanel } from "./components/ServiceGroupPanel.svelte"; export { default as ServicePanel } from "./components/ServicePanel.svelte"; export { default as ServiceRow } from "./components/ServiceRow.svelte"; +export { default as Sparkline } from "./components/Sparkline.svelte"; export { default as StatusBadge } from "./components/StatusBadge.svelte"; export { default as StatusStrip } from "./components/StatusStrip.svelte"; +export { default as SystemState } from "./components/SystemState.svelte"; export { default as TelemetryCard } from "./components/TelemetryCard.svelte"; export { default as TelemetryGrid } from "./components/TelemetryGrid.svelte"; +export { default as TelemetryStrip } from "./components/TelemetryStrip.svelte"; +export { default as WeatherModule } from "./components/WeatherModule.svelte"; export { dashboardPreviewFixtures } from "./fixtures"; export type { UiDashboardPreview, diff --git a/src/lib/ui/stories/Badge.stories.svelte b/src/lib/ui/stories/Badge.stories.svelte new file mode 100644 index 0000000..ac557ff --- /dev/null +++ b/src/lib/ui/stories/Badge.stories.svelte @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/src/lib/ui/stories/Button.stories.svelte b/src/lib/ui/stories/Button.stories.svelte new file mode 100644 index 0000000..7c34c3e --- /dev/null +++ b/src/lib/ui/stories/Button.stories.svelte @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/src/lib/ui/stories/DashboardFrame.stories.svelte b/src/lib/ui/stories/DashboardFrame.stories.svelte new file mode 100644 index 0000000..c31627b --- /dev/null +++ b/src/lib/ui/stories/DashboardFrame.stories.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/src/lib/ui/stories/DashboardHeader.stories.svelte b/src/lib/ui/stories/DashboardHeader.stories.svelte new file mode 100644 index 0000000..10d2133 --- /dev/null +++ b/src/lib/ui/stories/DashboardHeader.stories.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/src/lib/ui/stories/DashboardOnePager.stories.svelte b/src/lib/ui/stories/DashboardOnePager.stories.svelte new file mode 100644 index 0000000..8af2c5e --- /dev/null +++ b/src/lib/ui/stories/DashboardOnePager.stories.svelte @@ -0,0 +1,18 @@ + + + + +
+ +
+
diff --git a/src/lib/ui/stories/FooterCell.stories.svelte b/src/lib/ui/stories/FooterCell.stories.svelte new file mode 100644 index 0000000..cb4a08e --- /dev/null +++ b/src/lib/ui/stories/FooterCell.stories.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/lib/ui/stories/FooterStatusCell.stories.svelte b/src/lib/ui/stories/FooterStatusCell.stories.svelte new file mode 100644 index 0000000..578cb11 --- /dev/null +++ b/src/lib/ui/stories/FooterStatusCell.stories.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/lib/ui/stories/GridFrame.stories.svelte b/src/lib/ui/stories/GridFrame.stories.svelte new file mode 100644 index 0000000..d8be595 --- /dev/null +++ b/src/lib/ui/stories/GridFrame.stories.svelte @@ -0,0 +1,37 @@ + + + + + + + + + + + +
+ + + + + +
+
+ +
+ + + + +
+
diff --git a/src/lib/ui/stories/IconButton.stories.svelte b/src/lib/ui/stories/IconButton.stories.svelte new file mode 100644 index 0000000..a303642 --- /dev/null +++ b/src/lib/ui/stories/IconButton.stories.svelte @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/src/lib/ui/stories/IconGlyph.stories.svelte b/src/lib/ui/stories/IconGlyph.stories.svelte new file mode 100644 index 0000000..043e7b3 --- /dev/null +++ b/src/lib/ui/stories/IconGlyph.stories.svelte @@ -0,0 +1,21 @@ + + + + + + + + +
+ +
+
diff --git a/src/lib/ui/stories/ModuleCard.stories.svelte b/src/lib/ui/stories/ModuleCard.stories.svelte new file mode 100644 index 0000000..890a3ca --- /dev/null +++ b/src/lib/ui/stories/ModuleCard.stories.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/src/lib/ui/stories/Panel.stories.svelte b/src/lib/ui/stories/Panel.stories.svelte new file mode 100644 index 0000000..c3d3a80 --- /dev/null +++ b/src/lib/ui/stories/Panel.stories.svelte @@ -0,0 +1,42 @@ + + + + +

Reusable panel content.

+
+
+ + +

Compact repeated content.

+
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/src/lib/ui/stories/ProgressMeter.stories.svelte b/src/lib/ui/stories/ProgressMeter.stories.svelte new file mode 100644 index 0000000..861009f --- /dev/null +++ b/src/lib/ui/stories/ProgressMeter.stories.svelte @@ -0,0 +1,16 @@ + + + + + + + diff --git a/src/lib/ui/stories/Separator.stories.svelte b/src/lib/ui/stories/Separator.stories.svelte new file mode 100644 index 0000000..3f2f6e3 --- /dev/null +++ b/src/lib/ui/stories/Separator.stories.svelte @@ -0,0 +1,21 @@ + + + + + + +

Generic section one

+ +

Generic section two

+
+
diff --git a/src/lib/ui/stories/ServiceGroupPanel.stories.svelte b/src/lib/ui/stories/ServiceGroupPanel.stories.svelte new file mode 100644 index 0000000..51c48bd --- /dev/null +++ b/src/lib/ui/stories/ServiceGroupPanel.stories.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/lib/ui/stories/ServicePanel.stories.svelte b/src/lib/ui/stories/ServicePanel.stories.svelte new file mode 100644 index 0000000..059b63b --- /dev/null +++ b/src/lib/ui/stories/ServicePanel.stories.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/lib/ui/stories/ServiceRow.stories.svelte b/src/lib/ui/stories/ServiceRow.stories.svelte new file mode 100644 index 0000000..1c386f3 --- /dev/null +++ b/src/lib/ui/stories/ServiceRow.stories.svelte @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/src/lib/ui/stories/Sparkline.stories.svelte b/src/lib/ui/stories/Sparkline.stories.svelte new file mode 100644 index 0000000..62744bd --- /dev/null +++ b/src/lib/ui/stories/Sparkline.stories.svelte @@ -0,0 +1,16 @@ + + + + + + + diff --git a/src/lib/ui/stories/StatusBadge.stories.svelte b/src/lib/ui/stories/StatusBadge.stories.svelte new file mode 100644 index 0000000..1429ceb --- /dev/null +++ b/src/lib/ui/stories/StatusBadge.stories.svelte @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/src/lib/ui/stories/StatusStrip.stories.svelte b/src/lib/ui/stories/StatusStrip.stories.svelte new file mode 100644 index 0000000..7b6d22d --- /dev/null +++ b/src/lib/ui/stories/StatusStrip.stories.svelte @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/src/lib/ui/stories/SystemState.stories.svelte b/src/lib/ui/stories/SystemState.stories.svelte new file mode 100644 index 0000000..0e28282 --- /dev/null +++ b/src/lib/ui/stories/SystemState.stories.svelte @@ -0,0 +1,16 @@ + + + + + + + diff --git a/src/lib/ui/stories/TelemetryCard.stories.svelte b/src/lib/ui/stories/TelemetryCard.stories.svelte new file mode 100644 index 0000000..004f47b --- /dev/null +++ b/src/lib/ui/stories/TelemetryCard.stories.svelte @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/src/lib/ui/stories/TelemetryGrid.stories.svelte b/src/lib/ui/stories/TelemetryGrid.stories.svelte new file mode 100644 index 0000000..8054cc9 --- /dev/null +++ b/src/lib/ui/stories/TelemetryGrid.stories.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/src/lib/ui/stories/TelemetryStrip.stories.svelte b/src/lib/ui/stories/TelemetryStrip.stories.svelte new file mode 100644 index 0000000..5eb01b5 --- /dev/null +++ b/src/lib/ui/stories/TelemetryStrip.stories.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/src/lib/ui/stories/WeatherModule.stories.svelte b/src/lib/ui/stories/WeatherModule.stories.svelte new file mode 100644 index 0000000..de86e73 --- /dev/null +++ b/src/lib/ui/stories/WeatherModule.stories.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/lib/ui/stories/story-data.ts b/src/lib/ui/stories/story-data.ts new file mode 100644 index 0000000..4085dac --- /dev/null +++ b/src/lib/ui/stories/story-data.ts @@ -0,0 +1,258 @@ +import { dashboardPreviewFixtures } from "../fixtures"; +import type { + UiDashboardPreview, + UiModuleBlock, + UiServiceGroup, + UiServiceRow, + UiSeverity, + UiStatusItem, + UiTelemetryCard, +} from "../types"; + +export const primaryDashboard = dashboardPreviewFixtures.primary; +export const secondaryDashboard = dashboardPreviewFixtures.secondary; + +export const emptyDashboard: UiDashboardPreview = { + eyebrow: "Preview Surface", + title: "Empty Console", + subtitle: "No records available", + telemetry: [], + modules: [], + serviceGroups: [], + statusItems: [], +}; + +export const moduleBlocks: Record = { + compact: { + id: "module-compact", + title: "Local Node", + value: "21.4 C", + detail: "static sample", + icon: "mdi:weather-partly-cloudy", + severity: "ok", + }, + unavailable: { + id: "module-unavailable", + title: "External Signal", + value: "n/a", + detail: "fallback value", + icon: "mdi:cloud-off-outline", + severity: "unavailable", + }, + long: { + id: "module-long", + title: "Regional Aggregate Signal", + value: "manual review queued", + detail: "long generic label wraps without layout shift", + icon: "mdi:map-marker-radius-outline", + severity: "warning", + }, +}; + +export const telemetryCards: Record = { + percent: { + id: "telemetry-percent", + label: "Capacity Used", + value: { kind: "percent", value: 82 }, + progress: 82, + icon: "mdi:gauge", + severity: "ok", + detail: "static sample", + sparkline: [24, 36, 28, 44, 52, 82], + }, + bytes: { + id: "telemetry-bytes", + label: "Data Volume", + value: { kind: "bytes", value: 982451653, precision: 1 }, + icon: "mdi:database", + severity: "neutral", + detail: "static sample", + sparkline: [8, 12, 20, 18, 24, 29], + }, + temperature: { + id: "telemetry-temperature", + label: "Node Temperature", + value: { kind: "temperature", value: 21.4, precision: 1 }, + icon: "mdi:thermometer", + severity: "neutral", + detail: "static sample", + sparkline: [18, 20, 19, 21, 20, 21.4], + }, + latency: { + id: "telemetry-latency", + label: "Median Latency", + value: { kind: "latency", value: 87 }, + icon: "mdi:timer-outline", + severity: "warning", + detail: "static sample", + sparkline: [45, 51, 72, 63, 80, 87], + }, + unavailable: { + id: "telemetry-unavailable", + label: "Remote Signal", + value: { kind: "text", value: "n/a" }, + icon: "mdi:cloud-off-outline", + severity: "unavailable", + detail: "fallback value", + }, + stale: { + id: "telemetry-stale", + label: "Sync Age", + value: { kind: "text", value: "stale" }, + icon: "mdi:clock-alert-outline", + severity: "stale", + detail: "cached sample", + sparkline: [22, 22, 22, 22, 22, 22], + }, + danger: { + id: "telemetry-danger", + label: "Error Budget", + value: { kind: "percent", value: 7 }, + progress: 7, + icon: "mdi:alert-octagon-outline", + severity: "danger", + detail: "threshold breached", + sparkline: [56, 41, 34, 20, 13, 7], + }, + loading: { + id: "telemetry-loading", + label: "Pending Refresh", + value: { kind: "text", value: "sync" }, + icon: "mdi:progress-clock", + severity: "loading", + detail: "waiting for update", + }, + long: { + id: "telemetry-long", + label: "Very Long Generic Source Label That Must Wrap", + value: { kind: "percent", value: 63 }, + progress: 63, + icon: "mdi:chart-box-outline", + severity: "neutral", + detail: "long source label", + sparkline: [14, 20, 28, 31, 49, 63], + }, +}; + +export const serviceRows: Record = { + normal: row("service-normal", "Ingest", "Event intake pipeline", "mdi:arrow-collapse-down", "ok", "1.252 ms", { + href: "#ingest", + label: "Open ingest", + }), + down: row("service-down", "Replica", "Replica traffic cell", "mdi:access-point-off", "danger", "down"), + degraded: row("service-degraded", "Scheduler", "Timed job coordinator", "mdi:calendar-clock", "warning", "1.487 ms"), + stale: row("service-stale", "Archive", "Cold storage transfer", "mdi:archive-clock", "stale", "paused"), + noLink: row("service-no-link", "Policy", "Rules evaluation", "mdi:shield-check", "neutral", "manual"), + long: row( + "service-long", + "Long Generic Service Name That Wraps Cleanly", + "Long generic service description with enough detail to exercise wrapping in constrained panels", + "mdi:text-box-search-outline", + "warning", + "review queued", + ), +}; + +export const serviceGroups: Record = { + short: { + id: "group-short", + title: "Short List", + services: [serviceRows.normal, serviceRows.degraded], + }, + long: { + id: "group-long", + title: "Long List", + services: [ + serviceRows.normal, + serviceRows.degraded, + serviceRows.stale, + serviceRows.down, + serviceRows.noLink, + serviceRows.long, + ], + }, + empty: { + id: "group-empty", + title: "Empty Group", + services: [], + }, + mixed: { + id: "group-mixed", + title: "Mixed Statuses", + services: [serviceRows.normal, serviceRows.down, serviceRows.degraded, serviceRows.stale], + summary: [ + { id: "summary-ok", label: "Ok", value: "3", severity: "ok" }, + { id: "summary-warn", label: "Warn", value: "1", severity: "warning" }, + { id: "summary-down", label: "Down", value: "1", severity: "danger" }, + ], + }, +}; + +export const statusItems: Record = { + ok: { id: "status-ok", label: "System", value: "Operational", severity: "ok" }, + degraded: { id: "status-degraded", label: "Routing", value: "Degraded", severity: "warning" }, + incident: { id: "status-incident", label: "Incident", value: "Active", severity: "danger" }, + syncing: { id: "status-syncing", label: "Refresh", value: "Syncing", severity: "loading" }, + stale: { id: "status-stale", label: "Last Sync", value: "18 minutes ago", severity: "stale" }, + action: { + id: "status-action", + label: "Action", + value: "Inspect", + severity: "neutral", + link: { href: "#inspect", label: "Inspect status" }, + }, + long: { + id: "status-long", + label: "Long Generic Status Label", + value: "long generic status value wraps", + severity: "neutral", + }, +}; + +export const eightTelemetryCards = [ + telemetryCards.percent, + telemetryCards.bytes, + telemetryCards.temperature, + telemetryCards.latency, + telemetryCards.unavailable, + telemetryCards.stale, + telemetryCards.danger, + telemetryCards.loading, +]; + +export const sixteenTelemetryCards = Array.from({ length: 16 }, (_, index) => { + const card = eightTelemetryCards[index % eightTelemetryCards.length]; + return { + ...card, + id: `${card.id}-${index}`, + label: `${card.label} ${index + 1}`, + }; +}); + +export const mixedStatusStrip = [ + statusItems.ok, + statusItems.degraded, + statusItems.incident, + statusItems.syncing, + statusItems.stale, +]; + +export const fullCompositionDashboard: UiDashboardPreview = { + ...primaryDashboard, + telemetry: eightTelemetryCards, + modules: [moduleBlocks.compact, moduleBlocks.unavailable], + serviceGroups: [serviceGroups.mixed, serviceGroups.short, serviceGroups.long], + statusItems: mixedStatusStrip, +}; + +function row( + id: string, + label: string, + description: string, + icon: string, + severity: UiSeverity, + detail: string, + link?: UiServiceRow["link"], +): UiServiceRow { + return { id, label, description, icon, severity, detail, link }; +} diff --git a/src/lib/ui/storybook.test.ts b/src/lib/ui/storybook.test.ts new file mode 100644 index 0000000..779cd99 --- /dev/null +++ b/src/lib/ui/storybook.test.ts @@ -0,0 +1,77 @@ +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; + +const root = process.cwd(); +const storiesDir = join(root, "src/lib/ui/stories"); + +const requiredStoryFiles = [ + "Badge.stories.svelte", + "Button.stories.svelte", + "DashboardFrame.stories.svelte", + "DashboardHeader.stories.svelte", + "DashboardOnePager.stories.svelte", + "FooterCell.stories.svelte", + "FooterStatusCell.stories.svelte", + "GridFrame.stories.svelte", + "IconButton.stories.svelte", + "IconGlyph.stories.svelte", + "ModuleCard.stories.svelte", + "Panel.stories.svelte", + "ProgressMeter.stories.svelte", + "Separator.stories.svelte", + "ServiceGroupPanel.stories.svelte", + "ServicePanel.stories.svelte", + "ServiceRow.stories.svelte", + "Sparkline.stories.svelte", + "StatusBadge.stories.svelte", + "StatusStrip.stories.svelte", + "SystemState.stories.svelte", + "TelemetryCard.stories.svelte", + "TelemetryGrid.stories.svelte", + "TelemetryStrip.stories.svelte", + "WeatherModule.stories.svelte", +] as const; + +const forbiddenStoryContent = [ + "dimension lab", + "dimensionlab", + "vince", + "homepage", +] as const; + +describe("Storybook inventory", () => { + test("exposes scripts for local and static Storybook review", () => { + const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { + scripts?: Record; + }; + + expect(packageJson.scripts?.storybook).toBeTypeOf("string"); + expect(packageJson.scripts?.storybook).toContain("storybook dev"); + expect(packageJson.scripts?.["build-storybook"]).toBe("storybook build"); + }); + + test("has a story for every reusable dashboard UI component", () => { + for (const filename of requiredStoryFiles) { + expect(existsSync(join(storiesDir, filename)), `${filename} is missing`).toBe(true); + } + }); + + test("keeps Storybook fixtures generic and content-free", () => { + const storyText = readdirSync(storiesDir) + .filter((filename) => filename.endsWith(".svelte") || filename.endsWith(".ts")) + .map((filename) => readFileSync(join(storiesDir, filename), "utf8").toLowerCase()) + .join("\n"); + + for (const forbidden of forbiddenStoryContent) { + expect(storyText).not.toContain(forbidden); + } + }); + + test("does not add deferred form/navigation primitives", () => { + for (const component of ["Input", "ToggleGroup", "ScrollArea"]) { + expect(existsSync(join(root, `src/lib/ui/components/${component}.svelte`))).toBe(false); + expect(existsSync(join(storiesDir, `${component}.stories.svelte`))).toBe(false); + } + }); +}); From 17c9d24e316e9daf3ef2c95a67f9d0b23da51921 Mon Sep 17 00:00:00 2001 From: vince Date: Thu, 18 Jun 2026 19:01:53 +0200 Subject: [PATCH 08/90] fix: address Storybook review findings --- .storybook/main.ts | 2 +- src/lib/ui/components/Button.svelte | 25 +++++++++------ src/lib/ui/components/IconButton.svelte | 27 +++++++++++----- src/lib/ui/components/render.test.ts | 29 +++++++++++++++++ src/lib/ui/stories/FocusPreview.svelte | 33 ++++++++++++++++++++ src/lib/ui/stories/IconButton.stories.svelte | 7 ++++- src/lib/ui/stories/ServiceRow.stories.svelte | 7 ++++- src/lib/ui/storybook.test.ts | 11 +++++++ 8 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 src/lib/ui/stories/FocusPreview.svelte diff --git a/.storybook/main.ts b/.storybook/main.ts index 89968a2..3dd8a72 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,7 +1,7 @@ import type { StorybookConfig } from "@storybook/sveltekit"; const config: StorybookConfig = { - stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|ts|svelte)"], + stories: ["../src/**/*.stories.@(js|ts|svelte)"], addons: [ "@storybook/addon-svelte-csf", "@storybook/addon-a11y", diff --git a/src/lib/ui/components/Button.svelte b/src/lib/ui/components/Button.svelte index f35817a..fa26d5f 100644 --- a/src/lib/ui/components/Button.svelte +++ b/src/lib/ui/components/Button.svelte @@ -1,6 +1,16 @@ diff --git a/src/lib/ui/components/render.test.ts b/src/lib/ui/components/render.test.ts index dd26b81..bf1e98f 100644 --- a/src/lib/ui/components/render.test.ts +++ b/src/lib/ui/components/render.test.ts @@ -1,7 +1,9 @@ import { render } from "svelte/server"; import { describe, expect, test } from "vitest"; +import Button from "./Button.svelte"; import DashboardFrame from "./DashboardFrame.svelte"; import FooterCell from "./FooterCell.svelte"; +import IconButton from "./IconButton.svelte"; import ServiceRow from "./ServiceRow.svelte"; import TelemetryCard from "./TelemetryCard.svelte"; import { dashboardPreviewFixtures } from "../fixtures"; @@ -94,4 +96,31 @@ describe("dashboard UI components", () => { expect(withoutProgress.body).not.toContain("telemetry-card__bar"); expect(withProgress.body).toContain("--metric-progress: 42%"); }); + + test("base button controls forward native attributes", () => { + const button = render(Button, { + props: { + label: "Refresh", + id: "refresh-action", + class: "custom-action", + "aria-controls": "refresh-target", + }, + }); + const iconButton = render(IconButton, { + props: { + icon: "mdi:refresh", + label: "Refresh status", + id: "refresh-icon-action", + class: "custom-icon-action", + "aria-expanded": "false", + }, + }); + + expect(button.body).toContain("id=\"refresh-action\""); + expect(button.body).toContain("class=\"ui-button custom-action "); + expect(button.body).toContain("aria-controls=\"refresh-target\""); + expect(iconButton.body).toContain("id=\"refresh-icon-action\""); + expect(iconButton.body).toContain("class=\"icon-button custom-icon-action "); + expect(iconButton.body).toContain("aria-expanded=\"false\""); + }); }); diff --git a/src/lib/ui/stories/FocusPreview.svelte b/src/lib/ui/stories/FocusPreview.svelte new file mode 100644 index 0000000..514691f --- /dev/null +++ b/src/lib/ui/stories/FocusPreview.svelte @@ -0,0 +1,33 @@ + + +
+ {@render children?.()} +
+ + diff --git a/src/lib/ui/stories/IconButton.stories.svelte b/src/lib/ui/stories/IconButton.stories.svelte index a303642..faa05b6 100644 --- a/src/lib/ui/stories/IconButton.stories.svelte +++ b/src/lib/ui/stories/IconButton.stories.svelte @@ -1,6 +1,7 @@ -