diff --git a/.gitignore b/.gitignore index 54142a0..a9d9e0e 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store -v0-docs \ No newline at end of file +crystal \ No newline at end of file diff --git a/.ignore b/.ignore index 2433681..6eefca4 100644 --- a/.ignore +++ b/.ignore @@ -1,5 +1,5 @@ # Override .gitignore exclusions for AI editors/indexers -# This makes v0-docs accessible to AI coding assistants +# This makes crystal accessible to AI coding assistants # even though it's excluded from Git tracking -!v0-docs +!crystal diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e40375e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "svelte.plugin.svelte.useNewTransformation": true, + "svelte.enable-ts-plugin": true, + "files.associations": { + "svelte.config.js": "javascript" + }, + "files.exclude": { + "**/.svelte-kit": true + }, + "[svelte]": { + "editor.defaultFormatter": "svelte.svelte-vscode" + } +} \ No newline at end of file diff --git a/bun.lock b/bun.lock index ba7abe4..191c8dc 100644 --- a/bun.lock +++ b/bun.lock @@ -50,6 +50,36 @@ "hono": "^4.11.4", }, }, + "packages/ui": { + "name": "@say2/ui", + "version": "0.0.1", + "dependencies": { + "@tanstack/svelte-virtual": "^3.13.18", + "bits-ui": "^2.15.4", + "focus-trap": "^7.8.0", + "lucide-svelte": "^0.562.0", + }, + "devDependencies": { + "@happy-dom/global-registrator": "^20.3.4", + "@rollup/rollup-darwin-arm64": "^4.55.2", + "@storybook/addon-essentials": "8.6.15", + "@storybook/svelte": "8.6.15", + "@storybook/svelte-vite": "8.6.15", + "@sveltejs/package": "^2.5.7", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@testing-library/svelte": "^5.3.1", + "@testing-library/user-event": "^14.6.1", + "@types/bun": "latest", + "storybook": "8.6.15", + "svelte": "^5.47.1", + "svelte-check": "^4.3.5", + "typescript": "^5.9.3", + "vite": "^7.3.1", + }, + "peerDependencies": { + "svelte": "^5.0.0", + }, + }, }, "packages": { "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], @@ -110,6 +140,8 @@ "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], "@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], @@ -134,8 +166,74 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@1.4.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.4.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.3.4", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.3.4" } }, "sha512-h8spgNO3ykyYAjcnazUfo9i0877rpn6qZtm4ixtA6PR3GNkcxSlIwbWzO4hCVsCCeRkJMRPGVCNue46WpQfhKg=="], + "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.9.5", "", { "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", "minimatch": "^3.0.4" } }, "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw=="], + + "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@1.2.1", "", {}, "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="], + "@inquirer/ansi": ["@inquirer/ansi@2.0.3", "", {}, "sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw=="], "@inquirer/checkbox": ["@inquirer/checkbox@5.0.4", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/core": "^11.1.1", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-DrAMU3YBGMUAp6ArwTIp/25CNDtDbxk7UjIrrtM25JVVrlVYlVzHh5HR1BDFu9JMyUoZ4ZanzeaHqNDttf3gVg=="], @@ -168,6 +266,8 @@ "@inquirer/type": ["@inquirer/type@4.0.3", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw=="], + "@internationalized/date": ["@internationalized/date@3.10.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA=="], + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], @@ -182,18 +282,120 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.2", "", { "os": "android", "cpu": "arm" }, "sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.2", "", { "os": "android", "cpu": "arm64" }, "sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.2", "", { "os": "linux", "cpu": "arm" }, "sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.2", "", { "os": "linux", "cpu": "arm" }, "sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.2", "", { "os": "linux", "cpu": "none" }, "sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.2", "", { "os": "linux", "cpu": "none" }, "sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.2", "", { "os": "linux", "cpu": "none" }, "sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.2", "", { "os": "linux", "cpu": "none" }, "sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.2", "", { "os": "linux", "cpu": "x64" }, "sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.2", "", { "os": "win32", "cpu": "x64" }, "sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.2", "", { "os": "win32", "cpu": "x64" }, "sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q=="], + "@say2/core": ["@say2/core@workspace:packages/core"], "@say2/mcp": ["@say2/mcp@workspace:packages/mcp"], "@say2/server": ["@say2/server@workspace:packages/server"], + "@say2/ui": ["@say2/ui@workspace:packages/ui"], + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + "@storybook/addon-actions": ["@storybook/addon-actions@8.6.15", "", { "dependencies": { "@storybook/global": "^5.0.0", "@types/uuid": "^9.0.1", "dequal": "^2.0.2", "polished": "^4.2.2", "uuid": "^9.0.0" }, "peerDependencies": { "storybook": "^8.6.15" } }, "sha512-zc600PBJqP9hCyRY5escKgKf6Zt9kdNZfm+Jwb46k5/NMSO4tNVeOPGBFxW9kSsIYk8j55sNske+Yh60G+8bcw=="], + + "@storybook/addon-backgrounds": ["@storybook/addon-backgrounds@8.6.15", "", { "dependencies": { "@storybook/global": "^5.0.0", "memoizerific": "^1.11.3", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^8.6.15" } }, "sha512-W36uEzMWPO/K3+8vV1R/GozdaFrIix0qqmxX0qoAT6/o4+zqHiloZkTF+2iuUTx/VmuztLcAoSaPDh8UPy3Q+g=="], + + "@storybook/addon-controls": ["@storybook/addon-controls@8.6.15", "", { "dependencies": { "@storybook/global": "^5.0.0", "dequal": "^2.0.2", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^8.6.15" } }, "sha512-CgV8WqGxQrqSKs1a/Y1v4mrsBJXGFmO5u4kvdhPbftRVfln11W4Hvc1SFmgXwGvmcwekAKH79Uwwkjhj3l6gzA=="], + + "@storybook/addon-docs": ["@storybook/addon-docs@8.6.15", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/blocks": "8.6.15", "@storybook/csf-plugin": "8.6.15", "@storybook/react-dom-shim": "8.6.15", "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", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^8.6.15" } }, "sha512-Nm5LlxwAmGQRkCUY36FhtCLz21C+5XlydF7/bkBOHsf08p2xR5MNLMSPrIhte/PY7ne9viNUCm1d3d3LiWnkKg=="], + + "@storybook/addon-essentials": ["@storybook/addon-essentials@8.6.15", "", { "dependencies": { "@storybook/addon-actions": "8.6.15", "@storybook/addon-backgrounds": "8.6.15", "@storybook/addon-controls": "8.6.15", "@storybook/addon-docs": "8.6.15", "@storybook/addon-highlight": "8.6.15", "@storybook/addon-measure": "8.6.15", "@storybook/addon-outline": "8.6.15", "@storybook/addon-toolbars": "8.6.15", "@storybook/addon-viewport": "8.6.15", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^8.6.15" } }, "sha512-BIcE/7t5WXDXs4+zycm7MLNPHA2219ImkKO70IH7uxGM4cm7jDuJ5v0crkAvNeeRVsZixT2P2L9EfUfi1cFCQg=="], + + "@storybook/addon-highlight": ["@storybook/addon-highlight@8.6.15", "", { "dependencies": { "@storybook/global": "^5.0.0" }, "peerDependencies": { "storybook": "^8.6.15" } }, "sha512-lOu44QTVw5nR8kzag0ukxWnLq48oy2MqMUDuMVFQWPBKX8ayhmgl2OiEcvAOVNsieTHrr2W4CkP7FFvF4D0vlg=="], + + "@storybook/addon-measure": ["@storybook/addon-measure@8.6.15", "", { "dependencies": { "@storybook/global": "^5.0.0", "tiny-invariant": "^1.3.1" }, "peerDependencies": { "storybook": "^8.6.15" } }, "sha512-F78fJlmuXMulTphFp9Iqx7I1GsbmNLboChnW/VqR6nRZx5o9cdGjc8IaEyXVFXZ7k1pnSvdaP5ndFmzkcPxQdg=="], + + "@storybook/addon-outline": ["@storybook/addon-outline@8.6.15", "", { "dependencies": { "@storybook/global": "^5.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^8.6.15" } }, "sha512-rpGRLajsjBdpbggPmdNZbftF68zQwsYLosu7YiUSBaR4dm+gQ+7m5nLLI/MjZDHbt2nJRW94yXpn7dUw2CDF6g=="], + + "@storybook/addon-toolbars": ["@storybook/addon-toolbars@8.6.15", "", { "peerDependencies": { "storybook": "^8.6.15" } }, "sha512-NfHAbOOu5qI9SQq6jJr2VfinaZpHrmz3bavBeUppxCxM+zfPuNudK8MlMOOuyPBPAoUqcDSoKZgNfCkOBQcyGg=="], + + "@storybook/addon-viewport": ["@storybook/addon-viewport@8.6.15", "", { "dependencies": { "memoizerific": "^1.11.3" }, "peerDependencies": { "storybook": "^8.6.15" } }, "sha512-ylTK4sehAeVTwcYMZyisyP3xX+m43NjJrQHKc3DAII3Z3RFqTv9l6CUMogM2/8mysTzoo8xYVtQB6hX7zB8Dew=="], + + "@storybook/blocks": ["@storybook/blocks@8.6.15", "", { "dependencies": { "@storybook/icons": "^1.2.12", "ts-dedent": "^2.0.0" }, "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", "storybook": "^8.6.15" }, "optionalPeers": ["react", "react-dom"] }, "sha512-nc5jQkvPo0EirteHsrmcx9on/0lGQ8F4lUNky7kN2I5WM8Frr3cPTeRoAvzjUkOwrqt/vm3g+T4zSbmDq/OEDA=="], + + "@storybook/builder-vite": ["@storybook/builder-vite@8.6.15", "", { "dependencies": { "@storybook/csf-plugin": "8.6.15", "browser-assert": "^1.2.1", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^8.6.15", "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" } }, "sha512-9Y05/ndZE6/eI7ZIUCD/QtH2htRIUs9j1gxE6oW0zRo9TJO1iqxfLNwgzd59KEkId7gdZxPei0l+LGTUGXYKRg=="], + + "@storybook/components": ["@storybook/components@8.6.15", "", { "peerDependencies": { "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "sha512-+9GVKXPEW8Kl9zvNSTm9+VrJtx/puMZiO7gxCML63nK4aTWJXHQr4t9YUoGammSBM3AV1JglsKm6dBgJEeCoiA=="], + + "@storybook/core": ["@storybook/core@8.6.15", "", { "dependencies": { "@storybook/theming": "8.6.15", "better-opn": "^3.0.2", "browser-assert": "^1.2.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", "esbuild-register": "^3.5.0", "jsdoc-type-pratt-parser": "^4.0.0", "process": "^0.11.10", "recast": "^0.23.5", "semver": "^7.6.2", "util": "^0.12.5", "ws": "^8.2.3" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"] }, "sha512-VFpKcphNurJpSC4fpUfKL3GTXVoL53oytghGR30QIw5jKWwaT50HVbTyb41BLOUuZjmMhUQA8weiQEew6RX0gw=="], + + "@storybook/csf": ["@storybook/csf@0.1.12", "", { "dependencies": { "type-fest": "^2.19.0" } }, "sha512-9/exVhabisyIVL0VxTCxo01Tdm8wefIXKXfltAPTSr8cbLn5JAxGQ6QV3mjdecLGEOucfoVhAKtJfVHxEK1iqw=="], + + "@storybook/csf-plugin": ["@storybook/csf-plugin@8.6.15", "", { "dependencies": { "unplugin": "^1.3.1" }, "peerDependencies": { "storybook": "^8.6.15" } }, "sha512-ZLz/mtOoE1Jj2lE4pK3U7MmYrv5+lot3mGtwxGb832tcABMc97j9O+reCVxZYc7DeFbBuuEdMT9rBL/O3kXYmw=="], + + "@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="], + + "@storybook/icons": ["@storybook/icons@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" } }, "sha512-hcFZIjW8yQz8O8//2WTIXylm5Xsgc+lW9ISLgUk1xGmptIJQRdlhVIXCpSyLrQaaRiyhQRaVg7l3BD9S216BHw=="], + + "@storybook/manager-api": ["@storybook/manager-api@8.6.15", "", { "peerDependencies": { "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "sha512-ZOFtH821vFcwzECbFYFTKtSVO96Cvwwg45dMh3M/9bZIdN7klsloX7YNKw8OKvwE6XLFLsi2OvsNNcmTW6g88w=="], + + "@storybook/preview-api": ["@storybook/preview-api@8.6.15", "", { "peerDependencies": { "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "sha512-oqsp8f7QekB9RzpDqOXZQcPPRXXd/mTsnZSdAAQB/pBVqUpC9h/y5hgovbYnJ6DWXcpODbMwH+wbJHZu5lvm+w=="], + + "@storybook/react-dom-shim": ["@storybook/react-dom-shim@8.6.15", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^8.6.15" } }, "sha512-m2trBmmd4iom1qwrp1F109zjRDc0cPaHYhDQxZR4Qqdz8pYevYJTlipDbH/K4NVB6Rn687RT29OoOPfJh6vkFA=="], + + "@storybook/svelte": ["@storybook/svelte@8.6.15", "", { "dependencies": { "@storybook/components": "8.6.15", "@storybook/csf": "0.1.12", "@storybook/global": "^5.0.0", "@storybook/manager-api": "8.6.15", "@storybook/preview-api": "8.6.15", "@storybook/theming": "8.6.15", "sveltedoc-parser": "^4.2.1", "ts-dedent": "^2.0.0", "type-fest": "~2.19" }, "peerDependencies": { "storybook": "^8.6.15", "svelte": "^4.0.0 || ^5.0.0" } }, "sha512-Qj1T6m+GR+GHdzISoMa87AlPvW5L1dam2vR1lSdc7q/+Dn2nl1XN2iZCzxg66+oQnkajD2xmY7cFYqMTX9A6TQ=="], + + "@storybook/svelte-vite": ["@storybook/svelte-vite@8.6.15", "", { "dependencies": { "@storybook/builder-vite": "8.6.15", "@storybook/svelte": "8.6.15", "magic-string": "^0.30.0", "svelte-preprocess": "^5.1.1", "svelte2tsx": "^0.7.35", "sveltedoc-parser": "^4.2.1", "ts-dedent": "^2.2.0", "typescript": "^4.9.4 || ^5.0.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "storybook": "^8.6.15", "svelte": "^4.0.0 || ^5.0.0", "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" } }, "sha512-QkoKBgKpd6qeZO6Lt6QsnqI9+ZAEl5aaHg1eFjmjZM4nKZfWeC5O9UDPFcY207RRhBSgtqu1PE3FQVMpNso6IQ=="], + + "@storybook/theming": ["@storybook/theming@8.6.15", "", { "peerDependencies": { "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "sha512-dAbL0XOekyT6XsF49R6Etj3WxQ/LpdJDIswUUeHgVJ6/yd2opZOGbPxnwA3zlmAh1c0tvpPyhSDXxSG79u8e4Q=="], + "@stryker-mutator/api": ["@stryker-mutator/api@9.4.0", "", { "dependencies": { "mutation-testing-metrics": "3.5.1", "mutation-testing-report-schema": "3.5.1", "tslib": "~2.8.0", "typed-inject": "~5.0.0" } }, "sha512-X7jY63wvWAp6ScQT3DO8aaYyCLTl3I7UjKLuRQ9uUKvtMb11wAtMz0ILahAVttV7T/9S2S2epw1zqrqMA7waqQ=="], "@stryker-mutator/core": ["@stryker-mutator/core@9.4.0", "", { "dependencies": { "@inquirer/prompts": "^8.0.0", "@stryker-mutator/api": "9.4.0", "@stryker-mutator/instrumenter": "9.4.0", "@stryker-mutator/util": "9.4.0", "ajv": "~8.17.1", "chalk": "~5.6.0", "commander": "~14.0.0", "diff-match-patch": "1.0.5", "emoji-regex": "~10.6.0", "execa": "~9.6.0", "json-rpc-2.0": "^1.7.0", "lodash.groupby": "~4.6.0", "minimatch": "~10.1.0", "mutation-server-protocol": "~0.4.0", "mutation-testing-elements": "3.6.0", "mutation-testing-metrics": "3.5.1", "mutation-testing-report-schema": "3.5.1", "npm-run-path": "~6.0.0", "progress": "~2.0.3", "rxjs": "~7.8.1", "semver": "^7.6.3", "source-map": "~0.7.4", "tree-kill": "~1.2.2", "tslib": "2.8.1", "typed-inject": "~5.0.0", "typed-rest-client": "~2.1.0" }, "bin": { "stryker": "bin/stryker.js" } }, "sha512-DynQ5vYjfBGpZxPmRA2NIsacU942yAkxW+pNENX47lIj9WIF56oJsI2JuFISlQZ7HIm1UCa/bk9RPalzyxfJMw=="], @@ -204,8 +406,32 @@ "@stryker-mutator/util": ["@stryker-mutator/util@9.4.0", "", {}, "sha512-xtL8hCY/3LvorlaM19bdbTvTL822Bh1S7L4s9z0wO4XvzU9ZXv5GsLeMVnkcOxRWJ1VLWO97U87eM12HZXyzag=="], + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="], + + "@sveltejs/package": ["@sveltejs/package@2.5.7", "", { "dependencies": { "chokidar": "^5.0.0", "kleur": "^4.1.5", "sade": "^1.8.1", "semver": "^7.5.4", "svelte2tsx": "~0.7.33" }, "peerDependencies": { "svelte": "^3.44.0 || ^4.0.0 || ^5.0.0-next.1" }, "bin": { "svelte-package": "svelte-package.js" } }, "sha512-qqD9xa9H7TDiGFrF6rz7AirOR8k15qDK/9i4MIE8te4vWsv5GEogPks61rrZcLy+yWph+aI6pIj2MdoK3YI8AQ=="], + + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="], + + "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="], + + "@swc/helpers": ["@swc/helpers@0.5.18", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="], + + "@tanstack/svelte-virtual": ["@tanstack/svelte-virtual@3.13.18", "", { "dependencies": { "@tanstack/virtual-core": "3.13.18" }, "peerDependencies": { "svelte": "^3.48.0 || ^4.0.0 || ^5.0.0" } }, "sha512-BHh8WkFK58eE9KzLctPQkCkvCj46LnM9tIGkpwo5Unx5YaBPf0uBJBqvSdc2jMwdT8gLXLHFHtCnSujlZP69BA=="], + + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.18", "", {}, "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg=="], + + "@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/svelte": ["@testing-library/svelte@5.3.1", "", { "dependencies": { "@testing-library/dom": "9.x.x || 10.x.x", "@testing-library/svelte-core": "1.0.0" }, "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0", "vite": "*", "vitest": "*" }, "optionalPeers": ["vite", "vitest"] }, "sha512-8Ez7ZOqW5geRf9PF5rkuopODe5RGy3I9XR+kc7zHh26gBiktLaxTfKmhlGaSHYUOTQE7wFsLMN9xCJVCszw47w=="], + + "@testing-library/svelte-core": ["@testing-library/svelte-core@1.0.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0" } }, "sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ=="], + + "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + "@types/accepts": ["@types/accepts@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ=="], + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], @@ -216,6 +442,8 @@ "@types/cookies": ["@types/cookies@0.9.2", "", { "dependencies": { "@types/connect": "*", "@types/express": "*", "@types/keygrip": "*", "@types/node": "*" } }, "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], @@ -230,54 +458,106 @@ "@types/koa-compose": ["@types/koa-compose@3.2.9", "", { "dependencies": { "@types/koa": "*" } }, "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA=="], + "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + "@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="], + "@types/pug": ["@types/pug@2.0.10", "", {}, "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA=="], + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + "@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="], + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], "@types/uuid": ["@types/uuid@11.0.0", "", { "dependencies": { "uuid": "*" } }, "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA=="], + "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "ajv": ["ajv@8.17.1", "", { "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-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "angular-html-parser": ["angular-html-parser@10.1.1", "", {}, "sha512-+xumziB0dBj/ySLRK42LAKyfnf1jPSZC3FmqI/AKygE3aB6ZZDSITZ+iC+1MfrETtjc8Ocs6GZIV6KSSi5przA=="], - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], - "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="], + "better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="], + + "bits-ui": ["bits-ui@2.15.4", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-7H9YUfp03KOk1LVDh8wPYSRPxlZgG/GRWLNSA8QC73/8Z8ytun+DWJhIuibyFyz7A0cP/RANVcB4iDrbY8q+Og=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "browser-assert": ["browser-assert@1.2.1", "", {}, "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ=="], + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], + "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001764", "", {}, "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -292,14 +572,44 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "dedent-js": ["dedent-js@1.0.1", "", {}, "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="], + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], + + "devalue": ["devalue@5.6.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="], + "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="], + "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + + "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + + "dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@3.3.0", "", { "dependencies": { "domelementtype": "^2.0.1" } }, "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA=="], + + "domutils": ["domutils@2.8.0", "", { "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", "domhandler": "^4.2.0" } }, "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], @@ -310,16 +620,52 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es6-promise": ["es6-promise@3.3.1", "", {}, "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg=="], + + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@8.4.1", "", { "dependencies": { "@eslint/eslintrc": "^1.0.5", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.1.0", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.1.0", "espree": "^9.2.0", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^6.0.1", "globals": "^13.6.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.0.4", "natural-compare": "^1.4.0", "optionator": "^0.9.1", "progress": "^2.0.0", "regexpp": "^3.2.0", "semver": "^7.2.1", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-TxU/p7LB1KxQ6+7aztTnO7K0i+h0tDi81YRY9VzB6Id71kNz+fFYnf5HD5UOQmxkzcoa0TlVZf9dpMtUv0GpWg=="], + + "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], + + "eslint-utils": ["eslint-utils@3.0.0", "", { "dependencies": { "eslint-visitor-keys": "^2.0.0" }, "peerDependencies": { "eslint": ">=5" } }, "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "espree": ["espree@9.2.0", "", { "dependencies": { "acorn": "^8.6.0", "acorn-jsx": "^5.3.1", "eslint-visitor-keys": "^3.1.0" } }, "sha512-oP3utRkynpZWF/F2x/HZJ+AGtnIclaR7z1pYPxy7NYM2fSO6LgK/Rkny8anRSPK/VwEA1eqm2squui0T7ZMOBg=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrap": ["esrap@2.2.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], @@ -336,18 +682,42 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "focus-trap": ["focus-trap@7.8.0", "", { "dependencies": { "tabbable": "^6.4.0" } }, "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "functional-red-black-tree": ["functional-red-black-tree@1.0.1", "", {}, "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g=="], + + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], @@ -358,32 +728,80 @@ "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "happy-dom": ["happy-dom@20.3.4", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^4.5.0", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-rfbiwB6OKxZFIFQ7SRnCPB2WL9WhyXsFoTfecYgeCeFSOBxvkWLaXsdv5ehzJrfqwXQmDephAKWLRQoFoJwrew=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], + "htmlparser2-svelte": ["htmlparser2-svelte@4.1.0", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^3.0.0", "domutils": "^2.0.0", "entities": "^2.0.0" } }, "sha512-+4f4RBFz7Rj2Hp0ZbFbXC+Kzbd6S9PgjiuFtdT76VMNgKogrEZy0pG2UrPycPbrZzVEIM5lAT3lAdkSTCHLPjg=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "ignore": ["ignore@4.0.6", "", {}, "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], @@ -392,36 +810,72 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsdoc-type-pratt-parser": ["jsdoc-type-pratt-parser@4.8.0", "", {}, "sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-rpc-2.0": ["json-rpc-2.0@1.7.1", "", {}, "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "koa-compose": ["koa-compose@4.1.0", "", {}, "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + "lodash.groupby": ["lodash.groupby@4.6.0", "", {}, "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "lucide-svelte": ["lucide-svelte@0.562.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-kSJDH/55lf0mun/o4nqWBXOcq0fWYzPeIjbTD97ywoeumAB9kWxtM06gC7oynqjtK3XhAljWSz5RafIzPEYIQA=="], + + "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=="], + + "map-or-similar": ["map-or-similar@1.5.0", "", {}, "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "memoizerific": ["memoizerific@1.11.3", "", { "dependencies": { "map-or-similar": "^1.5.0" } }, "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog=="], + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], "minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "mutation-server-protocol": ["mutation-server-protocol@0.4.0", "", { "dependencies": { "zod": "^4.1.12" } }, "sha512-z22ttDtj0RUZuhVzg0wkBkyvcJoauWq99qJ6O7eO4eEudZAhc3qVh5fBeb0qjRy84XzYiSO6m4I9i1Gx8nQcrw=="], @@ -434,6 +888,10 @@ "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -444,28 +902,54 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + "path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "polished": ["polished@4.3.1", "", { "dependencies": { "@babel/runtime": "^7.17.8" } }, "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "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=="], + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "pure-rand": ["pure-rand@7.0.1", "", {}, "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ=="], "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], @@ -474,20 +958,52 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + + "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], + + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + + "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "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=="], + + "regexpp": ["regexpp@3.2.0", "", {}, "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], + + "rollup": ["rollup@4.55.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.2", "@rollup/rollup-android-arm64": "4.55.2", "@rollup/rollup-darwin-arm64": "4.55.2", "@rollup/rollup-darwin-x64": "4.55.2", "@rollup/rollup-freebsd-arm64": "4.55.2", "@rollup/rollup-freebsd-x64": "4.55.2", "@rollup/rollup-linux-arm-gnueabihf": "4.55.2", "@rollup/rollup-linux-arm-musleabihf": "4.55.2", "@rollup/rollup-linux-arm64-gnu": "4.55.2", "@rollup/rollup-linux-arm64-musl": "4.55.2", "@rollup/rollup-linux-loong64-gnu": "4.55.2", "@rollup/rollup-linux-loong64-musl": "4.55.2", "@rollup/rollup-linux-ppc64-gnu": "4.55.2", "@rollup/rollup-linux-ppc64-musl": "4.55.2", "@rollup/rollup-linux-riscv64-gnu": "4.55.2", "@rollup/rollup-linux-riscv64-musl": "4.55.2", "@rollup/rollup-linux-s390x-gnu": "4.55.2", "@rollup/rollup-linux-x64-gnu": "4.55.2", "@rollup/rollup-linux-x64-musl": "4.55.2", "@rollup/rollup-openbsd-x64": "4.55.2", "@rollup/rollup-openharmony-arm64": "4.55.2", "@rollup/rollup-win32-arm64-msvc": "4.55.2", "@rollup/rollup-win32-ia32-msvc": "4.55.2", "@rollup/rollup-win32-x64-gnu": "4.55.2", "@rollup/rollup-win32-x64-msvc": "4.55.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="], + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sander": ["sander@0.5.1", "", { "dependencies": { "es6-promise": "^3.1.2", "graceful-fs": "^4.1.3", "mkdirp": "^0.5.1", "rimraf": "^2.5.2" } }, "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA=="], + + "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.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -504,24 +1020,64 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "sorcery": ["sorcery@0.11.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.14", "buffer-crc32": "^1.0.0", "minimist": "^1.2.0", "sander": "^0.5.0" }, "bin": { "sorcery": "bin/sorcery" } }, "sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "storybook": ["storybook@8.6.15", "", { "dependencies": { "@storybook/core": "8.6.15" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": { "sb": "./bin/index.cjs", "storybook": "./bin/index.cjs", "getstorybook": "./bin/index.cjs" } }, "sha512-Ob7DMlwWx8s7dMvcQ3xPc02TvUeralb+xX3oaPRk9wY9Hc6M1IBC/7cEoITkSmRS2v38DHubC+mtEKNc1u2gQg=="], + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "svelte": ["svelte@5.47.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-MhSWfWEpG5T57z0Oyfk9D1GhAz/KTZKZZlWtGEsy9zNk2fafpuU7sJQlXNSA8HtvwKxVC9XlDyl5YovXUXjjHA=="], + + "svelte-check": ["svelte-check@4.3.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "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-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="], + + "svelte-preprocess": ["svelte-preprocess@5.1.4", "", { "dependencies": { "@types/pug": "^2.0.6", "detect-indent": "^6.1.0", "magic-string": "^0.30.5", "sorcery": "^0.11.0", "strip-indent": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.10.2", "coffeescript": "^2.5.1", "less": "^3.11.3 || ^4.0.0", "postcss": "^7 || ^8", "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "pug": "^3.0.0", "sass": "^1.26.8", "stylus": "^0.55.0", "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["@babel/core", "coffeescript", "less", "postcss", "postcss-load-config", "pug", "sass", "stylus", "sugarss", "typescript"] }, "sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA=="], + + "svelte-toolbelt": ["svelte-toolbelt@0.10.6", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="], + + "svelte2tsx": ["svelte2tsx@0.7.46", "", { "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" } }, "sha512-S++Vw3w47a8rBuhbz4JK0fcGea8tOoX1boT53Aib8+oUO2EKeOG+geXprJVTDfBlvR+IJdf3jIpR2RGwT6paQA=="], + + "sveltedoc-parser": ["sveltedoc-parser@4.2.1", "", { "dependencies": { "eslint": "8.4.1", "espree": "9.2.0", "htmlparser2-svelte": "4.1.0" } }, "sha512-sWJRa4qOfRdSORSVw9GhfDEwsbsYsegnDzBevUCF6k/Eis/QqCu9lJ6I0+d/E2wOWCjOhlcJ3+jl/Iur+5mmCw=="], + + "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], + + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], "typed-inject": ["typed-inject@5.0.0", "", {}, "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA=="], @@ -538,26 +1094,50 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "unplugin": ["unplugin@1.16.1", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], + "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], + "v8-compile-cache": ["v8-compile-cache@2.4.0", "", {}, "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], + "weapon-regex": ["weapon-regex@1.3.6", "", {}, "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "xstate": ["xstate@5.25.0", "", {}, "sha512-yyWzfhVRoTHNLjLoMmdwZGagAYfmnzpm9gPjlX2MhJZsDojXGqRxODDOi4BsgGRKD46NZRAdcLp6CKOyvQS0Bw=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], + "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], @@ -568,6 +1148,120 @@ "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@eslint/eslintrc/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "@eslint/eslintrc/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + + "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "@humanwhocodes/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "@storybook/addon-actions/@types/uuid": ["@types/uuid@9.0.8", "", {}, "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="], + + "@storybook/addon-actions/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + + "@storybook/core/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=="], + + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + "cross-spawn/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "dom-serializer/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], + + "dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], + + "domutils/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], + + "eslint/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@2.1.0", "", {}, "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw=="], + + "flat-cache/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "globals/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + + "htmlparser2-svelte/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], + + "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "svelte-check/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "@storybook/core/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@storybook/core/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@storybook/core/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@storybook/core/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@storybook/core/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@storybook/core/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@storybook/core/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@storybook/core/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@storybook/core/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@storybook/core/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@storybook/core/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@storybook/core/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@storybook/core/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@storybook/core/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@storybook/core/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@storybook/core/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@storybook/core/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@storybook/core/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@storybook/core/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@storybook/core/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@storybook/core/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@storybook/core/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@storybook/core/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@storybook/core/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@storybook/core/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@storybook/core/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "svelte-check/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], } } diff --git a/bunfig.toml b/bunfig.toml index 7eb791f..2688187 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -28,5 +28,5 @@ coverageIgnorePatterns = [ # Test timeout in milliseconds timeout = 10000 -# Preload test setup (if needed) -# preload = ["./test-setup.ts"] +# Preload test setup (UI package needs happy-dom for DOM environment) +preload = ["./packages/ui/tests/svelte-plugin.ts", "./packages/ui/tests/setup.ts"] diff --git a/package.json b/package.json index d99dcd1..9cb3860 100644 --- a/package.json +++ b/package.json @@ -30,4 +30,4 @@ "@types/bun": "^1.3.6", "fast-check": "^4.5.3" } -} +} \ No newline at end of file diff --git a/packages/ui/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc b/packages/ui/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc new file mode 100644 index 0000000..ebda995 --- /dev/null +++ b/packages/ui/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc @@ -0,0 +1,111 @@ +--- +description: Use Bun instead of Node.js, npm, pnpm, or vite. +globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json" +alwaysApply: false +--- + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; +import { createRoot } from "react-dom/client"; + +// import .css files directly and it works +import './index.css'; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`. diff --git a/packages/ui/.gitignore b/packages/ui/.gitignore new file mode 100644 index 0000000..05b8c8e --- /dev/null +++ b/packages/ui/.gitignore @@ -0,0 +1,37 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz +storybook-static +debug-storybook.log + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store +.svelte-kit diff --git a/packages/ui/.storybook/main.ts b/packages/ui/.storybook/main.ts new file mode 100644 index 0000000..0f6fbda --- /dev/null +++ b/packages/ui/.storybook/main.ts @@ -0,0 +1,15 @@ +import type { StorybookConfig } from "@storybook/svelte-vite"; + +const config: StorybookConfig = { + stories: ["../src/**/*.stories.@(js|ts|svelte)"], + addons: ["@storybook/addon-essentials"], + framework: { + name: "@storybook/svelte-vite", + options: {}, + }, + docs: { + autodocs: "tag", + }, +}; + +export default config; diff --git a/packages/ui/.storybook/preview-head.html b/packages/ui/.storybook/preview-head.html new file mode 100644 index 0000000..f583b99 --- /dev/null +++ b/packages/ui/.storybook/preview-head.html @@ -0,0 +1,8 @@ + diff --git a/packages/ui/.storybook/preview.ts b/packages/ui/.storybook/preview.ts new file mode 100644 index 0000000..998ebbb --- /dev/null +++ b/packages/ui/.storybook/preview.ts @@ -0,0 +1,46 @@ +import type { Preview } from "@storybook/svelte"; + +// Import tokens (includes typography) +import '../src/lib/tokens/index.css'; + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + backgrounds: { + default: "light", + values: [ + { name: "light", value: "#ffffff" }, + { name: "dark", value: "#1a1a1a" }, + ], + }, + }, + decorators: [ + // Ensure fonts are loaded + (Story, context) => { + // Add Google Fonts link if not present + if (typeof document !== 'undefined') { + const linkId = 'google-fonts'; + if (!document.getElementById(linkId)) { + const link = document.createElement('link'); + link.id = linkId; + link.rel = 'stylesheet'; + link.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'; + document.head.appendChild(link); + } + + // Sync data-theme with Storybook background + const bgName = context.globals?.backgrounds?.value; + const theme = bgName === '#1a1a1a' ? 'dark' : 'light'; + document.documentElement.setAttribute('data-theme', theme); + } + return Story(); + } + ] +}; + +export default preview; diff --git a/packages/ui/README.md b/packages/ui/README.md new file mode 100644 index 0000000..cf00d22 --- /dev/null +++ b/packages/ui/README.md @@ -0,0 +1,87 @@ +# @say2/ui + +UI component library for Say2, built with Svelte 5 and TypeScript. + +## Overview + +This package provides a set of primitive and composite UI components using: +- **Svelte 5** (with runes) +- **bits-ui** for headless component primitives +- **TypeScript** for type safety +- **Storybook** for component development and documentation +- **Bun** for testing + +## Installation + +```bash +bun install +``` + +## Development + +### Storybook + +Run Storybook for interactive component development: + +```bash +bun run dev +``` + +This will start Storybook at [http://localhost:6006](http://localhost:6006) where you can: +- Browse all components in isolation +- View component documentation +- Interact with component controls +- Test different component states and variants + +### Build Storybook + +To build a static version of Storybook: + +```bash +bun run build:storybook +``` + +## Testing + +Run all tests: + +```bash +bun run test +``` + +Run tests in watch mode: + +```bash +bun run test:watch +``` + +## Building + +Build the component library for distribution: + +```bash +bun run build +``` + +This generates the package output in the `dist/` directory. + +## Type Checking + +Run Svelte type checking: + +```bash +bun run check +``` + +## Component Structure + +Components are organized into: +- **Primitives** (`src/lib/primitives/`) - Basic UI building blocks + - Button, Badge, Input, Checkbox, Select, etc. +- **Composites** (`src/lib/composites/`) - Complex components built from primitives + +Each component typically includes: +- `*.svelte` - Component implementation +- `*.stories.ts` - Storybook stories +- `*.test.ts` - Unit tests +- `index.ts` - Barrel export diff --git a/packages/ui/bunfig.toml b/packages/ui/bunfig.toml new file mode 100644 index 0000000..1635bf4 --- /dev/null +++ b/packages/ui/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = ["./tests/svelte-plugin.ts", "./tests/setup.ts"] diff --git a/packages/ui/index.ts b/packages/ui/index.ts new file mode 100644 index 0000000..2a5e4b8 --- /dev/null +++ b/packages/ui/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..d444240 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,47 @@ +{ + "name": "@say2/ui", + "version": "0.0.1", + "type": "module", + "svelte": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "svelte": "./dist/index.js" + } + }, + "scripts": { + "dev": "storybook dev -p 6006", + "build": "svelte-package", + "build:storybook": "storybook build", + "test": "bun test", + "test:watch": "bun test --watch", + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "peerDependencies": { + "svelte": "^5.0.0" + }, + "dependencies": { + "@tanstack/svelte-virtual": "^3.13.18", + "bits-ui": "^2.15.4", + "focus-trap": "^7.8.0", + "lucide-svelte": "^0.562.0" + }, + "devDependencies": { + "@happy-dom/global-registrator": "^20.3.4", + "@rollup/rollup-darwin-arm64": "^4.55.2", + "@storybook/addon-essentials": "8.6.15", + "@storybook/svelte": "8.6.15", + "@storybook/svelte-vite": "8.6.15", + "@sveltejs/package": "^2.5.7", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@testing-library/svelte": "^5.3.1", + "@testing-library/user-event": "^14.6.1", + "@types/bun": "latest", + "storybook": "8.6.15", + "svelte": "^5.47.1", + "svelte-check": "^4.3.5", + "typescript": "^5.9.3", + "vite": "^7.3.1" + } +} \ No newline at end of file diff --git a/packages/ui/src/app.d.ts b/packages/ui/src/app.d.ts new file mode 100644 index 0000000..0759d31 --- /dev/null +++ b/packages/ui/src/app.d.ts @@ -0,0 +1,5 @@ +declare module "*.svelte" { + import type { Component } from "svelte"; + const component: Component; + export default component; +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts new file mode 100644 index 0000000..735d006 --- /dev/null +++ b/packages/ui/src/index.ts @@ -0,0 +1 @@ +export * from "./lib/index.js"; diff --git a/packages/ui/src/lib/ThemeProvider.svelte b/packages/ui/src/lib/ThemeProvider.svelte new file mode 100644 index 0000000..3dd33d3 --- /dev/null +++ b/packages/ui/src/lib/ThemeProvider.svelte @@ -0,0 +1,63 @@ + + + +{@render children?.()} diff --git a/packages/ui/src/lib/Welcome.stories.ts b/packages/ui/src/lib/Welcome.stories.ts new file mode 100644 index 0000000..40f317b --- /dev/null +++ b/packages/ui/src/lib/Welcome.stories.ts @@ -0,0 +1,16 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import Welcome from "./Welcome.svelte"; + +const meta: Meta = { + title: "Welcome", + component: Welcome, + tags: ["autodocs"], + parameters: { + layout: "centered", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Introduction: Story = {}; diff --git a/packages/ui/src/lib/Welcome.svelte b/packages/ui/src/lib/Welcome.svelte new file mode 100644 index 0000000..c151ddb --- /dev/null +++ b/packages/ui/src/lib/Welcome.svelte @@ -0,0 +1,4 @@ +
+

@say2/ui

+

Component library for Say2 Inspector

+
diff --git a/packages/ui/src/lib/composites/analysis/FilterPanel/FilterPanel.stories.ts b/packages/ui/src/lib/composites/analysis/FilterPanel/FilterPanel.stories.ts new file mode 100644 index 0000000..c107774 --- /dev/null +++ b/packages/ui/src/lib/composites/analysis/FilterPanel/FilterPanel.stories.ts @@ -0,0 +1,73 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import type { Component, ComponentProps } from "svelte"; +import FilterPanel from "./FilterPanel.svelte"; +import Wrapper from "./FilterPanel.story-wrapper.svelte"; + +const meta = { + title: "Composites/Analysis/FilterPanel", + component: FilterPanel, + tags: ["autodocs"], + argTypes: { + direction: { + control: "select", + options: ["all", "inbound", "outbound"], + }, + hasError: { + control: "select", + options: [null, true, false], + }, + }, +} satisfies Meta>; + +export default meta; +type Story = StoryObj>; + +const renderWrapper = (args: ComponentProps) => ({ + Component: Wrapper as unknown as Component, + props: args, +}); + +export const Default: Story = { + render: renderWrapper, + args: { + direction: "all", + methods: [], + hasError: null, + }, +}; + +export const WithFilters: Story = { + render: renderWrapper, + args: { + direction: "inbound", + methods: ["tools/list", "tools/call"], + hasError: null, + }, +}; + +export const AllSelected: Story = { + render: renderWrapper, + args: { + direction: "all", + methods: ["tools/list", "tools/call", "resources/read", "prompts/get"], + hasError: null, + }, +}; + +export const WithErrorFilter: Story = { + render: renderWrapper, + args: { + direction: "all", + methods: [], + hasError: true, + }, +}; + +export const HideErrors: Story = { + render: renderWrapper, + args: { + direction: "outbound", + methods: [], + hasError: false, + }, +}; diff --git a/packages/ui/src/lib/composites/analysis/FilterPanel/FilterPanel.story-wrapper.svelte b/packages/ui/src/lib/composites/analysis/FilterPanel/FilterPanel.story-wrapper.svelte new file mode 100644 index 0000000..cb55a2c --- /dev/null +++ b/packages/ui/src/lib/composites/analysis/FilterPanel/FilterPanel.story-wrapper.svelte @@ -0,0 +1,57 @@ + + +
+ + +
+ Current State: +
{JSON.stringify(filters, null, 2)}
+
+
diff --git a/packages/ui/src/lib/composites/analysis/FilterPanel/FilterPanel.svelte b/packages/ui/src/lib/composites/analysis/FilterPanel/FilterPanel.svelte new file mode 100644 index 0000000..9db9f6a --- /dev/null +++ b/packages/ui/src/lib/composites/analysis/FilterPanel/FilterPanel.svelte @@ -0,0 +1,176 @@ + + + + +
+
+ Filters + {#if activeFilterCount() > 0} + + {/if} +
+ +
+ + +
+ + {#if query} + + {resultText()} + + +
+
+ {/if} +
+ + diff --git a/packages/ui/src/lib/composites/analysis/SearchBar/SearchBar.test.ts b/packages/ui/src/lib/composites/analysis/SearchBar/SearchBar.test.ts new file mode 100644 index 0000000..ae978f6 --- /dev/null +++ b/packages/ui/src/lib/composites/analysis/SearchBar/SearchBar.test.ts @@ -0,0 +1,101 @@ +import { render, fireEvent } from "@testing-library/svelte"; +import { describe, it, expect, vi } from "bun:test"; +import SearchBar from "./SearchBar.svelte"; + +describe("SearchBar", () => { + const defaultProps = { + query: "", + resultCount: 0, + currentIndex: 0, + }; + + it("renders search input", () => { + const { getByRole } = render(SearchBar, defaultProps); + + expect(getByRole("search")).toBeTruthy(); + expect(getByRole("searchbox")).toBeTruthy(); + }); + + it("calls onsearch when input changes", async () => { + const onsearch = vi.fn(); + const { getByRole } = render(SearchBar, { + ...defaultProps, + onsearch, + }); + + const input = getByRole("searchbox"); + await fireEvent.input(input, { target: { value: "test" } }); + + expect(onsearch).toHaveBeenCalledWith("test"); + }); + + it("shows result count when query is present", () => { + const { getByText } = render(SearchBar, { + query: "test", + resultCount: 5, + currentIndex: 2, + }); + + expect(getByText("2 of 5")).toBeTruthy(); + }); + + it("shows 'No results' when resultCount is 0", () => { + const { getByText } = render(SearchBar, { + query: "test", + resultCount: 0, + currentIndex: 0, + }); + + expect(getByText("No results")).toBeTruthy(); + }); + + it("calls onnext when next button clicked", async () => { + const onnext = vi.fn(); + const { getByLabelText } = render(SearchBar, { + query: "test", + resultCount: 5, + currentIndex: 1, + onnext, + }); + + await fireEvent.click(getByLabelText("Next match")); + expect(onnext).toHaveBeenCalled(); + }); + + it("calls onprev when prev button clicked", async () => { + const onprev = vi.fn(); + const { getByLabelText } = render(SearchBar, { + query: "test", + resultCount: 5, + currentIndex: 2, + onprev, + }); + + await fireEvent.click(getByLabelText("Previous match")); + expect(onprev).toHaveBeenCalled(); + }); + + it("calls onclose when close button clicked", async () => { + const onclose = vi.fn(); + const { getByLabelText } = render(SearchBar, { + query: "test", + resultCount: 5, + currentIndex: 1, + onclose, + }); + + await fireEvent.click(getByLabelText("Close search")); + expect(onclose).toHaveBeenCalled(); + }); + + it("disables nav buttons when no results", () => { + const { getByLabelText } = render(SearchBar, { + query: "test", + resultCount: 0, + currentIndex: 0, + }); + + expect(getByLabelText("Next match").hasAttribute("disabled")).toBe(true); + expect(getByLabelText("Previous match").hasAttribute("disabled")).toBe(true); + }); +}); diff --git a/packages/ui/src/lib/composites/analysis/SearchBar/index.ts b/packages/ui/src/lib/composites/analysis/SearchBar/index.ts new file mode 100644 index 0000000..90be289 --- /dev/null +++ b/packages/ui/src/lib/composites/analysis/SearchBar/index.ts @@ -0,0 +1,2 @@ +export { default as SearchBar } from "./SearchBar.svelte"; +export type { Props as SearchBarProps } from "./SearchBar.svelte"; diff --git a/packages/ui/src/lib/composites/analysis/SearchHighlight/SearchHighlight.stories.ts b/packages/ui/src/lib/composites/analysis/SearchHighlight/SearchHighlight.stories.ts new file mode 100644 index 0000000..d5d028e --- /dev/null +++ b/packages/ui/src/lib/composites/analysis/SearchHighlight/SearchHighlight.stories.ts @@ -0,0 +1,72 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import SearchHighlight from "./SearchHighlight.svelte"; + +const meta = { + title: "Composites/Analysis/SearchHighlight", + component: SearchHighlight, + tags: ["autodocs"], + argTypes: { + text: { control: "text" }, + query: { control: "text" }, + caseSensitive: { control: "boolean" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const NoMatch: Story = { + args: { + text: "The quick brown fox jumps over the lazy dog", + query: "cat", + caseSensitive: false, + }, +}; + +export const SingleMatch: Story = { + args: { + text: "The quick brown fox jumps over the lazy dog", + query: "fox", + caseSensitive: false, + }, +}; + +export const MultipleMatches: Story = { + args: { + text: "The fox is quick. The fox is brown. The fox jumps high.", + query: "fox", + caseSensitive: false, + }, +}; + +export const CaseSensitive: Story = { + args: { + text: "The FOX is quick. The fox is brown. The Fox jumps high.", + query: "fox", + caseSensitive: true, + }, +}; + +export const CaseInsensitive: Story = { + args: { + text: "The FOX is quick. The fox is brown. The Fox jumps high.", + query: "fox", + caseSensitive: false, + }, +}; + +export const SpecialCharacters: Story = { + args: { + text: "Search for (test) and [test] and test+1 patterns", + query: "(test)", + caseSensitive: false, + }, +}; + +export const EmptyQuery: Story = { + args: { + text: "This text will not be highlighted because query is empty", + query: "", + caseSensitive: false, + }, +}; diff --git a/packages/ui/src/lib/composites/analysis/SearchHighlight/SearchHighlight.svelte b/packages/ui/src/lib/composites/analysis/SearchHighlight/SearchHighlight.svelte new file mode 100644 index 0000000..ef12355 --- /dev/null +++ b/packages/ui/src/lib/composites/analysis/SearchHighlight/SearchHighlight.svelte @@ -0,0 +1,63 @@ + + + +{#each segments as segment} + {#if segment.highlighted} + {segment.text} + {:else}{segment.text}{/if} +{/each} + + diff --git a/packages/ui/src/lib/composites/analysis/SearchHighlight/SearchHighlight.test.ts b/packages/ui/src/lib/composites/analysis/SearchHighlight/SearchHighlight.test.ts new file mode 100644 index 0000000..2d3e82c --- /dev/null +++ b/packages/ui/src/lib/composites/analysis/SearchHighlight/SearchHighlight.test.ts @@ -0,0 +1,94 @@ +import { render } from "@testing-library/svelte"; +import { describe, it, expect } from "bun:test"; +import SearchHighlight from "./SearchHighlight.svelte"; + +describe("SearchHighlight", () => { + it("renders text without highlights when no match", () => { + const { container } = render(SearchHighlight, { + text: "Hello world", + query: "foo", + }); + + expect(container.textContent).toBe("Hello world"); + expect(container.querySelector("mark")).toBeNull(); + }); + + it("highlights single match", () => { + const { container } = render(SearchHighlight, { + text: "Hello world", + query: "world", + }); + + const mark = container.querySelector("mark"); + expect(mark).toBeTruthy(); + expect(mark?.textContent).toBe("world"); + expect(container.textContent).toBe("Hello world"); + }); + + it("highlights multiple matches", () => { + const { container } = render(SearchHighlight, { + text: "The fox and the fox", + query: "fox", + }); + + const marks = container.querySelectorAll("mark"); + expect(marks.length).toBe(2); + expect(marks[0].textContent).toBe("fox"); + expect(marks[1].textContent).toBe("fox"); + }); + + it("case insensitive by default", () => { + const { container } = render(SearchHighlight, { + text: "Hello HELLO hello", + query: "hello", + caseSensitive: false, + }); + + const marks = container.querySelectorAll("mark"); + expect(marks.length).toBe(3); + }); + + it("respects case sensitive option", () => { + const { container } = render(SearchHighlight, { + text: "Hello HELLO hello", + query: "hello", + caseSensitive: true, + }); + + const marks = container.querySelectorAll("mark"); + expect(marks.length).toBe(1); + expect(marks[0].textContent).toBe("hello"); + }); + + it("escapes regex special characters in query", () => { + const { container } = render(SearchHighlight, { + text: "Search for (test) value", + query: "(test)", + }); + + const mark = container.querySelector("mark"); + expect(mark).toBeTruthy(); + expect(mark?.textContent).toBe("(test)"); + }); + + it("renders plain text when query is empty", () => { + const { container } = render(SearchHighlight, { + text: "Hello world", + query: "", + }); + + expect(container.textContent).toBe("Hello world"); + expect(container.querySelector("mark")).toBeNull(); + }); + + it("escapes brackets and other regex special chars", () => { + const { container } = render(SearchHighlight, { + text: "Match [test] and test+1", + query: "[test]", + }); + + const mark = container.querySelector("mark"); + expect(mark).toBeTruthy(); + expect(mark?.textContent).toBe("[test]"); + }); +}); diff --git a/packages/ui/src/lib/composites/analysis/SearchHighlight/index.ts b/packages/ui/src/lib/composites/analysis/SearchHighlight/index.ts new file mode 100644 index 0000000..c93ab14 --- /dev/null +++ b/packages/ui/src/lib/composites/analysis/SearchHighlight/index.ts @@ -0,0 +1 @@ +export { default as SearchHighlight } from "./SearchHighlight.svelte"; diff --git a/packages/ui/src/lib/composites/analysis/StatsPanel/StatsPanel.stories.ts b/packages/ui/src/lib/composites/analysis/StatsPanel/StatsPanel.stories.ts new file mode 100644 index 0000000..629d740 --- /dev/null +++ b/packages/ui/src/lib/composites/analysis/StatsPanel/StatsPanel.stories.ts @@ -0,0 +1,70 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import StatsPanel from "./StatsPanel.svelte"; + +const meta = { + title: "Composites/Analysis/StatsPanel", + component: StatsPanel, + tags: ["autodocs"], + argTypes: { + loading: { control: "boolean" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + stats: { + totalMessages: 1234, + requestCount: 456, + responseCount: 432, + notificationCount: 346, + errorCount: 12, + avgLatency: 42, + }, + loading: false, + }, +}; + +export const Loading: Story = { + args: { + stats: { + totalMessages: 0, + requestCount: 0, + responseCount: 0, + notificationCount: 0, + errorCount: 0, + avgLatency: 0, + }, + loading: true, + }, +}; + +export const NoMessages: Story = { + args: { + stats: { + totalMessages: 0, + requestCount: 0, + responseCount: 0, + notificationCount: 0, + errorCount: 0, + avgLatency: 0, + }, + loading: false, + }, +}; + +export const HighVolume: Story = { + args: { + stats: { + totalMessages: 1234567, + requestCount: 456789, + responseCount: 432100, + notificationCount: 345678, + errorCount: 1234, + avgLatency: 127, + }, + loading: false, + }, +}; diff --git a/packages/ui/src/lib/composites/analysis/StatsPanel/StatsPanel.svelte b/packages/ui/src/lib/composites/analysis/StatsPanel/StatsPanel.svelte new file mode 100644 index 0000000..d4a1cc3 --- /dev/null +++ b/packages/ui/src/lib/composites/analysis/StatsPanel/StatsPanel.svelte @@ -0,0 +1,150 @@ + + + + +
+
Statistics
+ +
+
+ Total Messages + {#if loading} + + {:else} + {formatNumber(stats.totalMessages)} + {/if} +
+ +
+ Requests + {#if loading} + + {:else} + {formatNumber(stats.requestCount)} + {/if} +
+ +
+ Responses + {#if loading} + + {:else} + {formatNumber(stats.responseCount)} + {/if} +
+ +
+ Notifications + {#if loading} + + {:else} + {formatNumber(stats.notificationCount)} + {/if} +
+ +
+ Errors + {#if loading} + + {:else} + {formatNumber(stats.errorCount)} + {/if} +
+ +
+ Avg Latency + {#if loading} + + {:else} + {formatNumber(stats.avgLatency)}ms + {/if} +
+
+
+ + diff --git a/packages/ui/src/lib/composites/analysis/StatsPanel/StatsPanel.test.ts b/packages/ui/src/lib/composites/analysis/StatsPanel/StatsPanel.test.ts new file mode 100644 index 0000000..82dcbe1 --- /dev/null +++ b/packages/ui/src/lib/composites/analysis/StatsPanel/StatsPanel.test.ts @@ -0,0 +1,75 @@ +import { render } from "@testing-library/svelte"; +import { describe, it, expect } from "bun:test"; +import StatsPanel from "./StatsPanel.svelte"; + +describe("StatsPanel", () => { + const defaultStats = { + totalMessages: 1234, + requestCount: 456, + responseCount: 432, + notificationCount: 346, + errorCount: 12, + avgLatency: 42, + }; + + it("displays all stats correctly", () => { + const { getByText } = render(StatsPanel, { stats: defaultStats }); + + expect(getByText("1,234")).toBeTruthy(); // totalMessages + expect(getByText("456")).toBeTruthy(); // requestCount + expect(getByText("432")).toBeTruthy(); // responseCount + expect(getByText("346")).toBeTruthy(); // notificationCount + expect(getByText("12")).toBeTruthy(); // errorCount + expect(getByText("42ms")).toBeTruthy(); // avgLatency with suffix + }); + + it("formats large numbers with commas", () => { + const { getByText } = render(StatsPanel, { + stats: { + totalMessages: 1234567, + requestCount: 456789, + responseCount: 0, + notificationCount: 0, + errorCount: 0, + avgLatency: 0, + }, + }); + + expect(getByText("1,234,567")).toBeTruthy(); + expect(getByText("456,789")).toBeTruthy(); + }); + + it("displays zero values correctly", () => { + const { container, getByText } = render(StatsPanel, { + stats: { + totalMessages: 0, + requestCount: 0, + responseCount: 0, + notificationCount: 0, + errorCount: 0, + avgLatency: 0, + }, + }); + + // Should show "0", not empty or dash - check badges exist + const badges = container.querySelectorAll(".badge"); + expect(badges.length).toBe(6); + // Check latency shows 0ms + expect(container.textContent).toContain("0ms"); + }); + + it("shows skeleton placeholders when loading", () => { + const { container } = render(StatsPanel, { + stats: defaultStats, + loading: true, + }); + + const skeletons = container.querySelectorAll(".ce-stats-panel__skeleton"); + expect(skeletons.length).toBe(6); // 6 stats + }); + + it("displays avgLatency with ms suffix", () => { + const { container } = render(StatsPanel, { stats: defaultStats }); + expect(container.textContent).toContain("42ms"); + }); +}); diff --git a/packages/ui/src/lib/composites/analysis/StatsPanel/index.ts b/packages/ui/src/lib/composites/analysis/StatsPanel/index.ts new file mode 100644 index 0000000..0e2ed28 --- /dev/null +++ b/packages/ui/src/lib/composites/analysis/StatsPanel/index.ts @@ -0,0 +1,2 @@ +export { default as StatsPanel } from "./StatsPanel.svelte"; +export type { TrafficStats, Props as StatsPanelProps } from "./StatsPanel.svelte"; diff --git a/packages/ui/src/lib/composites/cross-cutting/EmptyState/EmptyState.stories.ts b/packages/ui/src/lib/composites/cross-cutting/EmptyState/EmptyState.stories.ts new file mode 100644 index 0000000..1e8e334 --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/EmptyState/EmptyState.stories.ts @@ -0,0 +1,61 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import type { Component, ComponentProps } from "svelte"; +import EmptyState from "./EmptyState.svelte"; +import EmptyStateWrapper from "./EmptyState.story-wrapper.svelte"; + +const meta = { + title: "Composites/Cross-Cutting/EmptyState", + component: EmptyState, + tags: ["autodocs"], + argTypes: { + title: { control: "text" }, + description: { control: "text" }, + // Wrapper args + showAction: { control: "boolean" }, + actionText: { control: "text" }, + iconName: { + control: { type: "select" }, + options: ["search", "folder", "wifi", "none"] + }, + }, +} satisfies Meta>; + +export default meta; +type Story = StoryObj>; + +// Use wrapper for all stories to support the snippets logic +const renderWrapper = (args: ComponentProps) => ({ + Component: EmptyStateWrapper as unknown as Component, + props: args, +}); + +export const Default: Story = { + render: renderWrapper, + args: { + title: "No Items", + description: "There is nothing to display here yet.", + iconName: "folder", + showAction: false, + }, +}; + +export const WithAction: Story = { + render: renderWrapper, + args: { + title: "No Connection", + description: "You are not connected to any server.", + iconName: "wifi", + showAction: true, + actionText: "Connect Now", + }, +}; + +export const TextOnly: Story = { + render: renderWrapper, + args: { + title: "Simple Empty State", + description: "Just a message without an icon.", + iconName: undefined, // "none" in select or undefined + showAction: false, + }, +}; diff --git a/packages/ui/src/lib/composites/cross-cutting/EmptyState/EmptyState.story-wrapper.svelte b/packages/ui/src/lib/composites/cross-cutting/EmptyState/EmptyState.story-wrapper.svelte new file mode 100644 index 0000000..ed52465 --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/EmptyState/EmptyState.story-wrapper.svelte @@ -0,0 +1,53 @@ + + +
+ + + {#snippet action()} + {#if showAction} + + {/if} + {/snippet} + +
diff --git a/packages/ui/src/lib/composites/cross-cutting/EmptyState/EmptyState.svelte b/packages/ui/src/lib/composites/cross-cutting/EmptyState/EmptyState.svelte new file mode 100644 index 0000000..9dd0b10 --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/EmptyState/EmptyState.svelte @@ -0,0 +1,86 @@ + + +
+ {#if Icon} +
+ +
+ {/if} + +

{title}

+ + {#if description} +

{description}

+ {/if} + + {#if action} +
+ {@render action()} +
+ {/if} +
+ + diff --git a/packages/ui/src/lib/composites/cross-cutting/EmptyState/EmptyState.test.ts b/packages/ui/src/lib/composites/cross-cutting/EmptyState/EmptyState.test.ts new file mode 100644 index 0000000..1c45056 --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/EmptyState/EmptyState.test.ts @@ -0,0 +1,29 @@ +import { render } from "@testing-library/svelte"; +import { describe, it, expect } from "bun:test"; +import EmptyState from "./EmptyState.svelte"; + +describe("EmptyState", () => { + it("renders title and description", () => { + const { getByText } = render(EmptyState, { + title: "Empty Folder", + description: "No files here yet." + }); + + expect(getByText("Empty Folder")).toBeTruthy(); + expect(getByText("No files here yet.")).toBeTruthy(); + }); + + it("renders title only if description is missing", () => { + const { getByText, queryByText } = render(EmptyState, { + title: "Just Title" + }); + + expect(getByText("Just Title")).toBeTruthy(); + // Since we don't know the exact class or structure without setup, + // verifying checking for description text absence is tricky if we don't have a specific text. + // But we can check it renders without error. + }); + + // To test icons or snippets, we would typically use a wrapper or inspect classes, + // but verifying basic prop rendering covers the contract for a dumb component. +}); diff --git a/packages/ui/src/lib/composites/cross-cutting/FocusTrap/FocusTrap.stories.ts b/packages/ui/src/lib/composites/cross-cutting/FocusTrap/FocusTrap.stories.ts new file mode 100644 index 0000000..d407c8d --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/FocusTrap/FocusTrap.stories.ts @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import FocusTrapWrapper from "./FocusTrap.story-wrapper.svelte"; + +const meta = { + title: "Composites/Cross-Cutting/FocusTrap", + component: FocusTrapWrapper, // Use wrapper as the component under test in story + tags: ["autodocs"], + argTypes: { + active: { control: "boolean" }, + returnFocus: { control: "boolean" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Interactive: Story = { + args: { + active: false, + returnFocus: true, + }, +}; + +export const ActiveByDefault: Story = { + args: { + active: true, + returnFocus: true, + } +}; diff --git a/packages/ui/src/lib/composites/cross-cutting/FocusTrap/FocusTrap.story-wrapper.svelte b/packages/ui/src/lib/composites/cross-cutting/FocusTrap/FocusTrap.story-wrapper.svelte new file mode 100644 index 0000000..eb200b9 --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/FocusTrap/FocusTrap.story-wrapper.svelte @@ -0,0 +1,197 @@ + + +
+
+ +
+ +
+ + +
+ +
+
+

Trap Area

+ + {isOpen ? "Active" : "Inactive"} + +
+ + +
+

+ Focus stays within this box when active. Escape key + deactivates. +

+
+ + +
+
+ +
+
+
+
+ +
+ + +
+
+ + diff --git a/packages/ui/src/lib/composites/cross-cutting/FocusTrap/FocusTrap.svelte b/packages/ui/src/lib/composites/cross-cutting/FocusTrap/FocusTrap.svelte new file mode 100644 index 0000000..a02cfa2 --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/FocusTrap/FocusTrap.svelte @@ -0,0 +1,75 @@ + + +
+ {#if children} + {@render children()} + {/if} +
+ + diff --git a/packages/ui/src/lib/composites/cross-cutting/FocusTrap/FocusTrap.test.ts b/packages/ui/src/lib/composites/cross-cutting/FocusTrap/FocusTrap.test.ts new file mode 100644 index 0000000..9e60535 --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/FocusTrap/FocusTrap.test.ts @@ -0,0 +1,66 @@ +import { render } from "@testing-library/svelte"; +import { describe, it, expect, jest, beforeEach, afterEach, mock } from "bun:test"; +import FocusTrap from "./FocusTrap.svelte"; + +// Mock focus-trap +const mockActivate = jest.fn(); +const mockDeactivate = jest.fn(); +const mockCreateFocusTrap = jest.fn(() => ({ + activate: mockActivate, + deactivate: mockDeactivate, +})); + +// Bun/Jest mocking +mock.module("focus-trap", () => ({ + createFocusTrap: mockCreateFocusTrap, +})); + +describe("FocusTrap", () => { + beforeEach(() => { + mockActivate.mockClear(); + mockDeactivate.mockClear(); + mockCreateFocusTrap.mockClear(); + }); + + it("creates trap initialization on mount", async () => { + render(FocusTrap, { active: false }); + // Effects run async + await new Promise((r) => setTimeout(r, 10)); + + expect(mockCreateFocusTrap).toHaveBeenCalled(); + expect(mockActivate).not.toHaveBeenCalled(); + }); + + it("activates trap when active prop is true", async () => { + render(FocusTrap, { active: true }); + await new Promise((r) => setTimeout(r, 10)); + + expect(mockCreateFocusTrap).toHaveBeenCalled(); + expect(mockActivate).toHaveBeenCalled(); + }); + + it("deactivates trap when unlocked/inactive", async () => { + const { component } = render(FocusTrap, { active: true }); + await new Promise((r) => setTimeout(r, 10)); + expect(mockActivate).toHaveBeenCalled(); + + // Update prop + // @ts-ignore - Svelte 5 component prop update for simple testing + // Actually component.$set is deprecated in Svelte 5 runes. + // We really should use a wrapper or re-render? + // render returns 'rerender' for manual updates? No, that's React. + // testing-library-svelte: component.$set works for legacy, but for runes we should likely mount with a wrapper. + // But let's try calling unmount to see at least deactivate on cleanup. + }); + + it("deactivates on unmount", async () => { + const { unmount } = render(FocusTrap, { active: true }); + await new Promise((r) => setTimeout(r, 10)); + expect(mockActivate).toHaveBeenCalled(); + + unmount(); + // force cleanup + await new Promise((r) => setTimeout(r, 10)); + expect(mockDeactivate).toHaveBeenCalled(); + }); +}); diff --git a/packages/ui/src/lib/composites/cross-cutting/FocusTrap/index.ts b/packages/ui/src/lib/composites/cross-cutting/FocusTrap/index.ts new file mode 100644 index 0000000..4204c20 --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/FocusTrap/index.ts @@ -0,0 +1 @@ +export { default as FocusTrap } from "./FocusTrap.svelte"; diff --git a/packages/ui/src/lib/composites/cross-cutting/LiveRegion/LiveRegion.stories.ts b/packages/ui/src/lib/composites/cross-cutting/LiveRegion/LiveRegion.stories.ts new file mode 100644 index 0000000..fb6914e --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/LiveRegion/LiveRegion.stories.ts @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import type { Component } from "svelte"; +import LiveRegion from "./LiveRegion.svelte"; +import LiveRegionStory from "./LiveRegion.story-wrapper.svelte"; + +const meta = { + title: "Composites/Cross-Cutting/LiveRegion", + component: LiveRegion, + tags: ["autodocs"], + argTypes: { + message: { control: "text" }, + assertive: { control: "boolean" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Interactive story using the wrapper +export const Interactive: Story = { + render: () => ({ + Component: LiveRegionStory as unknown as Component, + }), + args: { + message: "", // dummy args + } +}; + +// Dumb component Stories (not very visual, but good for docs) +export const Polite: Story = { + args: { + message: "This is a polite announcement.", + assertive: false, + }, +}; + +export const Assertive: Story = { + args: { + message: "This is an assertive announcement!", + assertive: true, + }, +}; diff --git a/packages/ui/src/lib/composites/cross-cutting/LiveRegion/LiveRegion.story-wrapper.svelte b/packages/ui/src/lib/composites/cross-cutting/LiveRegion/LiveRegion.story-wrapper.svelte new file mode 100644 index 0000000..f89c348 --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/LiveRegion/LiveRegion.story-wrapper.svelte @@ -0,0 +1,153 @@ + + +
+
+ e.key === "Enter" && announce()} + /> +
+ + +
+
+ + + + +
+

Announcement History (Visual Log)

+ {#if history.length === 0} +

No announcements yet.

+ {:else} +
    + {#each history as item, i} +
  • + {new Date().toLocaleTimeString()} + {item} + {#if i === 0 && assertive} + Assertive + {/if} +
  • + {/each} +
+ {/if} +
+
+ + diff --git a/packages/ui/src/lib/composites/cross-cutting/LiveRegion/LiveRegion.svelte b/packages/ui/src/lib/composites/cross-cutting/LiveRegion/LiveRegion.svelte new file mode 100644 index 0000000..105828a --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/LiveRegion/LiveRegion.svelte @@ -0,0 +1,32 @@ + + +
+ {message} +
+ + diff --git a/packages/ui/src/lib/composites/cross-cutting/LiveRegion/LiveRegion.test.ts b/packages/ui/src/lib/composites/cross-cutting/LiveRegion/LiveRegion.test.ts new file mode 100644 index 0000000..0b7e96b --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/LiveRegion/LiveRegion.test.ts @@ -0,0 +1,35 @@ +import { render } from "@testing-library/svelte"; +import { describe, it, expect } from "bun:test"; +import LiveRegion from "./LiveRegion.svelte"; + +describe("LiveRegion", () => { + it("renders with polite aria-live by default", () => { + const { getByRole } = render(LiveRegion, { message: "Hello" }); + const region = getByRole("status"); + + expect(region.getAttribute("aria-live")).toBe("polite"); + expect(region.textContent).toContain("Hello"); + }); + + it("renders with assertive aria-live when assertive prop is true", () => { + const { getByRole } = render(LiveRegion, { message: "Alert!", assertive: true }); + const region = getByRole("status"); + + expect(region.getAttribute("aria-live")).toBe("assertive"); + expect(region.textContent).toContain("Alert!"); + }); + + it("has visually hidden styles", () => { + // We can't easily check computed styles in JSDOM/BunDOM reliably for all CSS classes, + // but we can check if the class is present. + const { getByRole } = render(LiveRegion, { message: "Hidden" }); + const region = getByRole("status"); + expect(region.classList.contains("ce-live-region")).toBe(true); + }); + + it("renders empty when message is empty", () => { + const { getByRole } = render(LiveRegion, { message: "" }); + const region = getByRole("status"); + expect(region.textContent).toBe(""); + }); +}); diff --git a/packages/ui/src/lib/composites/cross-cutting/ThemeToggle/ThemeToggle.stories.ts b/packages/ui/src/lib/composites/cross-cutting/ThemeToggle/ThemeToggle.stories.ts new file mode 100644 index 0000000..7f90390 --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/ThemeToggle/ThemeToggle.stories.ts @@ -0,0 +1,45 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import type { Component } from "svelte"; +import ThemeToggle from "./ThemeToggle.svelte"; + +const meta = { + title: "Composites/Cross-Cutting/ThemeToggle", + component: ThemeToggle, + tags: ["autodocs"], + argTypes: { + theme: { + control: { type: "select" }, + options: ["light", "dark", "system"], + }, + onchange: { action: "changed" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Light: Story = { + args: { + theme: "light", + }, +}; + +export const Dark: Story = { + args: { + theme: "dark", + }, +}; + +export const System: Story = { + args: { + theme: "system", + }, +}; + +import InteractiveWrapper from "./ThemeToggle.story-wrapper.svelte"; +export const Interactive: Story = { + render: () => ({ + Component: InteractiveWrapper as unknown as Component, + }), + args: { theme: 'light' }, // specific args needed to satisfy types +}; diff --git a/packages/ui/src/lib/composites/cross-cutting/ThemeToggle/ThemeToggle.story-wrapper.svelte b/packages/ui/src/lib/composites/cross-cutting/ThemeToggle/ThemeToggle.story-wrapper.svelte new file mode 100644 index 0000000..2007fda --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/ThemeToggle/ThemeToggle.story-wrapper.svelte @@ -0,0 +1,18 @@ + + +
+

+ Click to cycle theme (State: {theme}) +

+ +
diff --git a/packages/ui/src/lib/composites/cross-cutting/ThemeToggle/ThemeToggle.svelte b/packages/ui/src/lib/composites/cross-cutting/ThemeToggle/ThemeToggle.svelte new file mode 100644 index 0000000..d193c2b --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/ThemeToggle/ThemeToggle.svelte @@ -0,0 +1,43 @@ + + +
+
+ + diff --git a/packages/ui/src/lib/composites/cross-cutting/ThemeToggle/ThemeToggle.test.ts b/packages/ui/src/lib/composites/cross-cutting/ThemeToggle/ThemeToggle.test.ts new file mode 100644 index 0000000..8eb61eb --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/ThemeToggle/ThemeToggle.test.ts @@ -0,0 +1,45 @@ +import { render, fireEvent } from "@testing-library/svelte"; +import { describe, it, expect, jest } from "bun:test"; +import ThemeToggle from "./ThemeToggle.svelte"; + +describe("ThemeToggle", () => { + it("renders with correct initial icon for light theme", () => { + const { getByLabelText } = render(ThemeToggle, { theme: "light" }); + const button = getByLabelText("Current theme: light. Click to change."); + expect(button).toBeTruthy(); + // Icon check is implicit via rendering without error, + // typically we'd check SVG contents but label is enough for "dumb" component logic + }); + + it("cycles theme light -> dark -> system -> light", async () => { + const onchange = jest.fn(); + const { getByRole, component } = render(ThemeToggle, { theme: "light", onchange }); + const button = getByRole("button"); + + // Click 1: light -> dark + await fireEvent.click(button); + expect(onchange).toHaveBeenCalledWith("dark"); + + // Update prop manually (since it's a dumb component) + // Re-render with new prop to simulate parent update + // Note: In unit test, we just check the event fired with expected value + }); + + it("fires onchange with correct next value for dark", async () => { + const onchange = jest.fn(); + const { getByRole } = render(ThemeToggle, { theme: "dark", onchange }); + const button = getByRole("button"); + + await fireEvent.click(button); + expect(onchange).toHaveBeenCalledWith("system"); + }); + + it("fires onchange with correct next value for system", async () => { + const onchange = jest.fn(); + const { getByRole } = render(ThemeToggle, { theme: "system", onchange }); + const button = getByRole("button"); + + await fireEvent.click(button); + expect(onchange).toHaveBeenCalledWith("light"); + }); +}); diff --git a/packages/ui/src/lib/composites/cross-cutting/ThemeToggle/index.ts b/packages/ui/src/lib/composites/cross-cutting/ThemeToggle/index.ts new file mode 100644 index 0000000..3fb7641 --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/ThemeToggle/index.ts @@ -0,0 +1 @@ +export { default as ThemeToggle } from "./ThemeToggle.svelte"; diff --git a/packages/ui/src/lib/composites/cross-cutting/VirtualList/VirtualList.stories.ts b/packages/ui/src/lib/composites/cross-cutting/VirtualList/VirtualList.stories.ts new file mode 100644 index 0000000..d667418 --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/VirtualList/VirtualList.stories.ts @@ -0,0 +1,54 @@ +import type { Meta, StoryObj } from '@storybook/svelte'; +import VirtualListWrapper from './VirtualList.story-wrapper.svelte'; + +interface Item { + id: string; + title: string; + description: string; + height: number; +} + +const generateItems = (count: number): Item[] => + Array.from({ length: count }, (_, i) => ({ + id: `item-${i}`, + title: `Item ${i + 1}`, + description: `This is the description for item ${i + 1}. It might be longer.`, + height: 40 + Math.floor(Math.random() * 60) + })); + +const meta = { + title: 'Composites/Cross-Cutting/VirtualList', + component: VirtualListWrapper, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const FixedHeight: Story = { + args: { + items: generateItems(1000), + itemHeight: 60, + } +}; + +export const VariableHeight: Story = { + args: { + items: generateItems(1000), + itemHeight: ((item: Item) => item.height) as any, + } +}; + +export const TenThousandItems: Story = { + args: { + items: generateItems(10000), + itemHeight: 50, + } +}; + +export const Empty: Story = { + args: { + items: [], + itemHeight: 50, + } +}; diff --git a/packages/ui/src/lib/composites/cross-cutting/VirtualList/VirtualList.story-wrapper.svelte b/packages/ui/src/lib/composites/cross-cutting/VirtualList/VirtualList.story-wrapper.svelte new file mode 100644 index 0000000..f115d93 --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/VirtualList/VirtualList.story-wrapper.svelte @@ -0,0 +1,144 @@ + + +
+
+
+ + +
+
+ {items.length} total items +
+
+ +
+ + {#snippet children(item, index)} +
+
+ {item.title} + #{index} +
+

+ {item.description} +

+
+ {/snippet} + + {#snippet empty()} +
No items found.
+ {/snippet} +
+
+
+ + diff --git a/packages/ui/src/lib/composites/cross-cutting/VirtualList/VirtualList.svelte b/packages/ui/src/lib/composites/cross-cutting/VirtualList/VirtualList.svelte new file mode 100644 index 0000000..e9ab8c2 --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/VirtualList/VirtualList.svelte @@ -0,0 +1,169 @@ + + + + +
+ {#if items.length === 0} +
+ {#if empty} + {@render empty()} + {:else} +

No items to display

+ {/if} +
+ {:else} +
+ {#each $virtualizer.getVirtualItems() as virtualItem (virtualItem.index)} +
+ {@render children( + items[virtualItem.index]!, + virtualItem.index, + )} +
+ {/each} +
+ {/if} +
+ + diff --git a/packages/ui/src/lib/composites/cross-cutting/VirtualList/VirtualList.test-wrapper.svelte b/packages/ui/src/lib/composites/cross-cutting/VirtualList/VirtualList.test-wrapper.svelte new file mode 100644 index 0000000..20dbfdb --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/VirtualList/VirtualList.test-wrapper.svelte @@ -0,0 +1,11 @@ + + + + {#snippet children(item, index)} +
{item.text}
+ {/snippet} +
diff --git a/packages/ui/src/lib/composites/cross-cutting/VirtualList/VirtualList.test.ts b/packages/ui/src/lib/composites/cross-cutting/VirtualList/VirtualList.test.ts new file mode 100644 index 0000000..a177115 --- /dev/null +++ b/packages/ui/src/lib/composites/cross-cutting/VirtualList/VirtualList.test.ts @@ -0,0 +1,45 @@ +import { render, screen } from "@testing-library/svelte"; +import { describe, expect, it } from "bun:test"; +import VirtualList from "./VirtualList.svelte"; +import VirtualListTestWrapper from "./VirtualList.test-wrapper.svelte"; + +// Mock ResizeObserver for TanStack Virtual +global.ResizeObserver = class ResizeObserver { + observe() { } + unobserve() { } + disconnect() { } +}; + +describe("VirtualList", () => { + const items = Array.from({ length: 100 }, (_, i) => ({ id: i, text: `Item ${i}` })); + const itemHeight = 50; + + it("renders only a subset of items initially", async () => { + const { container } = render(VirtualListTestWrapper, { + props: { + items, + itemHeight + } + }); + + const renderedItems = container.querySelectorAll("[data-index]"); + // With 0 height container, it should only render the overscan (default 3) + // but at least less than 100. + expect(renderedItems.length).toBeLessThan(items.length); + }); + + it("renders empty state when items is empty", () => { + render(VirtualList, { + props: { + items: [], + itemHeight, + children: () => ({} as any) + } + }); + expect(screen.getByText("No items to display")).toBeTruthy(); + }); + + // Testing scroll methods would require bind:this which is hard in + // testing-library-svelte without a wrapper component. + // We'll trust TanStack Virtual for the core logic and focus on our integration. +}); diff --git a/packages/ui/src/lib/composites/detail/ContextMenu/ContextMenu.stories.ts b/packages/ui/src/lib/composites/detail/ContextMenu/ContextMenu.stories.ts new file mode 100644 index 0000000..16154f6 --- /dev/null +++ b/packages/ui/src/lib/composites/detail/ContextMenu/ContextMenu.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import ContextMenu from "./ContextMenu.svelte"; +import Wrapper from "./ContextMenu.story-wrapper.svelte"; +import type { Component, ComponentProps } from "svelte"; + +const meta = { + title: "Composites/Detail/ContextMenu", + component: Wrapper as unknown as Component, + tags: ["autodocs"], +} satisfies Meta>; + +export default meta; +type Story = StoryObj>; + +export const Interactive: Story = { + render: () => ({ + Component: Wrapper as unknown as Component, + }), +}; diff --git a/packages/ui/src/lib/composites/detail/ContextMenu/ContextMenu.story-wrapper.svelte b/packages/ui/src/lib/composites/detail/ContextMenu/ContextMenu.story-wrapper.svelte new file mode 100644 index 0000000..550519c --- /dev/null +++ b/packages/ui/src/lib/composites/detail/ContextMenu/ContextMenu.story-wrapper.svelte @@ -0,0 +1,71 @@ + + + +
+
+

Right-click anywhere in this area

+

+ A context menu with JSON actions will appear at your mouse + position. +

+
+
+
+ + diff --git a/packages/ui/src/lib/composites/detail/ContextMenu/ContextMenu.svelte b/packages/ui/src/lib/composites/detail/ContextMenu/ContextMenu.svelte new file mode 100644 index 0000000..2310b6d --- /dev/null +++ b/packages/ui/src/lib/composites/detail/ContextMenu/ContextMenu.svelte @@ -0,0 +1,124 @@ + + + + + + + {@render children()} + + + + + {#each items as item} + + {#if item.icon} + {@const Icon = item.icon} + + + + {/if} + {item.label} + + {/each} + + + + + diff --git a/packages/ui/src/lib/composites/detail/ContextMenu/ContextMenu.test.ts b/packages/ui/src/lib/composites/detail/ContextMenu/ContextMenu.test.ts new file mode 100644 index 0000000..406d645 --- /dev/null +++ b/packages/ui/src/lib/composites/detail/ContextMenu/ContextMenu.test.ts @@ -0,0 +1,46 @@ +import { render, fireEvent, waitFor } from "@testing-library/svelte"; +import { describe, it, expect, jest, beforeAll } from "bun:test"; +import Wrapper from "./ContextMenu.story-wrapper.svelte"; + +// Mock Web Animations API +beforeAll(() => { + if (typeof Element !== "undefined" && !Element.prototype.animate) { + Element.prototype.animate = () => + ({ + finished: Promise.resolve(), + cancel: () => { }, + onfinish: null, + play: () => { }, + pause: () => { }, + reverse: () => { }, + }) as any; + } +}); + +describe("ContextMenu", () => { + it("renders trigger and opens menu on right-click", async () => { + const { getByText, queryByText } = render(Wrapper); + + // Trigger is visible + expect(getByText("Right-click anywhere in this area")).toBeTruthy(); + + // Menu content should not be in document initially + expect(queryByText("Copy Value")).toBeNull(); + + const area = getByText("Right-click anywhere in this area").closest( + ".demo-area", + ); + if (!area) throw new Error("Demo area not found"); + + // Simulate right-click + await fireEvent.contextMenu(area); + + // Wait for the portal to render the content + await waitFor(() => { + expect(getByText("Copy Value")).toBeTruthy(); + }); + + expect(getByText("Copy Path")).toBeTruthy(); + expect(getByText("Delete Node")).toBeTruthy(); + }); +}); diff --git a/packages/ui/src/lib/composites/detail/CopyButton/CopyButton.stories.ts b/packages/ui/src/lib/composites/detail/CopyButton/CopyButton.stories.ts new file mode 100644 index 0000000..83ac8ef --- /dev/null +++ b/packages/ui/src/lib/composites/detail/CopyButton/CopyButton.stories.ts @@ -0,0 +1,44 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import CopyButton from "./CopyButton.svelte"; + +const meta = { + title: "Composites/Detail/CopyButton", + component: CopyButton, + tags: ["autodocs"], + argTypes: { + variant: { + control: { type: "select" }, + options: ["primary", "secondary", "ghost", "danger"], + }, + size: { + control: { type: "select" }, + options: ["sm", "md", "lg"], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + value: "Hello, world!", + label: "Copy Text", + }, +}; + +export const IconOnly: Story = { + args: { + value: "Secret Key: 12345", + variant: "ghost", + size: "md", + }, +}; + +export const Primary: Story = { + args: { + value: "https://example.com", + label: "Copy Link", + variant: "primary", + }, +}; diff --git a/packages/ui/src/lib/composites/detail/CopyButton/CopyButton.svelte b/packages/ui/src/lib/composites/detail/CopyButton/CopyButton.svelte new file mode 100644 index 0000000..d7916f7 --- /dev/null +++ b/packages/ui/src/lib/composites/detail/CopyButton/CopyButton.svelte @@ -0,0 +1,99 @@ + + + + + diff --git a/packages/ui/src/lib/composites/detail/CopyButton/CopyButton.test.ts b/packages/ui/src/lib/composites/detail/CopyButton/CopyButton.test.ts new file mode 100644 index 0000000..794e36d --- /dev/null +++ b/packages/ui/src/lib/composites/detail/CopyButton/CopyButton.test.ts @@ -0,0 +1,62 @@ +import { render, fireEvent } from "@testing-library/svelte"; +import { describe, it, expect, jest, beforeAll, afterAll } from "bun:test"; +import CopyButton from "./CopyButton.svelte"; + +describe("CopyButton", () => { + let originalClipboard: any; + + beforeAll(() => { + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: jest.fn().mockResolvedValue(undefined) + }, + configurable: true, + writable: true + }); + + // Mock Web Animations API missing in JSDOM/Bun + if (typeof Element !== 'undefined' && !Element.prototype.animate) { + Element.prototype.animate = () => ({ + finished: Promise.resolve(), + cancel: () => { }, + onfinish: null, + play: () => { }, + pause: () => { }, + reverse: () => { }, + }) as any; + } + }); + + afterAll(() => { + // No simple way to restore if it was truly read-only, + // but since we made it configurable/writable it's fine for tests. + }); + + it("renders label and copies text on click", async () => { + const { getByText, getByRole } = render(CopyButton, { + value: "Test Value", + label: "Copy Me" + }); + + expect(getByText("Copy Me")).toBeTruthy(); + + const button = getByRole("button"); + await fireEvent.click(button); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith("Test Value"); + expect(getByText("Copied!")).toBeTruthy(); + }); + + it("handles icon-only variant", async () => { + const { getByRole, queryByText } = render(CopyButton, { + value: "Icon Only" + }); + + const button = getByRole("button"); + expect(queryByText("Copied!")).toBeNull(); + + await fireEvent.click(button); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith("Icon Only"); + expect(queryByText("Copied!")).toBeTruthy(); + }); +}); diff --git a/packages/ui/src/lib/composites/detail/DetailPanel/DetailPanel.stories.ts b/packages/ui/src/lib/composites/detail/DetailPanel/DetailPanel.stories.ts new file mode 100644 index 0000000..6cf049e --- /dev/null +++ b/packages/ui/src/lib/composites/detail/DetailPanel/DetailPanel.stories.ts @@ -0,0 +1,114 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import DetailPanel from "./DetailPanel.svelte"; +import type { Message } from "./DetailPanel.svelte"; + +const meta = { + title: "Composites/Detail/DetailPanel", + component: DetailPanel, + tags: ["autodocs"], + argTypes: { + loading: { control: "boolean" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const sampleMessage: Message = { + id: "msg-12345", + direction: "inbound", + type: "request", + method: "tools/call", + preview: "Calling function getData...", + timestamp: new Date(), + hasResponse: true, + content: { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: "getData", + arguments: { + query: "SELECT * FROM users", + limit: 100, + }, + }, + }, +}; + +export const WithMessage: Story = { + args: { + message: sampleMessage, + loading: false, + }, +}; + +export const Empty: Story = { + args: { + message: null, + loading: false, + }, +}; + +export const Loading: Story = { + args: { + message: null, + loading: true, + }, +}; + +export const Request: Story = { + args: { + message: { + ...sampleMessage, + type: "request", + direction: "outbound", + }, + loading: false, + }, +}; + +export const Response: Story = { + args: { + message: { + ...sampleMessage, + id: "msg-67890", + type: "response", + direction: "inbound", + method: "tools/call", + content: { + jsonrpc: "2.0", + id: 1, + result: { + data: [ + { id: 1, name: "Alice" }, + { id: 2, name: "Bob" }, + ], + total: 2, + }, + }, + }, + loading: false, + }, +}; + +export const Notification: Story = { + args: { + message: { + ...sampleMessage, + id: "msg-notif-001", + type: "notification", + method: "notifications/progress", + content: { + jsonrpc: "2.0", + method: "notifications/progress", + params: { + progressToken: "token-123", + progress: 75, + total: 100, + }, + }, + }, + loading: false, + }, +}; diff --git a/packages/ui/src/lib/composites/detail/DetailPanel/DetailPanel.svelte b/packages/ui/src/lib/composites/detail/DetailPanel/DetailPanel.svelte new file mode 100644 index 0000000..8590cda --- /dev/null +++ b/packages/ui/src/lib/composites/detail/DetailPanel/DetailPanel.svelte @@ -0,0 +1,345 @@ + + + + +
+
+

Message Details

+
+ + {#if loading} +
+ + Loading message content... +
+ {:else if !message} +
+ +

Select a message to view details

+
+ {:else} +
+ +
+
+ Method +
+ {message.method} + +
+
+ +
+ Type +
+ {message.type} +
+
+ +
+ Direction +
+ + {message.direction} +
+
+ +
+ Time +
+ +
+
+ +
+ ID +
+ {message.id} + +
+
+
+ + +
+
+

Content

+ +
+ +
+ {#if message.content} + + {:else} +
+ No content available +
+ {/if} +
+
+
+ {/if} +
+ + diff --git a/packages/ui/src/lib/composites/detail/DetailPanel/DetailPanel.test.ts b/packages/ui/src/lib/composites/detail/DetailPanel/DetailPanel.test.ts new file mode 100644 index 0000000..dc41fa8 --- /dev/null +++ b/packages/ui/src/lib/composites/detail/DetailPanel/DetailPanel.test.ts @@ -0,0 +1,82 @@ +import { render, fireEvent } from "@testing-library/svelte"; +import { describe, it, expect, vi } from "bun:test"; +import DetailPanel from "./DetailPanel.svelte"; +import type { Message } from "./DetailPanel.svelte"; + +describe("DetailPanel", () => { + const sampleMessage: Message = { + id: "msg-12345", + direction: "inbound", + type: "request", + method: "tools/call", + preview: "Test message", + timestamp: new Date("2026-01-21T12:00:00"), + content: { test: "data" }, + }; + + it("renders message content via JSONInspector", () => { + const { container } = render(DetailPanel, { + message: sampleMessage, + }); + + expect(container.textContent).toContain("tools/call"); + expect(container.querySelector(".ce-json-inspector")).toBeTruthy(); + }); + + it("shows empty state when message is null", () => { + const { container } = render(DetailPanel, { + message: null, + }); + + expect(container.textContent).toContain("Select a message to view details"); + }); + + it("shows loading state with spinner", () => { + const { container } = render(DetailPanel, { + message: null, + loading: true, + }); + + expect(container.textContent).toContain("Loading message content"); + }); + + it("fires onclose when close button clicked", async () => { + const onclose = vi.fn(); + const { container } = render(DetailPanel, { + message: sampleMessage, + onclose, + }); + + // Find close button in header (first button in the component) + const closeButton = container.querySelector(".ce-detail-panel__header button"); + if (closeButton) { + await fireEvent.click(closeButton); + expect(onclose).toHaveBeenCalled(); + } + }); + + it("shows message method and type", () => { + const { container } = render(DetailPanel, { + message: sampleMessage, + }); + + expect(container.textContent).toContain("tools/call"); + expect(container.textContent).toContain("request"); + }); + + it("displays message ID", () => { + const { container } = render(DetailPanel, { + message: sampleMessage, + }); + + expect(container.textContent).toContain("msg-12345"); + }); + + it("shows direction with icon", () => { + const { container } = render(DetailPanel, { + message: sampleMessage, + }); + + expect(container.textContent).toContain("inbound"); + }); +}); diff --git a/packages/ui/src/lib/composites/detail/DetailPanel/index.ts b/packages/ui/src/lib/composites/detail/DetailPanel/index.ts new file mode 100644 index 0000000..be4e2b8 --- /dev/null +++ b/packages/ui/src/lib/composites/detail/DetailPanel/index.ts @@ -0,0 +1,2 @@ +export { default as DetailPanel } from "./DetailPanel.svelte"; +export type { Message, Props as DetailPanelProps } from "./DetailPanel.svelte"; diff --git a/packages/ui/src/lib/composites/detail/ExpandCollapse/ExpandCollapse.stories.ts b/packages/ui/src/lib/composites/detail/ExpandCollapse/ExpandCollapse.stories.ts new file mode 100644 index 0000000..4f6457a --- /dev/null +++ b/packages/ui/src/lib/composites/detail/ExpandCollapse/ExpandCollapse.stories.ts @@ -0,0 +1,38 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import type { Component, ComponentProps } from "svelte"; +import ExpandCollapse from "./ExpandCollapse.svelte"; +import Wrapper from "./ExpandCollapse.story-wrapper.svelte"; + +const meta = { + title: "Composites/Detail/ExpandCollapse", + component: ExpandCollapse, + tags: ["autodocs"], + argTypes: { + label: { control: "text" }, + startExpanded: { control: "boolean" }, + }, +} satisfies Meta>; + +export default meta; +type Story = StoryObj>; + +const renderWrapper = (args: ComponentProps) => ({ + Component: Wrapper as unknown as Component, + props: args, +}); + +export const Default: Story = { + render: renderWrapper, + args: { + label: "Toggle Me", + startExpanded: false, + }, +}; + +export const Expanded: Story = { + render: renderWrapper, + args: { + label: "Initially Expanded", + startExpanded: true, + }, +}; diff --git a/packages/ui/src/lib/composites/detail/ExpandCollapse/ExpandCollapse.story-wrapper.svelte b/packages/ui/src/lib/composites/detail/ExpandCollapse/ExpandCollapse.story-wrapper.svelte new file mode 100644 index 0000000..cc93ebd --- /dev/null +++ b/packages/ui/src/lib/composites/detail/ExpandCollapse/ExpandCollapse.story-wrapper.svelte @@ -0,0 +1,36 @@ + + +
+ +
+

+ This content is revealed when expanded. +

+

+ It supports any HTML or components via snippet. +

+
+
+
diff --git a/packages/ui/src/lib/composites/detail/ExpandCollapse/ExpandCollapse.svelte b/packages/ui/src/lib/composites/detail/ExpandCollapse/ExpandCollapse.svelte new file mode 100644 index 0000000..03e5106 --- /dev/null +++ b/packages/ui/src/lib/composites/detail/ExpandCollapse/ExpandCollapse.svelte @@ -0,0 +1,115 @@ + + +
+ + + {#if expanded} +
+ {@render children()} +
+ {/if} +
+ + diff --git a/packages/ui/src/lib/composites/detail/ExpandCollapse/ExpandCollapse.test.ts b/packages/ui/src/lib/composites/detail/ExpandCollapse/ExpandCollapse.test.ts new file mode 100644 index 0000000..c03f467 --- /dev/null +++ b/packages/ui/src/lib/composites/detail/ExpandCollapse/ExpandCollapse.test.ts @@ -0,0 +1,48 @@ +import { render, fireEvent } from "@testing-library/svelte"; +import { describe, it, expect, jest, beforeAll } from "bun:test"; +import Wrapper from "./ExpandCollapse.story-wrapper.svelte"; + +// Mock Web Animations API which is missing in JSDOM/Bun environment +beforeAll(() => { + if (typeof Element !== 'undefined' && !Element.prototype.animate) { + Element.prototype.animate = () => ({ + finished: Promise.resolve(), + cancel: () => { }, + onfinish: null, + play: () => { }, + pause: () => { }, + reverse: () => { }, + }) as any; + } +}); + +describe("ExpandCollapse", () => { + it("renders label and toggles content", async () => { + const { getByText, queryByText } = render(Wrapper, { + label: "Click Me" + }); + + expect(getByText("Click Me")).toBeTruthy(); + expect(queryByText("This content is revealed when expanded.")).toBeNull(); + + const button = getByText("Click Me").closest("button"); + if (!button) throw new Error("Button not found"); + + await fireEvent.click(button); + expect(getByText("This content is revealed when expanded.")).toBeTruthy(); + + await fireEvent.click(button); + // Svelte transitions might delay the removal, but queryByText should catch it if not conditional or after wait + // Wait for removal if using transitions, or check for aria-expanded + expect(button.getAttribute("aria-expanded")).toBe("false"); + }); + + it("starts expanded if startExpanded prop is true", () => { + const { getByText } = render(Wrapper, { + label: "Open", + startExpanded: true + }); + + expect(getByText("This content is revealed when expanded.")).toBeTruthy(); + }); +}); diff --git a/packages/ui/src/lib/composites/detail/JSONInspector/JSONInspector.stories.ts b/packages/ui/src/lib/composites/detail/JSONInspector/JSONInspector.stories.ts new file mode 100644 index 0000000..80705b5 --- /dev/null +++ b/packages/ui/src/lib/composites/detail/JSONInspector/JSONInspector.stories.ts @@ -0,0 +1,125 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import JSONInspector from "./JSONInspector.svelte"; + +const meta = { + title: "Composites/Detail/JSONInspector", + component: JSONInspector, + tags: ["autodocs"], + argTypes: { + expandLevel: { control: { type: "number", min: 0, max: 10 } }, + maxDepth: { control: { type: "number", min: 1, max: 20 } }, + searchQuery: { control: "text" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const SimpleObject: Story = { + args: { + data: { + name: "John Doe", + age: 30, + active: true, + email: null, + }, + expandLevel: 2, + }, +}; + +export const DeepNesting: Story = { + args: { + data: { + level1: { + level2: { + level3: { + level4: { + level5: { + deeply: "nested", + }, + }, + }, + }, + }, + }, + expandLevel: 3, + maxDepth: 10, + }, +}; + +export const LargeArray: Story = { + args: { + data: { + tools: [ + { name: "list", count: 10 }, + { name: "call", count: 25 }, + { name: "read", count: 5 }, + { name: "write", count: 8 }, + { name: "delete", count: 2 }, + ], + }, + expandLevel: 2, + }, +}; + +export const WithSearch: Story = { + args: { + data: { + message: "Hello world", + response: { + status: "success", + data: { + greeting: "Hello there", + farewell: "Goodbye world", + }, + }, + }, + searchQuery: "world", + expandLevel: 5, + }, +}; + +export const AllCollapsed: Story = { + args: { + data: { + tools: [ + { name: "list" }, + { name: "call" }, + ], + config: { + timeout: 1000, + }, + }, + expandLevel: 0, + }, +}; + +export const AllExpanded: Story = { + args: { + data: { + tools: [ + { name: "list" }, + { name: "call" }, + ], + config: { + timeout: 1000, + }, + }, + expandLevel: 10, + }, +}; + +export const MixedTypes: Story = { + args: { + data: { + string: "text", + number: 42, + float: 3.14, + boolean: true, + null: null, + array: [1, 2, 3], + object: { nested: true }, + }, + expandLevel: 2, + }, +}; diff --git a/packages/ui/src/lib/composites/detail/JSONInspector/JSONInspector.svelte b/packages/ui/src/lib/composites/detail/JSONInspector/JSONInspector.svelte new file mode 100644 index 0000000..6de6810 --- /dev/null +++ b/packages/ui/src/lib/composites/detail/JSONInspector/JSONInspector.svelte @@ -0,0 +1,246 @@ + + + + + +{#snippet renderValue(value: unknown, path: string, depth: number)} + {@const valueType = getValueType(value)} + + {#if depth >= maxDepth} + ... + {:else if valueType === "object" || valueType === "array"} + {@const isArray = valueType === "array"} + {@const entries = getEntries(value, valueType)} + {@const isEmpty = entries.length === 0} + {@const initiallyExpanded = depth < expandLevel} + + {#if isEmpty} + {isArray ? "[]" : "{}"} + {:else} +
+ + + {isArray ? "[" : "{"} + {entries.length} {isArray ? "items" : "keys"} + +
+ {#each entries as [key, val]} + {@const childPath = buildChildPath(path, key, isArray)} +
+ + {#if searchQuery} + + {:else} + {key} + {/if} + + : + {@render renderValue(val, childPath, depth + 1)} + +
+ {/each} +
+ {isArray ? "]" : "}"} +
+ {/if} + {:else if valueType === "string"} + + "{#if searchQuery}{:else}{value}{/if}" + + {:else if valueType === "number"} + {value} + {:else if valueType === "boolean"} + {String(value)} + {:else if valueType === "null"} + null + {:else} + {String(value)} + {/if} +{/snippet} + +
+ {@render renderValue(data, "", 0)} +
+ + diff --git a/packages/ui/src/lib/composites/detail/JSONInspector/JSONInspector.test.ts b/packages/ui/src/lib/composites/detail/JSONInspector/JSONInspector.test.ts new file mode 100644 index 0000000..fe290df --- /dev/null +++ b/packages/ui/src/lib/composites/detail/JSONInspector/JSONInspector.test.ts @@ -0,0 +1,79 @@ +import { render, fireEvent } from "@testing-library/svelte"; +import { describe, it, expect, vi } from "bun:test"; +import JSONInspector from "./JSONInspector.svelte"; + +describe("JSONInspector", () => { + it("renders simple object", () => { + const { container, getByText } = render(JSONInspector, { + data: { name: "test", count: 42 }, + }); + + expect(getByText("name")).toBeTruthy(); + expect(getByText('"test"')).toBeTruthy(); + expect(getByText("42")).toBeTruthy(); + }); + + it("renders array with items", () => { + const { getByText } = render(JSONInspector, { + data: { items: [1, 2, 3] }, + expandLevel: 2, + }); + + expect(getByText("items")).toBeTruthy(); + expect(getByText("3 items")).toBeTruthy(); + }); + + it("syntax highlights by type", () => { + const { container } = render(JSONInspector, { + data: { + str: "hello", + num: 42, + bool: true, + nil: null, + }, + expandLevel: 2, + }); + + expect(container.querySelector(".ce-json-value--string")).toBeTruthy(); + expect(container.querySelector(".ce-json-value--number")).toBeTruthy(); + expect(container.querySelector(".ce-json-value--boolean")).toBeTruthy(); + expect(container.querySelector(".ce-json-value--null")).toBeTruthy(); + }); + + it("truncates at maxDepth", () => { + const { getByText } = render(JSONInspector, { + data: { + l1: { l2: { l3: { l4: "deep" } } }, + }, + expandLevel: 10, + maxDepth: 2, + }); + + expect(getByText("...")).toBeTruthy(); + }); + + it("calls oncopypath when copy button clicked", async () => { + const oncopypath = vi.fn(); + const { container } = render(JSONInspector, { + data: { name: "test" }, + expandLevel: 2, + oncopypath, + }); + + const copyButton = container.querySelector(".ce-json-copy-path"); + if (copyButton) { + await fireEvent.click(copyButton); + expect(oncopypath).toHaveBeenCalledWith("name"); + } + }); + + it("renders empty object/array correctly", () => { + const { getByText } = render(JSONInspector, { + data: { empty: {}, arr: [] }, + expandLevel: 2, + }); + + expect(getByText("{}")).toBeTruthy(); + expect(getByText("[]")).toBeTruthy(); + }); +}); diff --git a/packages/ui/src/lib/composites/detail/JSONInspector/index.ts b/packages/ui/src/lib/composites/detail/JSONInspector/index.ts new file mode 100644 index 0000000..905d25b --- /dev/null +++ b/packages/ui/src/lib/composites/detail/JSONInspector/index.ts @@ -0,0 +1,2 @@ +export { default as JSONInspector } from "./JSONInspector.svelte"; +export type { Props as JSONInspectorProps } from "./JSONInspector.svelte"; diff --git a/packages/ui/src/lib/composites/detail/ToolCard/ToolCard.stories.ts b/packages/ui/src/lib/composites/detail/ToolCard/ToolCard.stories.ts new file mode 100644 index 0000000..caee811 --- /dev/null +++ b/packages/ui/src/lib/composites/detail/ToolCard/ToolCard.stories.ts @@ -0,0 +1,59 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import ToolCard from "./ToolCard.svelte"; + +const meta = { + title: "Composites/Detail/ToolCard", + component: ToolCard, + tags: ["autodocs"], + argTypes: { + onclick: { action: "clicked" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + tool: { + name: "get_weather", + description: "Get the current weather for a specified location", + }, + }, +}; + +export const WithAnnotations: Story = { + args: { + tool: { + name: "delete_file", + description: "Delete a file from the filesystem. This action cannot be undone.", + annotations: { + destructiveHint: true, + readOnlyHint: false, + openWorldHint: false, + }, + }, + }, +}; + +export const FullAnnotations: Story = { + args: { + tool: { + name: "complex_operation", + description: "A tool with multiple hints and a very long description that should be truncated by the card layout to prevent layout shifts or overflows in narrow containers.", + annotations: { + destructiveHint: true, + readOnlyHint: true, + openWorldHint: true, + }, + }, + }, +}; + +export const NoDescription: Story = { + args: { + tool: { + name: "ping_service", + }, + }, +}; diff --git a/packages/ui/src/lib/composites/detail/ToolCard/ToolCard.svelte b/packages/ui/src/lib/composites/detail/ToolCard/ToolCard.svelte new file mode 100644 index 0000000..f06be4d --- /dev/null +++ b/packages/ui/src/lib/composites/detail/ToolCard/ToolCard.svelte @@ -0,0 +1,129 @@ + + + + + + + diff --git a/packages/ui/src/lib/composites/detail/ToolCard/ToolCard.test.ts b/packages/ui/src/lib/composites/detail/ToolCard/ToolCard.test.ts new file mode 100644 index 0000000..4049867 --- /dev/null +++ b/packages/ui/src/lib/composites/detail/ToolCard/ToolCard.test.ts @@ -0,0 +1,49 @@ +import { render, fireEvent } from "@testing-library/svelte"; +import { describe, it, expect, jest } from "bun:test"; +import ToolCard from "./ToolCard.svelte"; + +describe("ToolCard", () => { + const mockTool = { + name: "test_tool", + description: "A test tool description", + annotations: { + destructiveHint: true, + readOnlyHint: true + } + }; + + it("renders tool name and description", () => { + const { getByText } = render(ToolCard, { tool: mockTool }); + + expect(getByText("test_tool")).toBeTruthy(); + expect(getByText("A test tool description")).toBeTruthy(); + }); + + it("renders annotation badges", () => { + const { getByText } = render(ToolCard, { tool: mockTool }); + + expect(getByText("destructive")).toBeTruthy(); + expect(getByText("read-only")).toBeTruthy(); + }); + + it("triggers onclick when clicked", async () => { + const onclick = jest.fn(); + const { getByRole } = render(ToolCard, { + tool: mockTool, + onclick + }); + + const button = getByRole("button"); + await fireEvent.click(button); + + expect(onclick).toHaveBeenCalled(); + }); + + it("handles missing description and annotations", () => { + const minimalTool = { name: "minimal" }; + const { getByText, queryByText } = render(ToolCard, { tool: minimalTool }); + + expect(getByText("minimal")).toBeTruthy(); + expect(queryByText("destructive")).toBeNull(); + }); +}); diff --git a/packages/ui/src/lib/composites/messages/LiveUpdateMarker/LiveUpdateMarker.stories.ts b/packages/ui/src/lib/composites/messages/LiveUpdateMarker/LiveUpdateMarker.stories.ts new file mode 100644 index 0000000..804f208 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/LiveUpdateMarker/LiveUpdateMarker.stories.ts @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import LiveUpdateMarker from "./LiveUpdateMarker.svelte"; + +const meta = { + title: "Composites/Messages/LiveUpdateMarker", + component: LiveUpdateMarker, + tags: ["autodocs"], + argTypes: { + count: { control: "number" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { count: 5 }, +}; + +export const ManyMessages: Story = { + args: { count: 42 }, +}; + +export const Hidden: Story = { + args: { count: 0 }, +}; diff --git a/packages/ui/src/lib/composites/messages/LiveUpdateMarker/LiveUpdateMarker.svelte b/packages/ui/src/lib/composites/messages/LiveUpdateMarker/LiveUpdateMarker.svelte new file mode 100644 index 0000000..4cd4e39 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/LiveUpdateMarker/LiveUpdateMarker.svelte @@ -0,0 +1,47 @@ + + + + +{#if count > 0} +
+ +
+{/if} + + diff --git a/packages/ui/src/lib/composites/messages/LiveUpdateMarker/LiveUpdateMarker.test.ts b/packages/ui/src/lib/composites/messages/LiveUpdateMarker/LiveUpdateMarker.test.ts new file mode 100644 index 0000000..3434883 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/LiveUpdateMarker/LiveUpdateMarker.test.ts @@ -0,0 +1,25 @@ +import { render, fireEvent } from "@testing-library/svelte"; +import { describe, it, expect, vi } from "bun:test"; +import LiveUpdateMarker from "./LiveUpdateMarker.svelte"; + +describe("LiveUpdateMarker", () => { + it("renders when count > 0", () => { + const { getByText } = render(LiveUpdateMarker, { count: 5 }); + expect(getByText("5 new")).toBeTruthy(); + }); + + it("does not render when count is 0", () => { + const { container } = render(LiveUpdateMarker, { count: 0 }); + const marker = container.querySelector(".ce-live-update-marker"); + expect(marker).toBeNull(); + }); + + it("calls onClick when clicked", async () => { + const onClick = vi.fn(); + const { container } = render(LiveUpdateMarker, { count: 3, onClick }); + + const btn = container.querySelector(".ce-button--primary"); + await fireEvent.click(btn!); + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/packages/ui/src/lib/composites/messages/MessageRow/MessageRow.stories.ts b/packages/ui/src/lib/composites/messages/MessageRow/MessageRow.stories.ts new file mode 100644 index 0000000..f1b7fd7 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/MessageRow/MessageRow.stories.ts @@ -0,0 +1,93 @@ +import type { Meta, StoryObj } from '@storybook/svelte'; +import MessageRow from './MessageRow.svelte'; +import SelectionWrapper from './MessageRow.story-wrapper.svelte'; + +const meta = { + title: 'Composites/Messages/MessageRow', + component: MessageRow, + tags: ['autodocs'], + argTypes: { + direction: { + control: 'select', + options: ['inbound', 'outbound'] + }, + type: { + control: 'select', + options: ['request', 'response', 'notification'] + }, + selected: { control: 'boolean' }, + hasResponse: { control: 'boolean' }, + timestamp: { control: 'date' } + } +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// ... existing stories ... +export const InboundRequest: Story = { + args: { + id: '1', + direction: 'inbound', + type: 'request', + method: 'mcp.prompts.list', + preview: '{ "filter": "technical" }', + timestamp: new Date(), + hasResponse: false, + selected: false + } +}; + +export const OutboundResponse: Story = { + args: { + id: '2', + direction: 'outbound', + type: 'response', + method: 'mcp.prompts.list', + preview: '{ "prompts": [ ... ] }', + timestamp: new Date(Date.now() + 1000), + hasResponse: true, + selected: false + } +}; + +export const Notification: Story = { + args: { + id: '3', + direction: 'inbound', + type: 'notification', + method: 'notifications/initialized', + preview: 'Server is ready', + timestamp: new Date(Date.now() + 5000), + hasResponse: false, + selected: false + } +}; + +export const Selected: Story = { + args: { + ...InboundRequest.args, + id: '4', + selected: true + } +}; + +export const LongPreview: Story = { + args: { + id: '5', + direction: 'inbound', + type: 'request', + method: 'mcp.resources.read', + preview: 'This is a very long preview message that should be truncated because it exceeds the available width of the component container in a real scenario.', + timestamp: new Date(), + hasResponse: false, + selected: false + } +}; + +export const SelectionDemo: Story = { + args: {} as any, + render: () => ({ + Component: SelectionWrapper + }) +}; diff --git a/packages/ui/src/lib/composites/messages/MessageRow/MessageRow.story-wrapper.svelte b/packages/ui/src/lib/composites/messages/MessageRow/MessageRow.story-wrapper.svelte new file mode 100644 index 0000000..2dd5dd4 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/MessageRow/MessageRow.story-wrapper.svelte @@ -0,0 +1,96 @@ + + +
+
+ Selected ID: {selectedId} +
Click a row to select it
+
+ +
+ {#each messages as msg (msg.id)} + + {/each} +
+
+ + diff --git a/packages/ui/src/lib/composites/messages/MessageRow/MessageRow.svelte b/packages/ui/src/lib/composites/messages/MessageRow/MessageRow.svelte new file mode 100644 index 0000000..6e17c04 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/MessageRow/MessageRow.svelte @@ -0,0 +1,202 @@ + + + + +
onClick?.(id)} +> +
+
+ +
+
+ +
+
+ {method} +
+ {#if hasResponse} + +
+ {/if} + {type} + {formatTime(timestamp)} +
+
+
+ {preview} +
+
+
+ + diff --git a/packages/ui/src/lib/composites/messages/MessageRow/MessageRow.test.ts b/packages/ui/src/lib/composites/messages/MessageRow/MessageRow.test.ts new file mode 100644 index 0000000..5e316b0 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/MessageRow/MessageRow.test.ts @@ -0,0 +1,51 @@ +import { render, screen, fireEvent } from "@testing-library/svelte"; +import { describe, expect, it, vi } from "bun:test"; +import MessageRow from "./MessageRow.svelte"; + +describe("MessageRow", () => { + const defaultProps = { + id: "msg-123", + direction: "inbound" as const, + type: "request" as const, + method: "test.method", + preview: "Test preview content", + timestamp: new Date("2024-01-01T12:00:00") + }; + + it("renders method and preview", () => { + render(MessageRow, defaultProps); + expect(screen.getByText("test.method")).toBeTruthy(); + expect(screen.getByText("Test preview content")).toBeTruthy(); + }); + + it("renders timestamp correctly", () => { + render(MessageRow, defaultProps); + // Assuming implementation uses toLocaleTimeString + // Checking for partial match since locale might vary in specific environment + expect(screen.getByText(/12:00:00/)).toBeTruthy(); + }); + + it("shows direction badge with correct text", () => { + render(MessageRow, defaultProps); + // Badge content is 'request' + const badges = screen.getAllByText("request"); + expect(badges.length).toBeGreaterThan(0); + }); + + it("applies selected class when selected prop is true", () => { + const { container } = render(MessageRow, { ...defaultProps, selected: true }); + const row = container.querySelector(".ce-message-row--selected"); + expect(row).toBeTruthy(); + }); + + it("fires onClick handler with id when clicked", async () => { + const onClick = vi.fn(); + render(MessageRow, { ...defaultProps, onClick }); + + const row = screen.getByText("test.method").closest(".ce-message-row"); + expect(row).toBeTruthy(); + + await fireEvent.click(row!); + expect(onClick).toHaveBeenCalledWith("msg-123"); + }); +}); diff --git a/packages/ui/src/lib/composites/messages/PairIndicator/PairIndicator.stories.ts b/packages/ui/src/lib/composites/messages/PairIndicator/PairIndicator.stories.ts new file mode 100644 index 0000000..cff0b23 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/PairIndicator/PairIndicator.stories.ts @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import PairIndicator from "./PairIndicator.svelte"; + +const meta = { + title: "Composites/Messages/PairIndicator", + component: PairIndicator, + tags: ["autodocs"], + argTypes: { + paired: { control: "boolean" }, + pending: { control: "boolean" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Paired: Story = { + args: { paired: true }, +}; + +export const Unpaired: Story = { + args: { paired: false }, +}; + +export const Pending: Story = { + args: { pending: true }, +}; diff --git a/packages/ui/src/lib/composites/messages/PairIndicator/PairIndicator.svelte b/packages/ui/src/lib/composites/messages/PairIndicator/PairIndicator.svelte new file mode 100644 index 0000000..b1a0890 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/PairIndicator/PairIndicator.svelte @@ -0,0 +1,52 @@ + + + + + + {#if pending} + + {:else if paired} + + {:else} + + {/if} + + + diff --git a/packages/ui/src/lib/composites/messages/PairIndicator/PairIndicator.test.ts b/packages/ui/src/lib/composites/messages/PairIndicator/PairIndicator.test.ts new file mode 100644 index 0000000..070eae1 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/PairIndicator/PairIndicator.test.ts @@ -0,0 +1,23 @@ +import { render } from "@testing-library/svelte"; +import { describe, it, expect } from "bun:test"; +import PairIndicator from "./PairIndicator.svelte"; + +describe("PairIndicator", () => { + it("renders paired state", () => { + const { container } = render(PairIndicator, { paired: true }); + const paired = container.querySelector(".ce-pair-indicator--paired"); + expect(paired).toBeTruthy(); + }); + + it("renders unpaired state", () => { + const { container } = render(PairIndicator, { paired: false }); + const unpaired = container.querySelector(".ce-pair-indicator--unpaired"); + expect(unpaired).toBeTruthy(); + }); + + it("renders pending spinner", () => { + const { container } = render(PairIndicator, { pending: true }); + const spinner = container.querySelector(".ce-spinner"); + expect(spinner).toBeTruthy(); + }); +}); diff --git a/packages/ui/src/lib/composites/messages/ScrollToLatest/ScrollToLatest.stories.ts b/packages/ui/src/lib/composites/messages/ScrollToLatest/ScrollToLatest.stories.ts new file mode 100644 index 0000000..04577a4 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/ScrollToLatest/ScrollToLatest.stories.ts @@ -0,0 +1,22 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import ScrollToLatest from "./ScrollToLatest.svelte"; + +const meta = { + title: "Composites/Messages/ScrollToLatest", + component: ScrollToLatest, + tags: ["autodocs"], + argTypes: { + active: { control: "boolean" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { active: false }, +}; + +export const AutoScrollActive: Story = { + args: { active: true }, +}; diff --git a/packages/ui/src/lib/composites/messages/ScrollToLatest/ScrollToLatest.svelte b/packages/ui/src/lib/composites/messages/ScrollToLatest/ScrollToLatest.svelte new file mode 100644 index 0000000..a5092bb --- /dev/null +++ b/packages/ui/src/lib/composites/messages/ScrollToLatest/ScrollToLatest.svelte @@ -0,0 +1,59 @@ + + + + +
+ + +
+ + Auto-scroll +
+
+ + diff --git a/packages/ui/src/lib/composites/messages/ScrollToLatest/ScrollToLatest.test.ts b/packages/ui/src/lib/composites/messages/ScrollToLatest/ScrollToLatest.test.ts new file mode 100644 index 0000000..d266695 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/ScrollToLatest/ScrollToLatest.test.ts @@ -0,0 +1,19 @@ +import { render, fireEvent } from "@testing-library/svelte"; +import { describe, it, expect, vi } from "bun:test"; +import ScrollToLatest from "./ScrollToLatest.svelte"; + +describe("ScrollToLatest", () => { + it("renders auto-scroll label", () => { + const { getByText } = render(ScrollToLatest); + expect(getByText("Auto-scroll")).toBeTruthy(); + }); + + it("calls onScrollToBottom when scroll button clicked", async () => { + const onScrollToBottom = vi.fn(); + const { container } = render(ScrollToLatest, { onScrollToBottom }); + + const scrollBtn = container.querySelector("[aria-label='Scroll to bottom']"); + await fireEvent.click(scrollBtn!); + expect(onScrollToBottom).toHaveBeenCalled(); + }); +}); diff --git a/packages/ui/src/lib/composites/messages/TrafficList/MockVirtualList.svelte b/packages/ui/src/lib/composites/messages/TrafficList/MockVirtualList.svelte new file mode 100644 index 0000000..f45e444 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/TrafficList/MockVirtualList.svelte @@ -0,0 +1,15 @@ + + +
+ {#each items as item, index} +
+ {@render children(item, index)} +
+ {/each} +
diff --git a/packages/ui/src/lib/composites/messages/TrafficList/TrafficList.stories.ts b/packages/ui/src/lib/composites/messages/TrafficList/TrafficList.stories.ts new file mode 100644 index 0000000..6c0f671 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/TrafficList/TrafficList.stories.ts @@ -0,0 +1,76 @@ +import type { Meta, StoryObj } from '@storybook/svelte'; +import TrafficList from './TrafficList.svelte'; +import type { Message } from './TrafficList.svelte'; + +import TrafficListWrapper from './TrafficList.story-wrapper.svelte'; + +const meta = { + title: 'Composites/Messages/TrafficList', + component: TrafficList, + tags: ['autodocs'], + argTypes: { + selectedId: { control: 'text' }, + loading: { control: 'boolean' }, + autoScroll: { control: 'boolean' }, + atBottom: { control: 'boolean' }, + }, + render: (args) => ({ + Component: TrafficListWrapper, + props: args + }), + parameters: { + layout: 'fullscreen', + } +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Generate sample messages +const generateMessages = (count: number): Message[] => { + return Array.from({ length: count }, (_, i) => ({ + id: `msg-${i}`, + direction: i % 2 === 0 ? 'inbound' : 'outbound', + type: i % 3 === 0 ? 'notification' : (i % 2 === 0 ? 'request' : 'response'), + method: `method.test.${i}`, + preview: `Message preview content ${i}`, + timestamp: new Date(Date.now() - (count - i) * 60000), + hasResponse: i % 4 === 0 + })); +}; + +export const Default: Story = { + args: { + messages: generateMessages(20), + autoScroll: true, + atBottom: true + } +}; + +export const Empty: Story = { + args: { + messages: [] + } +}; + +export const Loading: Story = { + args: { + messages: [], + loading: true + } +}; + +export const ManyMessages: Story = { + args: { + messages: generateMessages(1000), // 1000 items to test virtualization + autoScroll: true, + atBottom: true + } +}; + +export const WithSelection: Story = { + args: { + messages: generateMessages(20), + selectedId: 'msg-10' + } +}; diff --git a/packages/ui/src/lib/composites/messages/TrafficList/TrafficList.story-wrapper.svelte b/packages/ui/src/lib/composites/messages/TrafficList/TrafficList.story-wrapper.svelte new file mode 100644 index 0000000..4b1e5f6 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/TrafficList/TrafficList.story-wrapper.svelte @@ -0,0 +1,11 @@ + + +
+ +
diff --git a/packages/ui/src/lib/composites/messages/TrafficList/TrafficList.svelte b/packages/ui/src/lib/composites/messages/TrafficList/TrafficList.svelte new file mode 100644 index 0000000..a0d35e1 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/TrafficList/TrafficList.svelte @@ -0,0 +1,184 @@ + + +
+ {#if messages.length === 0 && !loading} +
+ +
+ {:else} +
+ + {#snippet children(message, index)} + + {/snippet} + +
+ {/if} + + +
+ {#if !atBottom || !autoScroll} +
+ +
+ {/if} + + onAutoScrollChange?.(!autoScroll)} + showButton={!atBottom} + /> +
+
+ + diff --git a/packages/ui/src/lib/composites/messages/TrafficList/TrafficList.test.ts b/packages/ui/src/lib/composites/messages/TrafficList/TrafficList.test.ts new file mode 100644 index 0000000..ee72f04 --- /dev/null +++ b/packages/ui/src/lib/composites/messages/TrafficList/TrafficList.test.ts @@ -0,0 +1,36 @@ +import { render, screen } from "@testing-library/svelte"; +import { describe, expect, it, vi } from "bun:test"; +import TrafficList, { type Message } from "./TrafficList.svelte"; + +import MockVirtualList from "./MockVirtualList.svelte"; + +// Mock child components to isolate TrafficList logic +vi.mock("../../cross-cutting/VirtualList/VirtualList.svelte", () => ({ + default: MockVirtualList +})); + +describe("TrafficList", () => { + const mockMessages: Message[] = [ + { id: "1", direction: "inbound", type: "request", method: "test", preview: "test", timestamp: new Date() }, + { id: "2", direction: "outbound", type: "response", method: "test", preview: "test", timestamp: new Date() } + ]; + + it("renders empty state when no messages", () => { + render(TrafficList, { messages: [] }); + expect(screen.getByText("No messages yet")).toBeTruthy(); + }); + + it("renders VirtualList when messages exist", () => { + // Since we mocked VirtualList but svelte testing library renders real components usually, + // we might rely on the fact that VirtualList is used. + // However, standard testing of composition in Svelte 5 is tricky without shallow render. + // We'll check if content div exists. + const { container } = render(TrafficList, { messages: mockMessages }); + expect(container.querySelector(".ce-traffic-list__content")).toBeTruthy(); + }); + + it("renders controls", () => { + const { container } = render(TrafficList, { messages: mockMessages }); + expect(container.querySelector(".ce-traffic-list__controls")).toBeTruthy(); + }); +}); diff --git a/packages/ui/src/lib/composites/session/ConnectionStatus/ConnectionStatus.stories.ts b/packages/ui/src/lib/composites/session/ConnectionStatus/ConnectionStatus.stories.ts new file mode 100644 index 0000000..c2592e5 --- /dev/null +++ b/packages/ui/src/lib/composites/session/ConnectionStatus/ConnectionStatus.stories.ts @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import ConnectionStatus from "./ConnectionStatus.svelte"; + +const meta = { + title: "Composites/Session/ConnectionStatus", + component: ConnectionStatus, + tags: ["autodocs"], + argTypes: { + state: { + control: "select", + options: ["disconnected", "connecting", "connected", "reconnecting", "error"], + }, + compact: { control: "boolean" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Connected: Story = { + args: { + state: "connected", + }, +}; + +export const Connecting: Story = { + args: { + state: "connecting", + }, +}; + +export const Reconnecting: Story = { + args: { + state: "reconnecting", + }, +}; + +export const Disconnected: Story = { + args: { + state: "disconnected", + }, +}; + +export const Error: Story = { + args: { + state: "error", + }, +}; + +export const Compact: Story = { + args: { + state: "connected", + compact: true, + }, +}; diff --git a/packages/ui/src/lib/composites/session/ConnectionStatus/ConnectionStatus.svelte b/packages/ui/src/lib/composites/session/ConnectionStatus/ConnectionStatus.svelte new file mode 100644 index 0000000..48be570 --- /dev/null +++ b/packages/ui/src/lib/composites/session/ConnectionStatus/ConnectionStatus.svelte @@ -0,0 +1,119 @@ + + + + +{#snippet indicator()} +
+ {#if stateConfig.loading} + + {:else} + + {/if} +
+{/snippet} + +
+ {#if compact} + + {@render indicator()} + + {:else} + {@render indicator()} + + {stateConfig.label} + + {/if} +
+ + diff --git a/packages/ui/src/lib/composites/session/ConnectionStatus/ConnectionStatus.test.ts b/packages/ui/src/lib/composites/session/ConnectionStatus/ConnectionStatus.test.ts new file mode 100644 index 0000000..a19530b --- /dev/null +++ b/packages/ui/src/lib/composites/session/ConnectionStatus/ConnectionStatus.test.ts @@ -0,0 +1,35 @@ +import { render } from "@testing-library/svelte"; +import { describe, it, expect } from "bun:test"; +import ConnectionStatus from "./ConnectionStatus.svelte"; + +describe("ConnectionStatus", () => { + it("renders connected state", () => { + const { getByText } = render(ConnectionStatus, { state: "connected" }); + expect(getByText("Connected")).toBeTruthy(); + }); + + it("renders connecting state", () => { + const { getByText } = render(ConnectionStatus, { state: "connecting" }); + expect(getByText("Connecting...")).toBeTruthy(); + }); + + it("renders error state", () => { + const { getByText } = render(ConnectionStatus, { state: "error" }); + expect(getByText("Connection Failed")).toBeTruthy(); + }); + + it("renders compact mode without label", () => { + const { container } = render(ConnectionStatus, { + state: "connected", + compact: true + }); + const label = container.querySelector(".ce-connection-status__label"); + expect(label).toBeNull(); + }); + + it("has correct accessibility role", () => { + const { container } = render(ConnectionStatus, { state: "connected" }); + const status = container.querySelector("[role='status']"); + expect(status).toBeTruthy(); + }); +}); diff --git a/packages/ui/src/lib/composites/session/CreateSessionForm/CreateSessionForm.stories.ts b/packages/ui/src/lib/composites/session/CreateSessionForm/CreateSessionForm.stories.ts new file mode 100644 index 0000000..eedf5b9 --- /dev/null +++ b/packages/ui/src/lib/composites/session/CreateSessionForm/CreateSessionForm.stories.ts @@ -0,0 +1,31 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import CreateSessionForm from "./CreateSessionForm.svelte"; + +const meta = { + title: "Composites/Session/CreateSessionForm", + component: CreateSessionForm, + tags: ["autodocs"], + argTypes: { + loading: { control: "boolean" }, + error: { control: "text" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; + +export const Loading: Story = { + args: { + loading: true, + }, +}; + +export const WithError: Story = { + args: { + error: "Failed to connect to server. Please check the command and try again.", + }, +}; diff --git a/packages/ui/src/lib/composites/session/CreateSessionForm/CreateSessionForm.svelte b/packages/ui/src/lib/composites/session/CreateSessionForm/CreateSessionForm.svelte new file mode 100644 index 0000000..410a198 --- /dev/null +++ b/packages/ui/src/lib/composites/session/CreateSessionForm/CreateSessionForm.svelte @@ -0,0 +1,155 @@ + + + + +
+

Create Session

+ + {#if error} + + {/if} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + diff --git a/packages/ui/src/lib/composites/session/CreateSessionForm/CreateSessionForm.test.ts b/packages/ui/src/lib/composites/session/CreateSessionForm/CreateSessionForm.test.ts new file mode 100644 index 0000000..dd22ad1 --- /dev/null +++ b/packages/ui/src/lib/composites/session/CreateSessionForm/CreateSessionForm.test.ts @@ -0,0 +1,30 @@ +import { render, fireEvent } from "@testing-library/svelte"; +import { describe, it, expect, vi } from "bun:test"; +import CreateSessionForm from "./CreateSessionForm.svelte"; + +describe("CreateSessionForm", () => { + it("renders form title", () => { + const { getByText } = render(CreateSessionForm); + expect(getByText("Create Session")).toBeTruthy(); + }); + + it("renders error message when provided", () => { + const { getByText } = render(CreateSessionForm, { error: "Connection failed" }); + expect(getByText("Connection failed")).toBeTruthy(); + }); + + it("disables inputs when loading", () => { + const { container } = render(CreateSessionForm, { loading: true }); + const inputs = container.querySelectorAll("input:disabled"); + expect(inputs.length).toBeGreaterThan(0); + }); + + it("calls onCancel when cancel button is clicked", async () => { + const onCancel = vi.fn(); + const { container } = render(CreateSessionForm, { onCancel }); + + const cancelBtn = container.querySelector(".ce-button--ghost"); + await fireEvent.click(cancelBtn!); + expect(onCancel).toHaveBeenCalled(); + }); +}); diff --git a/packages/ui/src/lib/composites/session/SessionCard/SessionCard.stories.ts b/packages/ui/src/lib/composites/session/SessionCard/SessionCard.stories.ts new file mode 100644 index 0000000..86c4438 --- /dev/null +++ b/packages/ui/src/lib/composites/session/SessionCard/SessionCard.stories.ts @@ -0,0 +1,54 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import SessionCard from "./SessionCard.svelte"; + +const meta = { + title: "Composites/Session/SessionCard", + component: SessionCard, + tags: ["autodocs"], + argTypes: { + selected: { control: "boolean" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const baseSession = { + id: "session-1", + name: "MCP Tools Session", + status: "active" as const, + messageCount: 24, + createdAt: new Date(), + serverCommand: "tools/call", +}; + +export const Active: Story = { + args: { + session: baseSession, + }, +}; + +export const Selected: Story = { + args: { + session: baseSession, + selected: true, + }, +}; + +export const Pending: Story = { + args: { + session: { ...baseSession, status: "pending" as const }, + }, +}; + +export const Error: Story = { + args: { + session: { ...baseSession, status: "error" as const }, + }, +}; + +export const Disconnected: Story = { + args: { + session: { ...baseSession, status: "disconnected" as const }, + }, +}; diff --git a/packages/ui/src/lib/composites/session/SessionCard/SessionCard.svelte b/packages/ui/src/lib/composites/session/SessionCard/SessionCard.svelte new file mode 100644 index 0000000..4e96f29 --- /dev/null +++ b/packages/ui/src/lib/composites/session/SessionCard/SessionCard.svelte @@ -0,0 +1,167 @@ + + + + + + + diff --git a/packages/ui/src/lib/composites/session/SessionCard/SessionCard.test.ts b/packages/ui/src/lib/composites/session/SessionCard/SessionCard.test.ts new file mode 100644 index 0000000..505e326 --- /dev/null +++ b/packages/ui/src/lib/composites/session/SessionCard/SessionCard.test.ts @@ -0,0 +1,41 @@ +import { render, fireEvent } from "@testing-library/svelte"; +import { describe, it, expect, vi } from "bun:test"; +import SessionCard from "./SessionCard.svelte"; + +const baseSession = { + id: "session-1", + name: "Test Session", + status: "active" as const, + messageCount: 10, + createdAt: new Date("2026-01-21T12:00:00"), + serverCommand: "tools/call", +}; + +describe("SessionCard", () => { + it("renders session name", () => { + const { getByText } = render(SessionCard, { session: baseSession }); + expect(getByText("Test Session")).toBeTruthy(); + }); + + it("renders message count", () => { + const { container } = render(SessionCard, { session: baseSession }); + const badge = container.querySelector(".badge--info"); + expect(badge?.textContent).toContain("10 messages"); + }); + + it("applies selected style when selected", () => { + const { container } = render(SessionCard, { session: baseSession, selected: true }); + const card = container.querySelector(".ce-session-card--selected"); + expect(card).toBeTruthy(); + }); + + it("calls onClick when clicked", async () => { + const onClick = vi.fn(); + const { container } = render(SessionCard, { session: baseSession, onClick }); + + const button = container.querySelector(".ce-session-card"); + await fireEvent.click(button!); + + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/packages/ui/src/lib/composites/session/SessionList/SessionList.stories.ts b/packages/ui/src/lib/composites/session/SessionList/SessionList.stories.ts new file mode 100644 index 0000000..34e0807 --- /dev/null +++ b/packages/ui/src/lib/composites/session/SessionList/SessionList.stories.ts @@ -0,0 +1,80 @@ +import type { Meta, StoryObj } from '@storybook/svelte'; +import SessionList from './SessionList.svelte'; +import type { Session } from '../SessionCard/SessionCard.svelte'; + +const mockSessions: Session[] = [ + { + id: '1', + name: 'Echo Server', + status: 'active', + messageCount: 42, + createdAt: new Date(), + serverCommand: 'npx @modelcontextprotocol/server-echo' + }, + { + id: '2', + name: 'Memory Server', + status: 'connected', + messageCount: 12, + createdAt: new Date(Date.now() - 3600000), + serverCommand: 'npx @modelcontextprotocol/server-memory' + }, + { + id: '3', + name: 'Failed Server', + status: 'error', + messageCount: 0, + createdAt: new Date(Date.now() - 7200000), + serverCommand: 'cat /dev/null' + }, + { + id: '4', + name: 'Disconnected', + status: 'disconnected', + messageCount: 150, + createdAt: new Date(Date.now() - 86400000), + serverCommand: 'npx server-something' + } +]; + +const meta = { + title: 'Composites/Session/SessionList', + component: SessionList, + tags: ['autodocs'], + argTypes: { + loading: { control: 'boolean' }, + selectedId: { control: 'text' }, + searchQuery: { control: 'text' } + } +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + sessions: mockSessions, + selectedId: '1' + } +}; + +export const Empty: Story = { + args: { + sessions: [], + searchQuery: '' + } +}; + +export const NoResults: Story = { + args: { + sessions: mockSessions, + searchQuery: 'something that does not exist' + } +}; + +export const Loading: Story = { + args: { + sessions: [], + loading: true + } +}; diff --git a/packages/ui/src/lib/composites/session/SessionList/SessionList.svelte b/packages/ui/src/lib/composites/session/SessionList/SessionList.svelte new file mode 100644 index 0000000..4fb3654 --- /dev/null +++ b/packages/ui/src/lib/composites/session/SessionList/SessionList.svelte @@ -0,0 +1,145 @@ + + + + +
+
+

Sessions

+
+ + + +
+ {#if filteredSessions.length > 0} +
+ {#each filteredSessions as session (session.id)} + onSelect?.(session.id)} + /> + {/each} +
+ {:else if !loading} +
+ +
+ {/if} +
+
+ + diff --git a/packages/ui/src/lib/composites/session/SessionList/SessionList.test.ts b/packages/ui/src/lib/composites/session/SessionList/SessionList.test.ts new file mode 100644 index 0000000..f5ff393 --- /dev/null +++ b/packages/ui/src/lib/composites/session/SessionList/SessionList.test.ts @@ -0,0 +1,78 @@ +import { fireEvent, render, screen } from "@testing-library/svelte"; +import { describe, expect, it, vi } from "bun:test"; +import SessionList from "./SessionList.svelte"; +import type { Session } from "../SessionCard/SessionCard.svelte"; + +const mockSessions: Session[] = [ + { + id: "1", + name: "Echo Server", + status: "active", + messageCount: 42, + createdAt: new Date(), + serverCommand: "npx echo" + }, + { + id: "2", + name: "Memory Server", + status: "connected", + messageCount: 12, + createdAt: new Date(), + serverCommand: "npx memory" + } +]; + +describe("SessionList", () => { + it("renders title and session items", () => { + render(SessionList, { sessions: mockSessions }); + expect(screen.getByText("Sessions")).toBeTruthy(); + expect(screen.getByText("Echo Server")).toBeTruthy(); + expect(screen.getByText("Memory Server")).toBeTruthy(); + }); + + it("filters sessions based on search query", () => { + const { component } = render(SessionList, { + sessions: mockSessions, + searchQuery: "Echo" + }); + + expect(screen.getByText("Echo Server")).toBeTruthy(); + expect(screen.queryByText("Memory Server")).toBeNull(); + }); + + it("calls onSelect when a session card is clicked", async () => { + const onSelect = vi.fn(); + render(SessionList, { sessions: mockSessions, onSelect }); + + const card = screen.getByText("Echo Server"); + await fireEvent.click(card); + + expect(onSelect).toHaveBeenCalledWith("1"); + }); + + it("calls onAddSession when add button is clicked", async () => { + const onAddSession = vi.fn(); + render(SessionList, { sessions: mockSessions, onAddSession }); + + const addButton = screen.getByLabelText("Add Session"); + await fireEvent.click(addButton); + + expect(onAddSession).toHaveBeenCalled(); + }); + + it("shows empty state when no sessions match search", () => { + render(SessionList, { + sessions: mockSessions, + searchQuery: "NonExistent" + }); + + expect(screen.getByText("No results found")).toBeTruthy(); + expect(screen.getByText('No sessions matching "NonExistent"')).toBeTruthy(); + }); + + it("shows basic empty state when sessions array is empty", () => { + render(SessionList, { sessions: [] }); + + expect(screen.getByText("No sessions yet")).toBeTruthy(); + }); +}); diff --git a/packages/ui/src/lib/example.test.ts b/packages/ui/src/lib/example.test.ts new file mode 100644 index 0000000..a24c169 --- /dev/null +++ b/packages/ui/src/lib/example.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "bun:test"; + +describe("Test Setup", () => { + it("bun test works", () => { + expect(1 + 1).toBe(2); + }); + + it("DOM is available via happy-dom", () => { + const div = document.createElement("div"); + div.textContent = "Hello"; + expect(div.textContent).toBe("Hello"); + }); +}); + + diff --git a/packages/ui/src/lib/index.ts b/packages/ui/src/lib/index.ts new file mode 100644 index 0000000..01a6a78 --- /dev/null +++ b/packages/ui/src/lib/index.ts @@ -0,0 +1,4 @@ +// Re-export all components +export * from './primitives/Button'; + +export { }; diff --git a/packages/ui/src/lib/primitives/Badge/Badge.stories.ts b/packages/ui/src/lib/primitives/Badge/Badge.stories.ts new file mode 100644 index 0000000..f8a505a --- /dev/null +++ b/packages/ui/src/lib/primitives/Badge/Badge.stories.ts @@ -0,0 +1,123 @@ +import type { Meta, StoryObj } from '@storybook/svelte'; +import BadgeStory from './Badge.story-wrapper.svelte'; + +const meta = { + title: 'Primitives/Badge', + component: BadgeStory, + tags: ['autodocs'], + argTypes: { + variant: { + control: 'select', + options: ['default', 'success', 'warning', 'error', 'info'], + description: 'Color variant', + }, + size: { + control: 'select', + options: ['sm', 'md'], + description: 'Badge size', + }, + count: { + control: 'number', + description: 'Numeric count (displays 99+ for values > 99)', + }, + dot: { + control: 'boolean', + description: 'Dot-only mode (no text)', + }, + text: { + control: 'text', + description: 'Badge text content', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic variants +export const Default: Story = { + args: { + text: 'Badge', + }, +}; + +export const Success: Story = { + args: { + variant: 'success', + text: 'Connected', + }, +}; + +export const Warning: Story = { + args: { + variant: 'warning', + text: 'Pending', + }, +}; + +export const Error: Story = { + args: { + variant: 'error', + text: 'Failed', + }, +}; + +export const Info: Story = { + args: { + variant: 'info', + text: 'Information', + }, +}; + +// Count badges +export const WithCount: Story = { + args: { + variant: 'error', + count: 5, + }, +}; + +export const HighCount: Story = { + args: { + variant: 'error', + count: 150, + }, +}; + +// Dot badges +export const DotSuccess: Story = { + args: { + variant: 'success', + dot: true, + }, +}; + +export const DotWarning: Story = { + args: { + variant: 'warning', + dot: true, + }, +}; + +export const DotError: Story = { + args: { + variant: 'error', + dot: true, + }, +}; + +// Size variations +export const Small: Story = { + args: { + size: 'sm', + text: 'Small', + }, +}; + +export const SmallWithCount: Story = { + args: { + size: 'sm', + variant: 'info', + count: 12, + }, +}; diff --git a/packages/ui/src/lib/primitives/Badge/Badge.story-wrapper.svelte b/packages/ui/src/lib/primitives/Badge/Badge.story-wrapper.svelte new file mode 100644 index 0000000..16233e7 --- /dev/null +++ b/packages/ui/src/lib/primitives/Badge/Badge.story-wrapper.svelte @@ -0,0 +1,25 @@ + + + + + {text} + diff --git a/packages/ui/src/lib/primitives/Badge/Badge.svelte b/packages/ui/src/lib/primitives/Badge/Badge.svelte new file mode 100644 index 0000000..7fad4ad --- /dev/null +++ b/packages/ui/src/lib/primitives/Badge/Badge.svelte @@ -0,0 +1,111 @@ + + + + + {#if dot} + + {:else if count !== undefined} + {displayCount} + {:else if children} + {@render children()} + {/if} + + + diff --git a/packages/ui/src/lib/primitives/Badge/Badge.test.ts b/packages/ui/src/lib/primitives/Badge/Badge.test.ts new file mode 100644 index 0000000..3b7bab4 --- /dev/null +++ b/packages/ui/src/lib/primitives/Badge/Badge.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'bun:test'; +import { renderComponent, screen } from '../../../../tests/utils'; +import Badge from './Badge.svelte'; +import { createRawSnippet } from 'svelte'; + +describe('Badge', () => { + const textSnippet = createRawSnippet(() => ({ render: () => 'Label' })); + const successSnippet = createRawSnippet(() => ({ render: () => 'Connected' })); + + it('renders with default props', () => { + renderComponent(Badge, { children: textSnippet }); + const badge = screen.getByText('Label'); + expect(badge).toBeTruthy(); + expect(badge.className).toContain('badge--default'); + expect(badge.className).toContain('badge--md'); + }); + + it('renders success variant', () => { + renderComponent(Badge, { variant: 'success', children: successSnippet }); + const badge = screen.getByText('Connected'); + expect(badge).toBeTruthy(); + expect(badge.className).toContain('badge--success'); + }); + + it('renders warning variant', () => { + const snippet = createRawSnippet(() => ({ render: () => 'Pending' })); + renderComponent(Badge, { variant: 'warning', children: snippet }); + const badge = screen.getByText('Pending'); + expect(badge.className).toContain('badge--warning'); + }); + + it('renders error variant', () => { + const snippet = createRawSnippet(() => ({ render: () => 'Failed' })); + renderComponent(Badge, { variant: 'error', children: snippet }); + const badge = screen.getByText('Failed'); + expect(badge.className).toContain('badge--error'); + }); + + it('renders info variant', () => { + const snippet = createRawSnippet(() => ({ render: () => 'Info' })); + renderComponent(Badge, { variant: 'info', children: snippet }); + const badge = screen.getByText('Info'); + expect(badge.className).toContain('badge--info'); + }); + + it('renders count', () => { + renderComponent(Badge, { count: 5 }); + const badge = screen.getByText('5'); + expect(badge).toBeTruthy(); + }); + + it('renders 99+ for large counts', () => { + renderComponent(Badge, { count: 150 }); + const badge = screen.getByText('99+'); + expect(badge).toBeTruthy(); + }); + + it('renders dot mode', () => { + const { container } = renderComponent(Badge, { variant: 'success', dot: true }); + const badge = container.querySelector('.badge--dot'); + expect(badge).toBeTruthy(); + expect(badge?.className).toContain('badge--success'); + // Dot should have no text content + expect(badge?.textContent?.trim()).toBe(''); + }); + + it('renders small size', () => { + const smallSnippet = createRawSnippet(() => ({ render: () => 'Small Badge' })); + const { container } = renderComponent(Badge, { size: 'sm', children: smallSnippet }); + const badge = container.querySelector('.badge--sm'); + expect(badge).toBeTruthy(); + expect(badge?.textContent).toContain('Small Badge'); + }); + + it('uses span element for semantic correctness', () => { + const { container } = renderComponent(Badge, { children: textSnippet }); + const badge = container.querySelector('.badge'); + expect(badge?.tagName.toLowerCase()).toBe('span'); + }); +}); diff --git a/packages/ui/src/lib/primitives/Badge/index.ts b/packages/ui/src/lib/primitives/Badge/index.ts new file mode 100644 index 0000000..33d5622 --- /dev/null +++ b/packages/ui/src/lib/primitives/Badge/index.ts @@ -0,0 +1 @@ +export { default as Badge } from './Badge.svelte'; diff --git a/packages/ui/src/lib/primitives/Button/Button.stories.ts b/packages/ui/src/lib/primitives/Button/Button.stories.ts new file mode 100644 index 0000000..7917105 --- /dev/null +++ b/packages/ui/src/lib/primitives/Button/Button.stories.ts @@ -0,0 +1,88 @@ +import type { Meta, StoryObj } from '@storybook/svelte'; +import ButtonStory from './Button.story-wrapper.svelte'; +import { Save, Trash2, ArrowRight } from 'lucide-svelte'; + +const meta = { + title: 'Primitives/Button', + component: ButtonStory, + tags: ['autodocs'], + argTypes: { + variant: { + control: 'select', + options: ['primary', 'secondary', 'ghost', 'danger'], + }, + size: { + control: 'select', + options: ['sm', 'md', 'lg'], + }, + disabled: { control: 'boolean' }, + loading: { control: 'boolean' }, + iconOnly: { control: 'boolean' }, + text: { control: 'text' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + variant: 'primary', + text: 'Primary Action', + }, +}; + +export const Secondary: Story = { + args: { + variant: 'secondary', + text: 'Secondary Action', + }, +}; + +export const Ghost: Story = { + args: { + variant: 'ghost', + text: 'Ghost Action', + }, +}; + +export const Danger: Story = { + args: { + variant: 'danger', + text: 'Delete Item', + icon: Trash2, + }, +}; + +export const Loading: Story = { + args: { + variant: 'primary', + loading: true, + text: 'Saving...', + }, +}; + +export const Disabled: Story = { + args: { + variant: 'primary', + disabled: true, + text: 'Disabled', + }, +}; + +export const WithIcon: Story = { + args: { + variant: 'primary', + text: 'Save Changes', + icon: Save, + }, +}; + +export const IconOnly: Story = { + args: { + variant: 'secondary', + icon: ArrowRight, + iconOnly: true, + 'aria-label': 'Next page', + }, +}; diff --git a/packages/ui/src/lib/primitives/Button/Button.story-wrapper.svelte b/packages/ui/src/lib/primitives/Button/Button.story-wrapper.svelte new file mode 100644 index 0000000..6811416 --- /dev/null +++ b/packages/ui/src/lib/primitives/Button/Button.story-wrapper.svelte @@ -0,0 +1,32 @@ + + + + diff --git a/packages/ui/src/lib/primitives/Button/Button.svelte b/packages/ui/src/lib/primitives/Button/Button.svelte new file mode 100644 index 0000000..b51a9e5 --- /dev/null +++ b/packages/ui/src/lib/primitives/Button/Button.svelte @@ -0,0 +1,193 @@ + + + + + + + {#if loading} + + {:else if Icon} + + + + {/if} + + {#if children && !iconOnly} + {@render children()} + {/if} + + + diff --git a/packages/ui/src/lib/primitives/Button/Button.test.ts b/packages/ui/src/lib/primitives/Button/Button.test.ts new file mode 100644 index 0000000..2c96ffa --- /dev/null +++ b/packages/ui/src/lib/primitives/Button/Button.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, vi, afterEach } from 'bun:test'; +import { renderComponent, screen, fireEvent, cleanup } from '../../../../tests/utils'; +import Button from './Button.svelte'; +import { createRawSnippet } from 'svelte'; + +describe('Button', () => { + const textSnippet = createRawSnippet(() => ({ render: () => 'Click me' })); + + afterEach(() => { + cleanup(); + }); + + it('renders with default props', () => { + renderComponent(Button, { children: textSnippet }); + const button = screen.getByRole('button', { name: 'Click me' }); + expect(button).toBeTruthy(); + expect(button.className).toContain('ce-button--primary'); + expect(button.className).toContain('ce-button--md'); + }); + + it('fires onclick handler', async () => { + const handleClick = vi.fn(); + renderComponent(Button, { children: textSnippet, onclick: handleClick }); + const button = screen.getByRole('button'); + await fireEvent.click(button); + expect(handleClick).toHaveBeenCalled(); + }); + + it('disabled state prevents click', async () => { + const handleClick = vi.fn(); + renderComponent(Button, { children: textSnippet, onclick: handleClick, disabled: true }); + const button = screen.getByRole('button'); + expect(button.hasAttribute('disabled')).toBe(true); + }); + + it('loading state shows spinner and prevents click', async () => { + renderComponent(Button, { children: textSnippet, loading: true }); + const button = screen.getByRole('button'); + expect(button.hasAttribute('aria-busy')).toBe(true); + expect(button.hasAttribute('disabled')).toBe(true); + // Query by role="status" which is how the Spinner component is structured + const spinner = button.querySelector('[role="status"]'); + expect(spinner).toBeTruthy(); + }); +}); diff --git a/packages/ui/src/lib/primitives/Button/index.ts b/packages/ui/src/lib/primitives/Button/index.ts new file mode 100644 index 0000000..f6e1af1 --- /dev/null +++ b/packages/ui/src/lib/primitives/Button/index.ts @@ -0,0 +1,2 @@ +export { default as Button } from './Button.svelte'; +export type { Props as ButtonProps } from './Button.svelte'; diff --git a/packages/ui/src/lib/primitives/Checkbox/Checkbox.stories.ts b/packages/ui/src/lib/primitives/Checkbox/Checkbox.stories.ts new file mode 100644 index 0000000..d675711 --- /dev/null +++ b/packages/ui/src/lib/primitives/Checkbox/Checkbox.stories.ts @@ -0,0 +1,71 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import Checkbox, { type Props } from "./Checkbox.svelte"; + +const meta = { + title: "Primitives/Checkbox", + component: Checkbox, + tags: ["autodocs"], + argTypes: { + checked: { control: "boolean" }, + disabled: { control: "boolean" }, + indeterminate: { control: "boolean" }, + label: { control: "text" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + checked: false, + disabled: false, + indeterminate: false, + }, +}; + +export const Checked: Story = { + args: { + checked: true, + disabled: false, + indeterminate: false, + }, +}; + +export const WithLabel: Story = { + args: { + checked: false, + disabled: false, + indeterminate: false, + label: "Errors only", + }, +}; + +export const Indeterminate: Story = { + args: { + checked: false, + disabled: false, + indeterminate: true, + label: "Select all (partial)", + }, +}; + +export const Disabled: Story = { + args: { + checked: true, + disabled: true, + indeterminate: false, + label: "Disabled checked", + }, +}; + +export const DisabledUnchecked: Story = { + args: { + checked: false, + disabled: true, + indeterminate: false, + label: "Disabled unchecked", + }, +}; + + diff --git a/packages/ui/src/lib/primitives/Checkbox/Checkbox.svelte b/packages/ui/src/lib/primitives/Checkbox/Checkbox.svelte new file mode 100644 index 0000000..4f2a55a --- /dev/null +++ b/packages/ui/src/lib/primitives/Checkbox/Checkbox.svelte @@ -0,0 +1,138 @@ + + + + + +
+ { + checked = v; + onchange?.(v); + }} + class="ce-checkbox {indeterminate ? 'ce-checkbox--indeterminate' : ''}" + > +
+ {#if indeterminate} + + {:else if checked} + + {/if} +
+
+ {#if label} + + {/if} +
+ + diff --git a/packages/ui/src/lib/primitives/Checkbox/Checkbox.test.ts b/packages/ui/src/lib/primitives/Checkbox/Checkbox.test.ts new file mode 100644 index 0000000..7507604 --- /dev/null +++ b/packages/ui/src/lib/primitives/Checkbox/Checkbox.test.ts @@ -0,0 +1,64 @@ +import { renderComponent, screen, fireEvent } from "../../../../tests/utils"; +import { describe, expect, it, vi } from "bun:test"; +import Checkbox from "./Checkbox.svelte"; + +describe("Checkbox", () => { + it("renders unchecked by default", () => { + renderComponent(Checkbox, { checked: false }); + const checkbox = screen.getByRole("checkbox"); + expect(checkbox.getAttribute("aria-checked")).toBe("false"); + // data-state is what we use for styling now + expect(checkbox.getAttribute("data-state")).toBe("unchecked"); + }); + + it("renders checked state", () => { + renderComponent(Checkbox, { checked: true }); + const checkbox = screen.getByRole("checkbox"); + expect(checkbox.getAttribute("aria-checked")).toBe("true"); + expect(checkbox.getAttribute("data-state")).toBe("checked"); + }); + + it("shows label when provided", () => { + renderComponent(Checkbox, { label: "Test Label" }); + expect(screen.getByText("Test Label")).toBeTruthy(); + }); + + it("toggles on click", async () => { + let checkedValue = false; + const onChange = vi.fn((v: boolean) => { + checkedValue = v; + }); + + renderComponent(Checkbox, { checked: false, onchange: onChange }); + + const checkbox = screen.getByRole("checkbox"); + await fireEvent.click(checkbox); + expect(onChange).toHaveBeenCalled(); + expect(checkedValue).toBe(true); + }); + + it("does not toggle when disabled", async () => { + const onChange = vi.fn(); + + renderComponent(Checkbox, { checked: false, disabled: true, onchange: onChange }); + + const checkbox = screen.getByRole("checkbox"); + await fireEvent.click(checkbox); + expect(onChange).not.toHaveBeenCalled(); + }); + + it("shows indeterminate state", () => { + renderComponent(Checkbox, { checked: false, indeterminate: true }); + const checkbox = screen.getByRole("checkbox"); + expect(checkbox.classList.contains("ce-checkbox--indeterminate")).toBe(true); + // Indeterminate state should have aria-checked="mixed" per ARIA spec + expect(checkbox.getAttribute("aria-checked")).toBe("mixed"); + }); + + it("has correct role and aria attributes", () => { + renderComponent(Checkbox, { checked: true, disabled: true }); + const checkbox = screen.getByRole("checkbox"); + expect(checkbox.getAttribute("role")).toBe("checkbox"); + expect(checkbox.hasAttribute("disabled")).toBe(true); + }); +}); diff --git a/packages/ui/src/lib/primitives/Checkbox/index.ts b/packages/ui/src/lib/primitives/Checkbox/index.ts new file mode 100644 index 0000000..901c1fa --- /dev/null +++ b/packages/ui/src/lib/primitives/Checkbox/index.ts @@ -0,0 +1,2 @@ +export { default as Checkbox } from "./Checkbox.svelte"; +export type { Props as CheckboxProps } from "./Checkbox.svelte"; diff --git a/packages/ui/src/lib/primitives/Icon/Icon.stories.ts b/packages/ui/src/lib/primitives/Icon/Icon.stories.ts new file mode 100644 index 0000000..7635c97 --- /dev/null +++ b/packages/ui/src/lib/primitives/Icon/Icon.stories.ts @@ -0,0 +1,72 @@ +import type { Meta, StoryObj } from '@storybook/svelte'; +import Icon from './Icon.svelte'; +import { + Settings, + Check, + Play, + Pause, + Copy, + X, + Search, + ArrowRight +} from 'lucide-svelte'; + +const meta = { + title: 'Primitives/Icon', + component: Icon, + tags: ['autodocs'], + argTypes: { + size: { + control: 'select', + options: ['sm', 'md', 'lg', 24, 32, 48], + }, + color: { control: 'color' }, + strokeWidth: { control: { type: 'range', min: 1, max: 3, step: 0.5 } }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + icon: Settings, + }, +}; + +export const Small: Story = { + args: { + icon: Settings, + size: 'sm', // 14px + }, +}; + +export const Large: Story = { + args: { + icon: Settings, + size: 'lg', // 20px + }, +}; + +export const CustomSize: Story = { + args: { + icon: Settings, + size: 48, + }, +}; + +export const CustomColor: Story = { + args: { + icon: Check, + color: 'var(--color-success)', + strokeWidth: 3, + }, +}; + +export const WithLabel: Story = { + args: { + icon: Search, + label: 'Search', + }, +}; +// CommonIcons story removed to avoid inline template complexity) diff --git a/packages/ui/src/lib/primitives/Icon/Icon.svelte b/packages/ui/src/lib/primitives/Icon/Icon.svelte new file mode 100644 index 0000000..5e9a67e --- /dev/null +++ b/packages/ui/src/lib/primitives/Icon/Icon.svelte @@ -0,0 +1,79 @@ + + + + + +{#if label} + {label} +{/if} + + + + + + diff --git a/packages/ui/src/lib/primitives/Icon/Icon.test.ts b/packages/ui/src/lib/primitives/Icon/Icon.test.ts new file mode 100644 index 0000000..047d100 --- /dev/null +++ b/packages/ui/src/lib/primitives/Icon/Icon.test.ts @@ -0,0 +1,74 @@ +import { renderComponent, screen } from "../../../../tests/utils"; +import { describe, expect, it } from "bun:test"; +import Icon from "./Icon.svelte"; +import { Settings, Check, Search } from "lucide-svelte"; + +describe("Icon", () => { + it("renders icon", () => { + const { container } = renderComponent(Icon, { icon: Settings }); + const iconWrapper = container.querySelector(".icon"); + expect(iconWrapper).toBeTruthy(); + expect(iconWrapper?.getAttribute("aria-hidden")).toBe("true"); + // Check SVG is rendered + const svg = container.querySelector("svg"); + expect(svg).toBeTruthy(); + }); + + it("applies sm size", () => { + const { container } = renderComponent(Icon, { icon: Settings, size: "sm" }); + const svg = container.querySelector("svg"); + expect(svg).toBeTruthy(); + expect(svg?.getAttribute("width")).toBe("14"); + expect(svg?.getAttribute("height")).toBe("14"); + }); + + it("applies md size (default)", () => { + const { container } = renderComponent(Icon, { icon: Settings }); + const svg = container.querySelector("svg"); + expect(svg).toBeTruthy(); + expect(svg?.getAttribute("width")).toBe("16"); + expect(svg?.getAttribute("height")).toBe("16"); + }); + + it("applies lg size", () => { + const { container } = renderComponent(Icon, { icon: Settings, size: "lg" }); + const svg = container.querySelector("svg"); + expect(svg).toBeTruthy(); + expect(svg?.getAttribute("width")).toBe("20"); + expect(svg?.getAttribute("height")).toBe("20"); + }); + + it("applies custom numeric size", () => { + const { container } = renderComponent(Icon, { icon: Settings, size: 32 }); + const svg = container.querySelector("svg"); + expect(svg).toBeTruthy(); + expect(svg?.getAttribute("width")).toBe("32"); + expect(svg?.getAttribute("height")).toBe("32"); + }); + + it("applies custom color", () => { + const { container } = renderComponent(Icon, { icon: Check, color: "red" }); + const svg = container.querySelector("svg"); + expect(svg).toBeTruthy(); + expect(svg?.getAttribute("stroke")).toBe("red"); + }); + + it("applies label for accessibility", () => { + renderComponent(Icon, { icon: Search, label: "Search" }); + // When label is provided, aria-hidden should be false + const iconWrapper = screen.getByRole("img"); + expect(iconWrapper).toBeTruthy(); + expect(iconWrapper.getAttribute("aria-hidden")).toBe("false"); + // Screen reader text should exist + const srOnly = screen.getByText("Search"); + expect(srOnly).toBeTruthy(); + }); + + it("renders without label (decorative)", () => { + const { container } = renderComponent(Icon, { icon: Settings }); + const iconWrapper = container.querySelector(".icon"); + expect(iconWrapper).toBeTruthy(); + expect(iconWrapper?.getAttribute("aria-hidden")).toBe("true"); + expect(iconWrapper?.getAttribute("role")).toBeNull(); + }); +}); diff --git a/packages/ui/src/lib/primitives/Icon/index.ts b/packages/ui/src/lib/primitives/Icon/index.ts new file mode 100644 index 0000000..02518c8 --- /dev/null +++ b/packages/ui/src/lib/primitives/Icon/index.ts @@ -0,0 +1,2 @@ +export { default as Icon } from './Icon.svelte'; +export type { Props as IconProps } from './Icon.svelte'; diff --git a/packages/ui/src/lib/primitives/Input/Input.stories.ts b/packages/ui/src/lib/primitives/Input/Input.stories.ts new file mode 100644 index 0000000..013fec5 --- /dev/null +++ b/packages/ui/src/lib/primitives/Input/Input.stories.ts @@ -0,0 +1,87 @@ +import type { Meta, StoryObj } from '@storybook/svelte'; +import Input, { type Props } from './Input.svelte'; +import { Search, Mail, Lock } from 'lucide-svelte'; + +const meta = { + title: 'Primitives/Input', + component: Input, + tags: ['autodocs'], + argTypes: { + type: { + control: 'select', + options: ['text', 'search', 'number', 'password'], + }, + size: { + control: 'select', + options: ['sm', 'md'], + }, + disabled: { control: 'boolean' }, + error: { control: 'boolean' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + placeholder: 'Enter text...', + }, +}; + +export const WithValue: Story = { + args: { + value: 'Initial value', + }, +}; + +export const WithIcon: Story = { + args: { + icon: Mail, + placeholder: 'Email address', + }, +}; + +export const SearchInput: Story = { + args: { + type: 'search', + icon: Search, + placeholder: 'Search...', + }, +}; + +export const NumberInput: Story = { + args: { + type: 'number', + placeholder: 'Enter amount', + }, +}; + +export const PasswordInput: Story = { + args: { + type: 'password', + icon: Lock, + placeholder: 'Password', + }, +}; + +export const Disabled: Story = { + args: { + disabled: true, + value: 'Disabled input', + }, +}; + +export const Error: Story = { + args: { + error: true, + value: 'Invalid input', + }, +}; + +export const Small: Story = { + args: { + size: 'sm', + placeholder: 'Small input', + }, +}; diff --git a/packages/ui/src/lib/primitives/Input/Input.svelte b/packages/ui/src/lib/primitives/Input/Input.svelte new file mode 100644 index 0000000..1fbe68f --- /dev/null +++ b/packages/ui/src/lib/primitives/Input/Input.svelte @@ -0,0 +1,165 @@ + + + + + +
+ {#if Icon} + + + + {/if} + + +
+ + diff --git a/packages/ui/src/lib/primitives/Input/Input.test.ts b/packages/ui/src/lib/primitives/Input/Input.test.ts new file mode 100644 index 0000000..ecfee7c --- /dev/null +++ b/packages/ui/src/lib/primitives/Input/Input.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'bun:test'; + +// Skipping Svelte 5 component tests until bun-plugin-svelte-5 is configured +describe.skip('Input', () => { + it('renders with default props', () => { + // pending + }); + + it('updates value on input', () => { + // pending + }); + + it('renders icon', () => { + // pending + }); + + it('disabled state', () => { + // pending + }); + + it('error state', () => { + // pending + }); +}); diff --git a/packages/ui/src/lib/primitives/Input/index.ts b/packages/ui/src/lib/primitives/Input/index.ts new file mode 100644 index 0000000..26f3c99 --- /dev/null +++ b/packages/ui/src/lib/primitives/Input/index.ts @@ -0,0 +1 @@ +export { default as Input, type Props as InputProps } from './Input.svelte'; diff --git a/packages/ui/src/lib/primitives/Select/Select.stories.ts b/packages/ui/src/lib/primitives/Select/Select.stories.ts new file mode 100644 index 0000000..c195815 --- /dev/null +++ b/packages/ui/src/lib/primitives/Select/Select.stories.ts @@ -0,0 +1,97 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import { ArrowLeft, ArrowRight, Globe, User } from "lucide-svelte"; +import Select, { type Props } from "./Select.svelte"; + +const meta = { + title: "Primitives/Select", + component: Select, + tags: ["autodocs"], + argTypes: { + value: { control: "text" }, + placeholder: { control: "text" }, + disabled: { control: "boolean" }, + size: { + control: "select", + options: ["sm", "md"], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const defaultOptions = [ + { value: "all", label: "All Directions" }, + { value: "inbound", label: "Inbound ←", icon: ArrowLeft }, + { value: "outbound", label: "Outbound →", icon: ArrowRight }, +]; + +const iconOptions = [ + { value: "user", label: "User Profile", icon: User }, + { value: "settings", label: "Settings", icon: Globe }, +]; + +export const Default: Story = { + args: { + value: "", + options: defaultOptions, + placeholder: "Select direction...", + disabled: false, + size: "md", + }, +}; + +export const WithValue: Story = { + args: { + value: "inbound", + options: defaultOptions, + placeholder: "Select direction...", + disabled: false, + size: "md", + }, +}; + +export const Small: Story = { + args: { + value: "", + options: defaultOptions, + placeholder: "Select...", + disabled: false, + size: "sm", + }, +}; + +export const WithIcons: Story = { + args: { + value: "", + options: iconOptions, + placeholder: "Choose action...", + disabled: false, + size: "md", + }, +}; + +export const Disabled: Story = { + args: { + value: "inbound", + options: defaultOptions, + placeholder: "Select...", + disabled: true, + size: "md", + }, +}; + +export const WithDisabledOption: Story = { + args: { + value: "", + options: [ + { value: "all", label: "All Directions" }, + { value: "inbound", label: "Inbound ←" }, + { value: "outbound", label: "Outbound →" }, + { value: "disabled", label: "Disabled Option", disabled: true }, + ], + placeholder: "Select...", + disabled: false, + size: "md", + }, +}; diff --git a/packages/ui/src/lib/primitives/Select/Select.svelte b/packages/ui/src/lib/primitives/Select/Select.svelte new file mode 100644 index 0000000..7663559 --- /dev/null +++ b/packages/ui/src/lib/primitives/Select/Select.svelte @@ -0,0 +1,185 @@ + + + + + + onchange?.(v ?? "")} + {disabled} + type="single" +> + + {#if selectedOption?.icon} + {@const IconComponent = selectedOption.icon} + + {/if} + + {selectedOption?.label ?? placeholder} + + + + + + + + + + {#each options as option} + + {#if option.icon} + {@const IconComponent = option.icon} + + {/if} + {option.label} + {#if value === option.value} + + + + {/if} + + {/each} + + + + + + diff --git a/packages/ui/src/lib/primitives/Select/Select.test.ts b/packages/ui/src/lib/primitives/Select/Select.test.ts new file mode 100644 index 0000000..9087f66 --- /dev/null +++ b/packages/ui/src/lib/primitives/Select/Select.test.ts @@ -0,0 +1,95 @@ +import { fireEvent, render, screen } from "@testing-library/svelte"; +import { ArrowLeft, ArrowRight } from "lucide-svelte"; +import { describe, expect, it } from "bun:test"; +import Select from "./Select.svelte"; + +describe("Select", () => { + const options = [ + { value: "all", label: "All Directions" }, + { value: "inbound", label: "Inbound ←", icon: ArrowLeft }, + { value: "outbound", label: "Outbound →", icon: ArrowRight }, + ]; + + it("renders with placeholder when no value", () => { + render(Select, { value: "", options, placeholder: "Select direction..." }); + expect(screen.getByText("Select direction...")).toBeTruthy(); + }); + + it("renders selected value", () => { + render(Select, { value: "inbound", options }); + expect(screen.getByText("Inbound ←")).toBeTruthy(); + }); + + it("opens dropdown on click", async () => { + render(Select, { value: "", options }); + + const trigger = screen.getByRole("combobox"); + await fireEvent.click(trigger); + + expect(screen.getByText("All Directions")).toBeTruthy(); + expect(screen.getByText("Inbound ←")).toBeTruthy(); + expect(screen.getByText("Outbound →")).toBeTruthy(); + }); + + it("selects option on click", async () => { + let selectedValue = ""; + const onChange = (v: string) => { + selectedValue = v; + }; + + render(Select, { options, onchange: onChange }); + + const trigger = screen.getByRole("combobox"); + await fireEvent.click(trigger); + + const option = screen.getByText("Outbound →"); + await fireEvent.click(option); + + expect(selectedValue).toBe("outbound"); + }); + + it("applies size classes", () => { + const { container: smContainer } = render(Select, { + size: "sm", + value: "", + options, + }); + expect(smContainer.querySelector(".ce-select-trigger--sm")).toBeTruthy(); + + const { container: mdContainer } = render(Select, { + size: "md", + value: "", + options, + }); + expect(mdContainer.querySelector(".ce-select-trigger--md")).toBeTruthy(); + }); + + it("disables entire select when disabled prop is true", () => { + render(Select, { value: "", options, disabled: true }); + const trigger = screen.getByRole("combobox"); + expect(trigger.getAttribute("aria-disabled")).toBe("true"); + }); + + it("displays icon for selected option", () => { + const { container } = render(Select, { value: "inbound", options }); + const icon = container.querySelector("svg"); + expect(icon).toBeTruthy(); + }); + + it("calls onchange when selection changes", async () => { + let changedValue = ""; + const onChange = (v: string) => { + changedValue = v; + }; + + render(Select, { value: "", options, onchange: onChange }); + + const trigger = screen.getByRole("combobox"); + await fireEvent.click(trigger); + + const option = screen.getByText("Outbound →"); + await fireEvent.click(option); + + expect(changedValue).toBe("outbound"); + }); +}); diff --git a/packages/ui/src/lib/primitives/Select/index.ts b/packages/ui/src/lib/primitives/Select/index.ts new file mode 100644 index 0000000..7907332 --- /dev/null +++ b/packages/ui/src/lib/primitives/Select/index.ts @@ -0,0 +1 @@ +export { default as Select, type Props as SelectProps, type SelectOption } from "./Select.svelte"; diff --git a/packages/ui/src/lib/primitives/SkipLink/SkipLink.stories.ts b/packages/ui/src/lib/primitives/SkipLink/SkipLink.stories.ts new file mode 100644 index 0000000..b64416f --- /dev/null +++ b/packages/ui/src/lib/primitives/SkipLink/SkipLink.stories.ts @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import SkipLink, { type Props } from "./SkipLink.svelte"; + +const meta = { + title: "Primitives/SkipLink", + component: SkipLink, + tags: ["autodocs"], + argTypes: { + href: { control: "text" }, + targetId: { control: "text" }, + label: { control: "text" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + href: "#main-content", + targetId: "main-content", + label: "Skip to main content", + }, +}; + +export const CustomLabel: Story = { + args: { + href: "#content", + targetId: "content", + label: "Skip to content", + }, +}; + +export const CustomTarget: Story = { + args: { + href: "#app", + targetId: "app", + label: "Skip to app", + }, +}; diff --git a/packages/ui/src/lib/primitives/SkipLink/SkipLink.svelte b/packages/ui/src/lib/primitives/SkipLink/SkipLink.svelte new file mode 100644 index 0000000..00c3ec9 --- /dev/null +++ b/packages/ui/src/lib/primitives/SkipLink/SkipLink.svelte @@ -0,0 +1,61 @@ + + + + + + { + e.preventDefault(); + const target = document.getElementById(targetId); + if (target) { + target.setAttribute("tabindex", "-1"); + target.focus(); + target.removeAttribute("tabindex"); + } + }} +> + {label} + + + diff --git a/packages/ui/src/lib/primitives/SkipLink/SkipLink.test.ts b/packages/ui/src/lib/primitives/SkipLink/SkipLink.test.ts new file mode 100644 index 0000000..cb0d114 --- /dev/null +++ b/packages/ui/src/lib/primitives/SkipLink/SkipLink.test.ts @@ -0,0 +1,34 @@ +import { render, screen } from "@testing-library/svelte"; +import { describe, expect, it } from "bun:test"; +import SkipLink from "./SkipLink.svelte"; + +describe("SkipLink", () => { + it("renders with default props", () => { + render(SkipLink); + const link = screen.getByRole("link"); + expect(link).toBeTruthy(); + expect(link.getAttribute("href")).toBe("#main-content"); + expect(link.textContent).toBe("Skip to main content"); + }); + + it("renders with custom props", () => { + render(SkipLink, { + href: "#app", + targetId: "app", + label: "Skip to app", + }); + const link = screen.getByRole("link"); + expect(link.getAttribute("href")).toBe("#app"); + expect(link.textContent).toBe("Skip to app"); + }); + + it("has correct href", () => { + render(SkipLink, { href: "#content" }); + expect(screen.getByRole("link").getAttribute("href")).toBe("#content"); + }); + + it("has text content", () => { + render(SkipLink, { label: "Skip navigation" }); + expect(screen.getByText("Skip navigation")).toBeTruthy(); + }); +}); diff --git a/packages/ui/src/lib/primitives/SkipLink/index.ts b/packages/ui/src/lib/primitives/SkipLink/index.ts new file mode 100644 index 0000000..73f7aff --- /dev/null +++ b/packages/ui/src/lib/primitives/SkipLink/index.ts @@ -0,0 +1 @@ +export { default as SkipLink, type Props as SkipLinkProps } from "./SkipLink.svelte"; diff --git a/packages/ui/src/lib/primitives/Spinner/Spinner.stories.ts b/packages/ui/src/lib/primitives/Spinner/Spinner.stories.ts new file mode 100644 index 0000000..8490d2e --- /dev/null +++ b/packages/ui/src/lib/primitives/Spinner/Spinner.stories.ts @@ -0,0 +1,48 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import Spinner, { type Props } from "./Spinner.svelte"; + +const meta = { + title: "Primitives/Spinner", + component: Spinner, + tags: ["autodocs"], + argTypes: { + size: { + control: "select", + options: ["sm", "md", "lg"], + }, + color: { control: "color" }, + label: { control: "text" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + size: "md", + label: "Loading", + }, +}; + +export const Small: Story = { + args: { + size: "sm", + label: "Loading", + }, +}; + +export const Large: Story = { + args: { + size: "lg", + label: "Loading", + }, +}; + +export const CustomColor: Story = { + args: { + size: "md", + color: "#3b82f6", + label: "Loading", + }, +}; diff --git a/packages/ui/src/lib/primitives/Spinner/Spinner.svelte b/packages/ui/src/lib/primitives/Spinner/Spinner.svelte new file mode 100644 index 0000000..d516f21 --- /dev/null +++ b/packages/ui/src/lib/primitives/Spinner/Spinner.svelte @@ -0,0 +1,100 @@ + + + + + + + + + + diff --git a/packages/ui/src/lib/primitives/Spinner/Spinner.test.ts b/packages/ui/src/lib/primitives/Spinner/Spinner.test.ts new file mode 100644 index 0000000..2aa5153 --- /dev/null +++ b/packages/ui/src/lib/primitives/Spinner/Spinner.test.ts @@ -0,0 +1,39 @@ +import { render, screen } from "@testing-library/svelte"; +import { describe, expect, it } from "bun:test"; +import Spinner from "./Spinner.svelte"; + +describe("Spinner", () => { + it("renders with default props", () => { + render(Spinner); + const svg = screen.getByRole("status"); + expect(svg).toBeTruthy(); + expect(svg.getAttribute("aria-label")).toBe("Loading"); + }); + + it("renders with custom label", () => { + render(Spinner, { label: "Saving..." }); + expect(screen.getByRole("status").getAttribute("aria-label")).toBe("Saving..."); + }); + + it("applies size classes", () => { + const { container: smContainer } = render(Spinner, { size: "sm" }); + expect(smContainer.querySelector(".spinner--sm")).toBeTruthy(); + + const { container: mdContainer } = render(Spinner, { size: "md" }); + expect(mdContainer.querySelector(".spinner--md")).toBeTruthy(); + + const { container: lgContainer } = render(Spinner, { size: "lg" }); + expect(lgContainer.querySelector(".spinner--lg")).toBeTruthy(); + }); + + it("applies custom color", () => { + const { container } = render(Spinner, { color: "#ff0000" }); + const spinner = container.querySelector(".spinner"); + expect(spinner?.getAttribute("style")).toContain("--spinner-color: #ff0000"); + }); + + it("has correct role", () => { + render(Spinner); + expect(screen.getByRole("status")).toBeTruthy(); + }); +}); diff --git a/packages/ui/src/lib/primitives/Spinner/index.ts b/packages/ui/src/lib/primitives/Spinner/index.ts new file mode 100644 index 0000000..e86092d --- /dev/null +++ b/packages/ui/src/lib/primitives/Spinner/index.ts @@ -0,0 +1 @@ +export { default as Spinner, type Props as SpinnerProps } from "./Spinner.svelte"; diff --git a/packages/ui/src/lib/primitives/StatusDot/StatusDot.stories.ts b/packages/ui/src/lib/primitives/StatusDot/StatusDot.stories.ts new file mode 100644 index 0000000..98d3111 --- /dev/null +++ b/packages/ui/src/lib/primitives/StatusDot/StatusDot.stories.ts @@ -0,0 +1,66 @@ +import type { Meta, StoryObj } from '@storybook/svelte'; +import StatusDot, { type Props } from './StatusDot.svelte'; + +const meta = { + title: 'Primitives/StatusDot', + component: StatusDot, + tags: ['autodocs'], + argTypes: { + status: { + control: 'select', + options: ['success', 'warning', 'error', 'info', 'neutral'], + }, + size: { + control: 'select', + options: ['sm', 'md'], + }, + pulse: { control: 'boolean' }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Neutral: Story = { + args: { + status: 'neutral', + }, +}; + +export const Success: Story = { + args: { + status: 'success', + }, +}; + +export const Warning: Story = { + args: { + status: 'warning', + }, +}; + +export const Error: Story = { + args: { + status: 'error', + }, +}; + +export const Info: Story = { + args: { + status: 'info', + }, +}; + +export const Pulsing: Story = { + args: { + status: 'success', + pulse: true, + }, +}; + +export const Small: Story = { + args: { + status: 'success', + size: 'sm', + }, +}; diff --git a/packages/ui/src/lib/primitives/StatusDot/StatusDot.svelte b/packages/ui/src/lib/primitives/StatusDot/StatusDot.svelte new file mode 100644 index 0000000..e5e2fa1 --- /dev/null +++ b/packages/ui/src/lib/primitives/StatusDot/StatusDot.svelte @@ -0,0 +1,119 @@ + + + + + +{#if label} + {label} +{/if} + +
+ + diff --git a/packages/ui/src/lib/primitives/StatusDot/StatusDot.test.ts b/packages/ui/src/lib/primitives/StatusDot/StatusDot.test.ts new file mode 100644 index 0000000..7388ba5 --- /dev/null +++ b/packages/ui/src/lib/primitives/StatusDot/StatusDot.test.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from 'bun:test'; + +// Skipping Svelte 5 component tests until bun-plugin-svelte-5 is configured +describe.skip('StatusDot', () => { + it('renders correctly', () => { + // pending + }); + + it('applies status colors', () => { + // pending + }); + + it('applies pulse animation', () => { + // pending + }); +}); diff --git a/packages/ui/src/lib/primitives/StatusDot/index.ts b/packages/ui/src/lib/primitives/StatusDot/index.ts new file mode 100644 index 0000000..5901110 --- /dev/null +++ b/packages/ui/src/lib/primitives/StatusDot/index.ts @@ -0,0 +1 @@ +export { default as StatusDot, type Props as StatusDotProps } from './StatusDot.svelte'; diff --git a/packages/ui/src/lib/primitives/Toast/Toast.stories.ts b/packages/ui/src/lib/primitives/Toast/Toast.stories.ts new file mode 100644 index 0000000..93e59db --- /dev/null +++ b/packages/ui/src/lib/primitives/Toast/Toast.stories.ts @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import ToastStoryWrapper from "./Toast.story-wrapper.svelte"; + +const meta = { + title: "Primitives/Toast", + component: ToastStoryWrapper, + tags: ["autodocs"], + parameters: { + layout: "fullscreen", + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Interactive: Story = { + render: () => ({ + Component: ToastStoryWrapper, + }), +}; diff --git a/packages/ui/src/lib/primitives/Toast/Toast.story-wrapper.svelte b/packages/ui/src/lib/primitives/Toast/Toast.story-wrapper.svelte new file mode 100644 index 0000000..c872f16 --- /dev/null +++ b/packages/ui/src/lib/primitives/Toast/Toast.story-wrapper.svelte @@ -0,0 +1,41 @@ + + + +
+ + +
+ + + + +
+
+ + diff --git a/packages/ui/src/lib/primitives/Toast/Toast.test.ts b/packages/ui/src/lib/primitives/Toast/Toast.test.ts new file mode 100644 index 0000000..75e7902 --- /dev/null +++ b/packages/ui/src/lib/primitives/Toast/Toast.test.ts @@ -0,0 +1,70 @@ +import { renderComponent, screen } from "../../../../tests/utils"; +import { beforeEach, describe, expect, it, vi } from "bun:test"; +import ToastContainer from "./ToastContainer.svelte"; +import { toasts } from "./toast.store"; + +describe("Toast", () => { + beforeEach(() => { + // Clear any existing toasts + toasts.subscribe((list) => { + list.forEach((t) => toasts.removeToast(t.id)); + }); + }); + + it("renders toast container", () => { + renderComponent(ToastContainer, {}); + expect( + screen.getByRole("region", { name: "Notifications" }), + ).toBeTruthy(); + }); + + it("shows success toast", async () => { + renderComponent(ToastContainer, {}); + toasts.success("Operation successful"); + + // Wait for DOM update + await new Promise((r) => setTimeout(r, 50)); + expect(screen.getByText("Operation successful")).toBeTruthy(); + }); + + it("shows error toast", async () => { + renderComponent(ToastContainer, {}); + toasts.error("Something went wrong"); + + await new Promise((r) => setTimeout(r, 50)); + expect(screen.getByText("Something went wrong")).toBeTruthy(); + }); + + it("shows warning toast", async () => { + renderComponent(ToastContainer, {}); + toasts.warning("Please check your connection"); + + await new Promise((r) => setTimeout(r, 50)); + expect(screen.getByText("Please check your connection")).toBeTruthy(); + }); + + it("shows info toast", async () => { + renderComponent(ToastContainer, {}); + toasts.info("New update available"); + + await new Promise((r) => setTimeout(r, 50)); + expect(screen.getByText("New update available")).toBeTruthy(); + }); + + it("displays correct class for success type", async () => { + renderComponent(ToastContainer, {}); + toasts.success("Success"); + + await new Promise((r) => setTimeout(r, 50)); + const toast = screen.getByText("Success").closest(".ce-toast"); + expect(toast?.classList.contains("ce-toast-success")).toBe(true); + }); + + it("has dismiss button", async () => { + renderComponent(ToastContainer, {}); + toasts.success("Test message"); + + await new Promise((r) => setTimeout(r, 50)); + expect(screen.getByRole("button", { name: "Dismiss" })).toBeTruthy(); + }); +}); diff --git a/packages/ui/src/lib/primitives/Toast/ToastContainer.svelte b/packages/ui/src/lib/primitives/Toast/ToastContainer.svelte new file mode 100644 index 0000000..352c8d3 --- /dev/null +++ b/packages/ui/src/lib/primitives/Toast/ToastContainer.svelte @@ -0,0 +1,47 @@ + + + + + +
+ {#each toastList as toast (toast.id)} + + {/each} +
+ + diff --git a/packages/ui/src/lib/primitives/Toast/ToastItem.svelte b/packages/ui/src/lib/primitives/Toast/ToastItem.svelte new file mode 100644 index 0000000..a2106ea --- /dev/null +++ b/packages/ui/src/lib/primitives/Toast/ToastItem.svelte @@ -0,0 +1,127 @@ + + + + + + + + diff --git a/packages/ui/src/lib/primitives/Toast/index.ts b/packages/ui/src/lib/primitives/Toast/index.ts new file mode 100644 index 0000000..89ae391 --- /dev/null +++ b/packages/ui/src/lib/primitives/Toast/index.ts @@ -0,0 +1,3 @@ +export { default as ToastContainer, type Props as ToastContainerProps } from "./ToastContainer.svelte"; +export { default as ToastItem, type Props as ToastItemProps } from "./ToastItem.svelte"; +export { toasts, type Toast } from "./toast.store"; diff --git a/packages/ui/src/lib/primitives/Toast/toast.store.ts b/packages/ui/src/lib/primitives/Toast/toast.store.ts new file mode 100644 index 0000000..499ee59 --- /dev/null +++ b/packages/ui/src/lib/primitives/Toast/toast.store.ts @@ -0,0 +1,49 @@ +import { writable } from "svelte/store"; + +export interface Toast { + id: string; + message: string; + type: "info" | "success" | "warning" | "error"; + duration?: number; + dismissable?: boolean; +} + +function createToastStore() { + const { subscribe, update } = writable([]); + + const generateId = () => Math.random().toString(36).slice(2, 9); + + const removeToast = (id: string) => { + update((toasts) => toasts.filter((t) => t.id !== id)); + }; + + const addToast = (toast: Omit): string => { + const id = generateId(); + const newToast: Toast = { + ...toast, + id, + duration: toast.duration ?? 3000, + dismissable: toast.dismissable ?? true, + }; + + update((toasts) => [...toasts, newToast]); + + if (newToast.duration && newToast.duration > 0) { + setTimeout(() => removeToast(id), newToast.duration); + } + + return id; + }; + + return { + subscribe, + addToast, + removeToast, + success: (message: string) => addToast({ message, type: "success" }), + error: (message: string) => addToast({ message, type: "error" }), + warning: (message: string) => addToast({ message, type: "warning" }), + info: (message: string) => addToast({ message, type: "info" }), + }; +} + +export const toasts = createToastStore(); diff --git a/packages/ui/src/lib/primitives/Toggle/Toggle.stories.ts b/packages/ui/src/lib/primitives/Toggle/Toggle.stories.ts new file mode 100644 index 0000000..c0f0669 --- /dev/null +++ b/packages/ui/src/lib/primitives/Toggle/Toggle.stories.ts @@ -0,0 +1,81 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import Toggle, { type Props } from "./Toggle.svelte"; + +const meta = { + title: "Primitives/Toggle", + component: Toggle, + tags: ["autodocs"], + argTypes: { + checked: { control: "boolean" }, + disabled: { control: "boolean" }, + size: { + control: "select", + options: ["sm", "md"], + }, + label: { control: "text" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + checked: false, + disabled: false, + size: "md", + }, +}; + +export const Checked: Story = { + args: { + checked: true, + disabled: false, + size: "md", + }, +}; + +export const WithLabel: Story = { + args: { + checked: false, + disabled: false, + size: "md", + label: "Enable feature", + }, +}; + +export const Small: Story = { + args: { + checked: false, + disabled: false, + size: "sm", + label: "Small toggle", + }, +}; + +export const SmallChecked: Story = { + args: { + checked: true, + disabled: false, + size: "sm", + label: "Small toggle (on)", + }, +}; + +export const DisabledOff: Story = { + args: { + checked: false, + disabled: true, + size: "md", + label: "Disabled off", + }, +}; + +export const DisabledOn: Story = { + args: { + checked: true, + disabled: true, + size: "md", + label: "Disabled on", + }, +}; diff --git a/packages/ui/src/lib/primitives/Toggle/Toggle.svelte b/packages/ui/src/lib/primitives/Toggle/Toggle.svelte new file mode 100644 index 0000000..de7f0d0 --- /dev/null +++ b/packages/ui/src/lib/primitives/Toggle/Toggle.svelte @@ -0,0 +1,138 @@ + + + + + +
+ {#if label} + + {/if} + onchange?.(v ?? false)} + class="ce-switch ce-switch--{size}" + > +
+ +
+
+
+ + diff --git a/packages/ui/src/lib/primitives/Toggle/Toggle.test.ts b/packages/ui/src/lib/primitives/Toggle/Toggle.test.ts new file mode 100644 index 0000000..043bfd7 --- /dev/null +++ b/packages/ui/src/lib/primitives/Toggle/Toggle.test.ts @@ -0,0 +1,90 @@ +import { fireEvent, render, screen } from "@testing-library/svelte"; +import { describe, expect, it } from "bun:test"; +import Toggle from "./Toggle.svelte"; + +describe("Toggle", () => { + it("renders correctly", () => { + render(Toggle, { checked: false }); + const button = screen.getByRole("switch"); + expect(button).toBeTruthy(); + expect(button.getAttribute("aria-checked")).toBe("false"); + }); + + it("renders checked state", () => { + render(Toggle, { checked: true }); + const button = screen.getByRole("switch"); + expect(button.getAttribute("aria-checked")).toBe("true"); + }); + + it("applies size classes", () => { + const { container: smContainer } = render(Toggle, { size: "sm" }); + expect(smContainer.querySelector(".toggle--sm")).toBeTruthy(); + + const { container: mdContainer } = render(Toggle, { size: "md" }); + expect(mdContainer.querySelector(".toggle--md")).toBeTruthy(); + }); + + it("shows label when provided", () => { + render(Toggle, { label: "Test Label" }); + expect(screen.getByText("Test Label")).toBeTruthy(); + }); + + it("toggles on click", async () => { + let checkedValue = false; + const onChange = (v: boolean) => { + checkedValue = v; + }; + + render(Toggle, { checked: false, onchange: onChange }); + + const button = screen.getByRole("switch"); + await fireEvent.click(button); + expect(checkedValue).toBe(true); + }); + + it("does not toggle when disabled", async () => { + const onChange = () => { + throw new Error("Should not call onchange when disabled"); + }; + + render(Toggle, { checked: false, disabled: true, onchange: onChange }); + + const button = screen.getByRole("switch"); + await fireEvent.click(button); + expect(button.getAttribute("aria-checked")).toBe("false"); + }); + + it("toggles on space key", async () => { + let checkedValue = false; + const onChange = (v: boolean) => { + checkedValue = v; + }; + + render(Toggle, { checked: false, onchange: onChange }); + + const button = screen.getByRole("switch"); + await fireEvent.keyDown(button, { key: " " }); + expect(checkedValue).toBe(true); + }); + + it("toggles on enter key", async () => { + let checkedValue = false; + const onChange = (v: boolean) => { + checkedValue = v; + }; + + render(Toggle, { checked: false, onchange: onChange }); + + const button = screen.getByRole("switch"); + await fireEvent.keyDown(button, { key: "Enter" }); + expect(checkedValue).toBe(true); + }); + + it("has correct role and aria attributes", () => { + render(Toggle, { checked: true, disabled: true }); + const button = screen.getByRole("switch"); + expect(button.getAttribute("role")).toBe("switch"); + expect(button.getAttribute("aria-checked")).toBe("true"); + expect(button.getAttribute("aria-disabled")).toBe("true"); + }); +}); diff --git a/packages/ui/src/lib/primitives/Toggle/index.ts b/packages/ui/src/lib/primitives/Toggle/index.ts new file mode 100644 index 0000000..2e8427a --- /dev/null +++ b/packages/ui/src/lib/primitives/Toggle/index.ts @@ -0,0 +1 @@ +export { default as Toggle, type Props as ToggleProps } from "./Toggle.svelte"; diff --git a/packages/ui/src/lib/primitives/Tooltip/Tooltip.stories.ts b/packages/ui/src/lib/primitives/Tooltip/Tooltip.stories.ts new file mode 100644 index 0000000..6f4cc1e --- /dev/null +++ b/packages/ui/src/lib/primitives/Tooltip/Tooltip.stories.ts @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from "@storybook/svelte"; +import Tooltip, { type Props } from "./Tooltip.svelte"; +import TooltipStoryWrapper from "./Tooltip.story-wrapper.svelte"; + +const meta = { + title: "Primitives/Tooltip", + component: Tooltip, + tags: ["autodocs"], + argTypes: { + content: { control: "text" }, + side: { + control: "select", + options: ["top", "right", "bottom", "left"], + }, + delay: { control: "number" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Interactive: Story = { + render: () => ({ + Component: TooltipStoryWrapper, + }), +}; diff --git a/packages/ui/src/lib/primitives/Tooltip/Tooltip.story-wrapper.svelte b/packages/ui/src/lib/primitives/Tooltip/Tooltip.story-wrapper.svelte new file mode 100644 index 0000000..c44aebb --- /dev/null +++ b/packages/ui/src/lib/primitives/Tooltip/Tooltip.story-wrapper.svelte @@ -0,0 +1,49 @@ + + + +
+
+ + + + + + + + + + + + + + + +
+ +
+ + + +
+
+ + diff --git a/packages/ui/src/lib/primitives/Tooltip/Tooltip.svelte b/packages/ui/src/lib/primitives/Tooltip/Tooltip.svelte new file mode 100644 index 0000000..d5db9b0 --- /dev/null +++ b/packages/ui/src/lib/primitives/Tooltip/Tooltip.svelte @@ -0,0 +1,62 @@ + + + + + + + + + {@render children?.()} + + + + {content} + + + + + + diff --git a/packages/ui/src/lib/primitives/Tooltip/Tooltip.test.ts b/packages/ui/src/lib/primitives/Tooltip/Tooltip.test.ts new file mode 100644 index 0000000..1f85b35 --- /dev/null +++ b/packages/ui/src/lib/primitives/Tooltip/Tooltip.test.ts @@ -0,0 +1,16 @@ +import { render, screen } from "@testing-library/svelte"; +import { describe, expect, it } from "bun:test"; +import Tooltip from "./Tooltip.svelte"; + +describe("Tooltip", () => { + it("renders trigger element", () => { + const { container } = render(Tooltip, { content: "Tooltip content" }); + expect(container.querySelector(".ce-tooltip-trigger")).toBeTruthy(); + }); + + it("has correct aria attributes", () => { + render(Tooltip, { content: "Test tooltip" }); + const trigger = screen.getByRole("button"); + expect(trigger.getAttribute("aria-describedby")).toBeTruthy(); + }); +}); diff --git a/packages/ui/src/lib/primitives/Tooltip/index.ts b/packages/ui/src/lib/primitives/Tooltip/index.ts new file mode 100644 index 0000000..4c9dc89 --- /dev/null +++ b/packages/ui/src/lib/primitives/Tooltip/index.ts @@ -0,0 +1 @@ +export { default as Tooltip, type Props as TooltipProps } from "./Tooltip.svelte"; diff --git a/packages/ui/src/lib/theme.ts b/packages/ui/src/lib/theme.ts new file mode 100644 index 0000000..a73dac2 --- /dev/null +++ b/packages/ui/src/lib/theme.ts @@ -0,0 +1,18 @@ +// src/lib/theme.ts +import { getContext, setContext } from 'svelte'; + +const THEME_KEY = Symbol('theme'); + +export interface ThemeContext { + current: 'light' | 'dark' | 'system'; + set: (theme: 'light' | 'dark' | 'system') => void; + toggle: () => void; +} + +export function setThemeContext(context: ThemeContext) { + setContext(THEME_KEY, context); +} + +export function getThemeContext(): ThemeContext { + return getContext(THEME_KEY); +} diff --git a/packages/ui/src/lib/tokens/colors.css b/packages/ui/src/lib/tokens/colors.css new file mode 100644 index 0000000..c3e517c --- /dev/null +++ b/packages/ui/src/lib/tokens/colors.css @@ -0,0 +1,48 @@ +/* src/lib/tokens/colors.css */ + +/* =========================================== + SEMANTIC COLORS - Light Mode (default) + =========================================== */ +:root { + /* Backgrounds */ + --color-bg-primary: #ffffff; + --color-bg-secondary: #f5f5f5; + --color-bg-tertiary: #ebebeb; + --color-bg-surface: #ffffff; + --color-bg-control: #d4d4d4; + /* For form controls like toggles, checkboxes */ + + /* Text */ + --color-text-primary: #1a1a1a; + --color-text-secondary: #666666; + --color-text-tertiary: #999999; + --color-text-inverse: #ffffff; + /* Text on colored backgrounds */ + + /* Borders */ + --color-border: #e5e5e5; +} + +/* =========================================== + STATUS COLORS (same in light/dark) + =========================================== */ +:root { + --color-success: #22c55e; + --color-warning: #f59e0b; + --color-error: #ef4444; + --color-info: #3b82f6; + --color-neutral: #9ca3af; + /* Gray for neutral/inactive status */ +} + +/* =========================================== + PROTOCOL COLORS (MCP-specific) + =========================================== */ +:root { + --color-request: #8b5cf6; + /* purple - outbound */ + --color-response: #06b6d4; + /* cyan - inbound */ + --color-notification: #f97316; + /* orange - notifications */ +} \ No newline at end of file diff --git a/packages/ui/src/lib/tokens/index.css b/packages/ui/src/lib/tokens/index.css new file mode 100644 index 0000000..00febd7 --- /dev/null +++ b/packages/ui/src/lib/tokens/index.css @@ -0,0 +1,27 @@ +/* src/lib/tokens/index.css */ + +/* + * Design Tokens Entry Point + * ========================= + * Import this single file to get all design tokens. + * + * Usage in consuming apps: + * import '@say2/ui/tokens/index.css'; + * + * Usage in Storybook: + * import '../src/lib/tokens/index.css'; + */ + +/* Core tokens from 02-css-variables.md */ +@import './reset.css'; +@import './colors.css'; +@import './spacing.css'; +@import './shadows.css'; +@import './z-index.css'; +@import './motion.css'; + +/* Theme support from 03-theme-provider.md */ +@import './theme.css'; + +/* Typography from 04-typography.md */ +@import './typography.css'; \ No newline at end of file diff --git a/packages/ui/src/lib/tokens/motion.css b/packages/ui/src/lib/tokens/motion.css new file mode 100644 index 0000000..a9170f0 --- /dev/null +++ b/packages/ui/src/lib/tokens/motion.css @@ -0,0 +1,22 @@ +/* src/lib/tokens/motion.css */ + +:root { + /* Duration */ + --duration-fast: 100ms; + --duration-normal: 200ms; + --duration-slow: 300ms; + + /* Easing - functional, not decorative */ + --ease-out: cubic-bezier(0.0, 0.0, 0.2, 1); + --ease-in: cubic-bezier(0.4, 0.0, 1, 1); + --ease-in-out: cubic-bezier(0.4, 0.0, 0.2, 1); +} + +/* Respect user preference */ +@media (prefers-reduced-motion: reduce) { + :root { + --duration-fast: 0ms; + --duration-normal: 0ms; + --duration-slow: 0ms; + } +} \ No newline at end of file diff --git a/packages/ui/src/lib/tokens/reset.css b/packages/ui/src/lib/tokens/reset.css new file mode 100644 index 0000000..94903a4 --- /dev/null +++ b/packages/ui/src/lib/tokens/reset.css @@ -0,0 +1,20 @@ +/* Basic box-sizing reset */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +/* Global defaults */ +html, +body { + margin: 0; + padding: 0; + font-family: var(--font-ui) !important; + font-size: var(--text-base); + line-height: var(--leading-normal); + color: var(--color-text-primary); + background: var(--color-bg-primary); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} \ No newline at end of file diff --git a/packages/ui/src/lib/tokens/shadows.css b/packages/ui/src/lib/tokens/shadows.css new file mode 100644 index 0000000..5f2a7fc --- /dev/null +++ b/packages/ui/src/lib/tokens/shadows.css @@ -0,0 +1,11 @@ +/* src/lib/tokens/shadows.css */ + +:root { + /* Shadow color tokens for theme adaptation */ + --shadow-color: 0 0% 0%; + /* HSL values without alpha */ + + --shadow-sm: 0 1px 2px hsl(var(--shadow-color) / 0.05); + --shadow-md: 0 4px 6px hsl(var(--shadow-color) / 0.1); + --shadow-lg: 0 10px 15px hsl(var(--shadow-color) / 0.15); +} \ No newline at end of file diff --git a/packages/ui/src/lib/tokens/spacing.css b/packages/ui/src/lib/tokens/spacing.css new file mode 100644 index 0000000..6b70916 --- /dev/null +++ b/packages/ui/src/lib/tokens/spacing.css @@ -0,0 +1,20 @@ +/* src/lib/tokens/spacing.css */ + +:root { + /* Base unit: 0.25rem (4px at 16px root) */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.25rem; + --space-6: 1.5rem; + --space-7: 1.75rem; + --space-8: 2rem; + + /* Border radius - keep px for consistent rounding */ + --radius-sm: 0.25rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --radius-full: 9999px; + /* This stays px for true circles */ +} \ No newline at end of file diff --git a/packages/ui/src/lib/tokens/theme.css b/packages/ui/src/lib/tokens/theme.css new file mode 100644 index 0000000..bd3854c --- /dev/null +++ b/packages/ui/src/lib/tokens/theme.css @@ -0,0 +1,89 @@ +/* src/lib/tokens/theme.css */ + +/* =========================================== + DARK MODE OVERRIDES + =========================================== */ + +/* Manual dark mode via data-theme attribute */ +:root[data-theme="dark"] { + /* Backgrounds - softer for less eye strain */ + --color-bg-primary: #1a1a1a; + --color-bg-secondary: #252525; + --color-bg-tertiary: #303030; + --color-bg-surface: #252525; + --color-bg-control: #4a4a4a; + /* Visible in dark mode */ + + /* Text */ + --color-text-primary: #e5e5e5; + --color-text-secondary: #a3a3a3; + --color-text-tertiary: #737373; + + /* Borders - lighter for better visibility */ + --color-border: #525252; +} + +/* Manual light mode via data-theme attribute */ +:root[data-theme="light"] { + /* Backgrounds */ + --color-bg-primary: #ffffff; + --color-bg-secondary: #f5f5f5; + --color-bg-tertiary: #ebebeb; + --color-bg-surface: #ffffff; + --color-bg-control: #d4d4d4; + + /* Text */ + --color-text-primary: #1a1a1a; + --color-text-secondary: #666666; + --color-text-tertiary: #999999; + + /* Borders */ + --color-border: #e5e5e5; +} + +/* System preference (when no manual override) */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme]) { + /* Backgrounds - softer for less eye strain */ + --color-bg-primary: #1a1a1a; + --color-bg-secondary: #252525; + --color-bg-tertiary: #303030; + --color-bg-surface: #252525; + --color-bg-control: #4a4a4a; + + /* Text */ + --color-text-primary: #e5e5e5; + --color-text-secondary: #a3a3a3; + --color-text-tertiary: #737373; + + /* Borders - lighter for better visibility */ + --color-border: #525252; + } +} + +/* =========================================== + THEME TRANSITION + =========================================== */ + +/* + * Performance Note: This transition on :root uses CSS custom property + * inheritance. It's a common pattern (used by Tailwind, Radix, shadcn/ui) + * and doesn't force transitions on every element. Elements that override + * these properties locally won't be affected. + * + * For very heavy DOM trees, consider scoping transitions to specific + * containers if performance issues arise. + */ +:root { + /* Apply to properties that change between themes */ + transition: + background-color var(--duration-normal) var(--ease-out), + color var(--duration-normal) var(--ease-out), + border-color var(--duration-normal) var(--ease-out); +} + +/* Disable transition on initial load */ +:root.no-transition, +:root.no-transition * { + transition: none !important; +} \ No newline at end of file diff --git a/packages/ui/src/lib/tokens/typography.css b/packages/ui/src/lib/tokens/typography.css new file mode 100644 index 0000000..2032819 --- /dev/null +++ b/packages/ui/src/lib/tokens/typography.css @@ -0,0 +1,87 @@ +/* src/lib/tokens/typography.css */ + +/* =========================================== + FONT STACKS + =========================================== */ +:root { + --font-ui: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', monospace; +} + +/* =========================================== + TYPE SCALE + =========================================== */ +:root { + /* Size tokens */ + --text-xs: 11px; + --text-sm: 13px; + --text-base: 14px; + --text-lg: 16px; + --text-xl: 20px; + --text-2xl: 24px; + + /* Weight tokens */ + --font-normal: 400; + --font-medium: 500; + --font-semibold: 600; + --font-bold: 700; + + /* Line height tokens */ + --leading-tight: 1.25; + --leading-normal: 1.5; + --leading-relaxed: 1.75; +} + +/* =========================================== + CODE TYPOGRAPHY + =========================================== */ +:root { + --code-sm: 12px; + --code-base: 13px; +} + +/* =========================================== + UTILITY CLASSES (Internal Development Only) + ------------------------------------------- + These classes are provided for internal use in Storybook stories + and component development. They are NOT intended for library + consumers, who typically bring their own utility layer (Tailwind, + UnoCSS, etc.) or use the CSS custom properties directly. + =========================================== */ +.text-xs { + font-size: var(--text-xs); +} + +.text-sm { + font-size: var(--text-sm); +} + +.text-base { + font-size: var(--text-base); +} + +.text-lg { + font-size: var(--text-lg); +} + +.text-xl { + font-size: var(--text-xl); +} + +.text-2xl { + font-size: var(--text-2xl); +} + +.font-mono { + font-family: var(--font-mono); +} + +.code-sm { + font-family: var(--font-mono); + font-size: var(--code-sm); +} + +.code-base { + font-family: var(--font-mono); + font-size: var(--code-base); +} \ No newline at end of file diff --git a/packages/ui/src/lib/tokens/z-index.css b/packages/ui/src/lib/tokens/z-index.css new file mode 100644 index 0000000..474a4b2 --- /dev/null +++ b/packages/ui/src/lib/tokens/z-index.css @@ -0,0 +1,9 @@ +/* src/lib/tokens/z-index.css */ + +:root { + --z-base: 0; + --z-dropdown: 100; + --z-modal: 200; + --z-toast: 300; + --z-tooltip: 400; +} \ No newline at end of file diff --git a/packages/ui/src/lib/utils.ts b/packages/ui/src/lib/utils.ts new file mode 100644 index 0000000..fcb2cf1 --- /dev/null +++ b/packages/ui/src/lib/utils.ts @@ -0,0 +1,26 @@ +/** + * Simple utility to merge class names. + * Supports strings and objects with boolean values. + * + * Example: + * cn("foo", { "bar": true, "baz": false }) => "foo bar" + */ +export function cn(...inputs: (string | undefined | null | false | Record)[]): string { + const classes: string[] = []; + + for (const input of inputs) { + if (!input) continue; + + if (typeof input === 'string') { + classes.push(input); + } else if (typeof input === 'object') { + for (const [key, value] of Object.entries(input)) { + if (value) { + classes.push(key); + } + } + } + } + + return classes.join(' '); +} diff --git a/packages/ui/svelte.config.js b/packages/ui/svelte.config.js new file mode 100644 index 0000000..692cbf9 --- /dev/null +++ b/packages/ui/svelte.config.js @@ -0,0 +1,9 @@ +// Minimal svelte config for library mode +// vitePreprocess is configured in vite.config.ts instead + +/** @type {import('svelte/compiler').CompileOptions} */ +export default { + // No preprocess here - handled by Vite config + // This prevents the Svelte Language Server from triggering + // rollup dependency errors in the IDE +}; diff --git a/packages/ui/tests/setup.ts b/packages/ui/tests/setup.ts new file mode 100644 index 0000000..f39e972 --- /dev/null +++ b/packages/ui/tests/setup.ts @@ -0,0 +1,10 @@ +import { mock } from "bun:test"; +import { GlobalRegistrator } from "@happy-dom/global-registrator"; + +const svelteEntry = Bun.resolveSync("svelte", import.meta.dir); +const clientEntry = svelteEntry.replace("index-server.js", "index-client.js"); +const client = await import(clientEntry); +mock.module("svelte", () => client); + +// Register happy-dom globally for DOM APIs +GlobalRegistrator.register(); diff --git a/packages/ui/tests/svelte-plugin.ts b/packages/ui/tests/svelte-plugin.ts new file mode 100644 index 0000000..1457873 --- /dev/null +++ b/packages/ui/tests/svelte-plugin.ts @@ -0,0 +1,38 @@ +import { plugin } from "bun"; +import { compile, compileModule } from "svelte/compiler"; + +plugin({ + name: "svelte", + setup(build) { + build.onLoad({ filter: /\.svelte(\.[jt]s)?$/ }, async (args) => { + const content = await Bun.file(args.path).text(); + const isModule = args.path.endsWith('.js') || args.path.endsWith('.ts'); + + try { + let result; + if (isModule) { + result = compileModule(content, { + filename: args.path, + generate: "client", + dev: true, + }); + } else { + result = compile(content, { + filename: args.path, + generate: "client", + dev: true, // improved checking and warnings + }); + } + + return { + contents: result.js.code, + loader: "js", + }; + } catch (err) { + console.error(`Error compiling ${args.path}:`, err); + throw err; + } + }); + + }, +}); diff --git a/packages/ui/tests/utils.d.ts b/packages/ui/tests/utils.d.ts new file mode 100644 index 0000000..8d1c2f6 --- /dev/null +++ b/packages/ui/tests/utils.d.ts @@ -0,0 +1,8 @@ +import { type RenderResult } from "@testing-library/svelte"; +import type { Component, ComponentProps } from "svelte"; +/** + * Render a Svelte 5 component for testing. + * Wraps @testing-library/svelte render with proper typing. + */ +export declare function renderComponent>(component: T, props?: ComponentProps): RenderResult; +export * from "@testing-library/svelte"; diff --git a/packages/ui/tests/utils.ts b/packages/ui/tests/utils.ts new file mode 100644 index 0000000..4b71500 --- /dev/null +++ b/packages/ui/tests/utils.ts @@ -0,0 +1,16 @@ +import { type RenderResult, render } from "@testing-library/svelte"; +import type { Component, ComponentProps } from "svelte"; + +/** + * Render a Svelte 5 component for testing. + * Wraps @testing-library/svelte render with proper typing. + */ +export function renderComponent>( + component: T, + props?: ComponentProps, +): RenderResult { + return render(component as any, { props } as any); +} + +// Re-export everything from testing-library +export * from "@testing-library/svelte"; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json new file mode 100644 index 0000000..aaa6100 --- /dev/null +++ b/packages/ui/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "strict": true, + "moduleResolution": "bundler", + "target": "ESNext", + "module": "ESNext", + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "allowImportingTsExtensions": true, + "noEmit": true, + "resolveJsonModule": true, + "lib": ["ESNext", "DOM", "DOM.Iterable"] + }, + "include": ["src/**/*.ts", "src/**/*.svelte"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts new file mode 100644 index 0000000..1860d89 --- /dev/null +++ b/packages/ui/vite.config.ts @@ -0,0 +1,6 @@ +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [svelte()], +}); diff --git a/scripts/setup-worktree.sh b/scripts/setup-worktree.sh index c6e141f..ff54bd7 100755 --- a/scripts/setup-worktree.sh +++ b/scripts/setup-worktree.sh @@ -6,7 +6,7 @@ set -e -SPECS_REPO="git@github.com:context-engine/v0-docs.git" +SPECS_REPO="git@github.com:context-engine/crystal.git" SPECS_DIR="specs" # Check if we're in a worktree (not a bare clone) diff --git a/stryker.config.mjs b/stryker.config.mjs index c252cd6..3d9b0a0 100644 --- a/stryker.config.mjs +++ b/stryker.config.mjs @@ -9,7 +9,7 @@ const config = { // Use command runner since Bun doesn't have a native Stryker plugin yet testRunner: "command", commandRunner: { - // Only run tests from packages/ to avoid v0-docs reference project + // Only run tests from packages/ to avoid crystal reference project command: "bun test packages/", }, @@ -26,9 +26,9 @@ const config = { "!packages/*/src/**/index.ts", // Skip barrel exports ], - // Exclude v0-docs from being copied to sandbox (has uninstalled deps) + // Exclude crystal from being copied to sandbox (has uninstalled deps) ignorePatterns: [ - "v0-docs/**", + "crystal/**", ".git/**", "node_modules/**", "reports/**",