ui changes and cs prices
All checks were successful
Test and deploy staging / staging (push) Successful in 11m52s

This commit is contained in:
2026-09-25 00:44:06 +00:00
parent c757d0882a
commit abac077541
200 changed files with 6236 additions and 2311 deletions

View File

@@ -13,6 +13,7 @@
"ai": "^7.0.85", "ai": "^7.0.85",
"d3-geo": "^3.1.1", "d3-geo": "^3.1.1",
"drizzle-orm": "^1.0.0-rc.4", "drizzle-orm": "^1.0.0-rc.4",
"echarts": "^6.1.0",
"highlight.js": "^11.12.0", "highlight.js": "^11.12.0",
"marked": "^18.0.11", "marked": "^18.0.11",
"marked-highlight": "^2.2.4", "marked-highlight": "^2.2.4",
@@ -24,7 +25,10 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@fontsource-variable/montserrat": "^5.3.0",
"@iconify-json/simple-icons": "^1.2.94", "@iconify-json/simple-icons": "^1.2.94",
"@internationalized/date": "^3.12.0",
"@lucide/svelte": "^1.48.0",
"@playwright/test": "^1.62.1", "@playwright/test": "^1.62.1",
"@sveltejs/enhanced-img": "^0.11.0", "@sveltejs/enhanced-img": "^0.11.0",
"@sveltejs/kit": "^2.70.3", "@sveltejs/kit": "^2.70.3",
@@ -40,6 +44,7 @@
"bits-ui": "^2.19.0", "bits-ui": "^2.19.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk-sv": "^0.0.19", "cmdk-sv": "^0.0.19",
"cn": "^0.4.0",
"drizzle-kit": "^1.0.0-rc.4", "drizzle-kit": "^1.0.0-rc.4",
"eslint": "^10.9.1", "eslint": "^10.9.1",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
@@ -48,11 +53,13 @@
"lucide-svelte": "^1.0.1", "lucide-svelte": "^1.0.1",
"prettier": "3.9.6", "prettier": "3.9.6",
"prettier-plugin-svelte": "^4.1.1", "prettier-plugin-svelte": "^4.1.1",
"shadcn-svelte": "^1.7.0",
"svelte": "^5.57.0", "svelte": "^5.57.0",
"svelte-check": "4.7.6", "svelte-check": "4.7.6",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.3.1", "tailwind-variants": "^3.3.1",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.3",
"tw-animate-css": "^1.4.0",
"typescript": "~6", "typescript": "~6",
"typescript-eslint": "8.68.0", "typescript-eslint": "8.68.0",
"unplugin-icons": "^23.0.1", "unplugin-icons": "^23.0.1",
@@ -206,6 +213,8 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
"@fontsource-variable/montserrat": ["@fontsource-variable/montserrat@5.3.0", "", {}, "sha512-7PaZoxaxrWLAyrhO46v65An9LhUhfkTExWLhfbywYZCnZEgg/W1rEHnlNmZKjNZ3nJTVYyicqxlJt10z/26yTA=="],
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
@@ -286,6 +295,8 @@
"@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="],
"@lucide/svelte": ["@lucide/svelte@1.48.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-bLimVDjehxu3fpu9XEUpqEkBk2sJAHAT2sVcLHhlM70EdNsXuj9ZnYENWv5xgw3+05h4i5TirAxTiTgnZZf8gw=="],
"@mapbox/jsonlint-lines-primitives": ["@mapbox/jsonlint-lines-primitives@2.0.3", "", {}, "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg=="], "@mapbox/jsonlint-lines-primitives": ["@mapbox/jsonlint-lines-primitives@2.0.3", "", {}, "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg=="],
"@mapbox/point-geometry": ["@mapbox/point-geometry@1.1.0", "", {}, "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ=="], "@mapbox/point-geometry": ["@mapbox/point-geometry@1.1.0", "", {}, "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ=="],
@@ -630,7 +641,9 @@
"cmdk-sv": ["cmdk-sv@0.0.19", "", { "dependencies": { "bits-ui": "^0.21.12", "nanoid": "^5.0.7" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.1" } }, "sha512-Vm6+/up5nQbwGmNw0uYFbXTrbpF460/FhMohTskzzszMtIR/u/78haucSmJWjjp9bW0wxpHXZWa6fk+Lj1tKnA=="], "cmdk-sv": ["cmdk-sv@0.0.19", "", { "dependencies": { "bits-ui": "^0.21.12", "nanoid": "^5.0.7" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.1" } }, "sha512-Vm6+/up5nQbwGmNw0uYFbXTrbpF460/FhMohTskzzszMtIR/u/78haucSmJWjjp9bW0wxpHXZWa6fk+Lj1tKnA=="],
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "cn": ["cn@0.4.0", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-qOAeUhPwPzBxx6jaeQdo646XIKliKHc2j1ZF7Ngb1o0pJZKH5gq8FkA0aVdUdPFUDXNXyUCXpCtoJISo6dhizg=="],
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
@@ -668,6 +681,8 @@
"earcut": ["earcut@3.2.3", "", {}, "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q=="], "earcut": ["earcut@3.2.3", "", {}, "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q=="],
"echarts": ["echarts@6.1.0", "", { "dependencies": { "tslib": "2.3.0", "zrender": "6.1.0" } }, "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.416", "", {}, "sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA=="], "electron-to-chromium": ["electron-to-chromium@1.5.416", "", {}, "sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA=="],
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="], "enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
@@ -876,6 +891,8 @@
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
"node-releases": ["node-releases@2.0.54", "", {}, "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ=="], "node-releases": ["node-releases@2.0.54", "", {}, "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ=="],
"obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="],
@@ -974,6 +991,8 @@
"set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="], "set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="],
"shadcn-svelte": ["shadcn-svelte@1.7.0", "", { "dependencies": { "cn": "^0.3.0", "commander": "^14.0.0", "node-fetch-native": "^1.6.4", "postcss": "^8.5.10" }, "peerDependencies": { "svelte": "^5.0.0" }, "bin": { "shadcn-svelte": "dist/index.mjs" } }, "sha512-AK3ZsYOL8PN/RxcEBCHGvQj2TrLfaaBsbw0QohyEb3nn/yPNDRC/+9iSjnTp/uKEe+tAi0tBxB/Yhzno0N7EEg=="],
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
@@ -1032,6 +1051,8 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
@@ -1084,6 +1105,8 @@
"zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="], "zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="],
"zrender": ["zrender@6.1.0", "", { "dependencies": { "tslib": "2.3.0" } }, "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@maplibre/vt-pbf/pbf": ["pbf@5.1.2", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": "bin/pbf" }, "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w=="], "@maplibre/vt-pbf/pbf": ["pbf@5.1.2", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": "bin/pbf" }, "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w=="],
@@ -1110,6 +1133,8 @@
"cmdk-sv/bits-ui": ["bits-ui@0.21.16", "", { "dependencies": { "@internationalized/date": "^3.5.1", "@melt-ui/svelte": "0.76.2", "nanoid": "^5.0.5" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.118" } }, "sha512-XFZ7/bK7j/K+5iktxX/ZpmoFHjYjpPzP5EOO/4bWiaFg5TG1iMcfjDhlBTQnJxD6BoVoHuqeZPHZvaTgF4Iv3Q=="], "cmdk-sv/bits-ui": ["bits-ui@0.21.16", "", { "dependencies": { "@internationalized/date": "^3.5.1", "@melt-ui/svelte": "0.76.2", "nanoid": "^5.0.5" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.118" } }, "sha512-XFZ7/bK7j/K+5iktxX/ZpmoFHjYjpPzP5EOO/4bWiaFg5TG1iMcfjDhlBTQnJxD6BoVoHuqeZPHZvaTgF4Iv3Q=="],
"echarts/tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="],
"eslint-plugin-compat/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], "eslint-plugin-compat/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
"eslint-plugin-svelte/globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], "eslint-plugin-svelte/globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
@@ -1128,6 +1153,8 @@
"rollup/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "rollup/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"shadcn-svelte/cn": ["cn@0.3.3", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-OEiyJkSpYm2kOuNOKQ+c2wmzZRG8orrb9fnObtHTbCGNx/E9KLnGcmcocItOzREKk5KzQN5I2e3BUCj563KEpw=="],
"svelte-eslint-parser/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], "svelte-eslint-parser/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
"svelte-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], "svelte-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
@@ -1136,6 +1163,10 @@
"svelte-eslint-parser/postcss-selector-parser": ["postcss-selector-parser@7.1.5", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw=="], "svelte-eslint-parser/postcss-selector-parser": ["postcss-selector-parser@7.1.5", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw=="],
"topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"zrender/tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="],
"@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],

View File

@@ -2,7 +2,7 @@
"$schema": "https://shadcn-svelte.com/schema.json", "$schema": "https://shadcn-svelte.com/schema.json",
"tailwind": { "tailwind": {
"css": "src/app.css", "css": "src/app.css",
"baseColor": "slate" "baseColor": "neutral"
}, },
"aliases": { "aliases": {
"components": "$lib/components", "components": "$lib/components",
@@ -12,5 +12,9 @@
"lib": "$lib" "lib": "$lib"
}, },
"typescript": true, "typescript": true,
"registry": "https://tw3.shadcn-svelte.com/registry/default" "registry": "https://shadcn-svelte.com/registry",
"style": "vega",
"iconLibrary": "lucide",
"menuColor": "default",
"menuAccent": "subtle"
} }

814
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,8 @@
"db:migrate:foundation": "bun scripts/migrate-foundation.ts", "db:migrate:foundation": "bun scripts/migrate-foundation.ts",
"db:migrate:money-tracker": "bun scripts/migrate-money-tracker.ts", "db:migrate:money-tracker": "bun scripts/migrate-money-tracker.ts",
"db:migrate:game-tracker": "bun scripts/migrate-game-tracker.ts", "db:migrate:game-tracker": "bun scripts/migrate-game-tracker.ts",
"db:migrate:cs-prices": "bun scripts/migrate-cs-prices.ts",
"cs-prices:ingest": "bun scripts/ingest-cs-prices.ts",
"auth:role": "bun scripts/set-admin.ts", "auth:role": "bun scripts/set-admin.ts",
"catalog:build": "python3 scripts/build-pokemon-catalog.py", "catalog:build": "python3 scripts/build-pokemon-catalog.py",
"catalog:cod": "python3 scripts/build-cod-catalog.py && prettier --write data/call-of-duty.json", "catalog:cod": "python3 scripts/build-cod-catalog.py && prettier --write data/call-of-duty.json",
@@ -30,7 +32,10 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@fontsource-variable/montserrat": "^5.3.0",
"@iconify-json/simple-icons": "^1.2.94", "@iconify-json/simple-icons": "^1.2.94",
"@internationalized/date": "^3.12.0",
"@lucide/svelte": "^1.48.0",
"@playwright/test": "^1.62.1", "@playwright/test": "^1.62.1",
"@sveltejs/enhanced-img": "^0.11.0", "@sveltejs/enhanced-img": "^0.11.0",
"@sveltejs/kit": "^2.70.3", "@sveltejs/kit": "^2.70.3",
@@ -46,6 +51,7 @@
"bits-ui": "^2.19.0", "bits-ui": "^2.19.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk-sv": "^0.0.19", "cmdk-sv": "^0.0.19",
"cn": "^0.4.0",
"drizzle-kit": "^1.0.0-rc.4", "drizzle-kit": "^1.0.0-rc.4",
"eslint": "^10.9.1", "eslint": "^10.9.1",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
@@ -54,11 +60,13 @@
"lucide-svelte": "^1.0.1", "lucide-svelte": "^1.0.1",
"prettier": "3.9.6", "prettier": "3.9.6",
"prettier-plugin-svelte": "^4.1.1", "prettier-plugin-svelte": "^4.1.1",
"shadcn-svelte": "^1.7.0",
"svelte": "^5.57.0", "svelte": "^5.57.0",
"svelte-check": "4.7.6", "svelte-check": "4.7.6",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.3.1", "tailwind-variants": "^3.3.1",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.3",
"tw-animate-css": "^1.4.0",
"typescript": "~6", "typescript": "~6",
"typescript-eslint": "8.68.0", "typescript-eslint": "8.68.0",
"unplugin-icons": "^23.0.1", "unplugin-icons": "^23.0.1",
@@ -75,6 +83,7 @@
"ai": "^7.0.85", "ai": "^7.0.85",
"d3-geo": "^3.1.1", "d3-geo": "^3.1.1",
"drizzle-orm": "^1.0.0-rc.4", "drizzle-orm": "^1.0.0-rc.4",
"echarts": "^6.1.0",
"highlight.js": "^11.12.0", "highlight.js": "^11.12.0",
"marked": "^18.0.11", "marked": "^18.0.11",
"marked-highlight": "^2.2.4", "marked-highlight": "^2.2.4",

View File

@@ -0,0 +1,45 @@
CREATE TABLE IF NOT EXISTS cs_market_items (
id serial PRIMARY KEY,
market_hash_name text NOT NULL,
name text NOT NULL,
icon_url text,
item_type text,
first_seen_at timestamptz NOT NULL DEFAULT now(),
last_seen_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS cs_market_items_hash_idx ON cs_market_items (market_hash_name);
CREATE INDEX IF NOT EXISTS cs_market_items_name_idx ON cs_market_items (name);
CREATE TABLE IF NOT EXISTS cs_item_dimensions (
item_id integer NOT NULL REFERENCES cs_market_items(id) ON DELETE CASCADE,
key text NOT NULL,
value text NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS cs_item_dimensions_unique_idx ON cs_item_dimensions (item_id, key);
CREATE INDEX IF NOT EXISTS cs_item_dimensions_filter_idx ON cs_item_dimensions (key, value, item_id);
CREATE TABLE IF NOT EXISTS cs_price_streams (
source text NOT NULL,
currency text NOT NULL,
kind text NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS cs_price_streams_unique_idx
ON cs_price_streams (source, currency, kind);
CREATE TABLE IF NOT EXISTS cs_price_observations (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
item_id integer NOT NULL REFERENCES cs_market_items(id) ON DELETE CASCADE,
source text NOT NULL,
currency text NOT NULL,
observed_at timestamptz NOT NULL,
price_cents integer NOT NULL CHECK (price_cents >= 0),
volume integer CHECK (volume IS NULL OR volume >= 0),
kind text NOT NULL,
imported_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS cs_price_observations_unique_idx
ON cs_price_observations (item_id, source, currency, observed_at, kind);
CREATE INDEX IF NOT EXISTS cs_price_observations_lookup_idx
ON cs_price_observations (item_id, source, currency, observed_at);
CREATE INDEX IF NOT EXISTS cs_price_observations_time_idx
ON cs_price_observations (observed_at);

287
scripts/ingest-cs-prices.ts Normal file
View File

@@ -0,0 +1,287 @@
/**
* Steam Community Market ingestion. Run after db:migrate:cs-prices.
*
* bun scripts/ingest-cs-prices.ts catalog [--limit=500] [--start=0]
* STEAM_SESSIONID=... STEAM_LOGIN_SECURE=... bun scripts/ingest-cs-prices.ts history [--limit=100] [--start=0]
* bun scripts/ingest-cs-prices.ts import ./observations.jsonl
*
* The first two modes use Steam's public market web endpoints, which are not a
* contracted API and may rate limit or change. `import` remains provider-neutral.
*/
import { readFile } from 'node:fs/promises';
import postgres from 'postgres';
import { dimensionsFromName, type Dimensions } from '../src/lib/cs-prices/taxonomy';
import { steamDate } from '../src/lib/cs-prices/steam';
if (!process.env.DB_URL) throw new Error('DB_URL is required');
const sql = postgres(process.env.DB_URL, { max: 1 });
const mode = process.argv[2];
const flags = new Map(
process.argv
.slice(3)
.filter((arg) => arg.startsWith('--'))
.map((arg) => {
const [key, value] = arg.slice(2).split('=');
return [key, value ?? ''];
})
);
const start = Math.max(0, Number(flags.get('start') ?? 0));
const limit = Math.max(1, Number(flags.get('limit') ?? 1_000_000_000));
const delayMs = Math.max(1000, Number(flags.get('delay-ms') ?? 2000));
const sweepTime = new Date();
const knownStreams = new Set<string>();
type Observation = {
marketHashName: string;
name?: string;
source: string;
currency: string;
observedAt: string;
priceCents: number;
volume?: number | null;
kind: string;
dimensions?: Dimensions;
};
function validObservation(value: unknown): Observation {
if (!value || typeof value !== 'object') throw new Error('Observation must be an object');
const row = value as Record<string, unknown>;
for (const key of ['marketHashName', 'source', 'currency', 'observedAt', 'kind']) {
if (typeof row[key] !== 'string' || !row[key]) throw new Error(`Invalid ${key}`);
}
if (!/^[a-z0-9][a-z0-9-]{0,79}$/.test(row.source as string)) throw new Error('Invalid source');
if (!/^[a-z0-9][a-z0-9-]{0,79}$/.test(row.kind as string)) throw new Error('Invalid kind');
if (!/^[A-Z]{3}$/.test(row.currency as string)) throw new Error('Invalid currency');
if (row.name != null && typeof row.name !== 'string') throw new Error('Invalid name');
if (
!Number.isInteger(row.priceCents) ||
(row.priceCents as number) < 0 ||
(row.priceCents as number) > 2_147_483_647
)
throw new Error('Invalid priceCents');
if (
row.volume != null &&
(!Number.isInteger(row.volume) ||
(row.volume as number) < 0 ||
(row.volume as number) > 2_147_483_647)
)
throw new Error('Invalid volume');
if (Number.isNaN(Date.parse(row.observedAt as string))) throw new Error('Invalid observedAt');
if (
row.dimensions != null &&
(typeof row.dimensions !== 'object' ||
Array.isArray(row.dimensions) ||
Object.values(row.dimensions).some((value) => typeof value !== 'string'))
)
throw new Error('Invalid dimensions');
return row as Observation;
}
async function itemId(
name: string,
displayName = name,
iconUrl?: string,
itemType?: string,
extraDimensions: Dimensions = {}
) {
const [row] = await sql<{ id: number }[]>`
INSERT INTO cs_market_items (market_hash_name, name, icon_url, item_type)
VALUES (${name}, ${displayName}, ${iconUrl ?? null}, ${itemType ?? null})
ON CONFLICT (market_hash_name) DO UPDATE SET
name = EXCLUDED.name,
icon_url = COALESCE(EXCLUDED.icon_url, cs_market_items.icon_url),
item_type = COALESCE(EXCLUDED.item_type, cs_market_items.item_type),
last_seen_at = now()
RETURNING id`;
const inferred = dimensionsFromName(name);
if (itemType) inferred.steam_type = itemType;
for (const [key, value] of Object.entries(inferred)) {
if (!/^[a-z][a-z0-9_]*$/.test(key) || !value) continue;
await sql`
INSERT INTO cs_item_dimensions (item_id, key, value) VALUES (${row.id}, ${key}, ${value})
ON CONFLICT (item_id, key) DO NOTHING`;
}
for (const [key, value] of Object.entries(extraDimensions)) {
if (!/^[a-z][a-z0-9_]*$/.test(key) || !value) continue;
await sql`
INSERT INTO cs_item_dimensions (item_id, key, value) VALUES (${row.id}, ${key}, ${value})
ON CONFLICT (item_id, key) DO UPDATE SET value = EXCLUDED.value`;
}
return row.id;
}
async function saveObservation(row: Observation, knownItemId?: number) {
const item =
knownItemId ??
(await itemId(row.marketHashName, row.name, undefined, undefined, row.dimensions));
await ensureStream(row.source, row.currency, row.kind);
await sql`
INSERT INTO cs_price_observations
(item_id, source, currency, observed_at, price_cents, volume, kind)
VALUES (${item}, ${row.source}, ${row.currency}, ${new Date(row.observedAt)},
${row.priceCents}, ${row.volume ?? null}, ${row.kind})
ON CONFLICT (item_id, source, currency, observed_at, kind) DO UPDATE SET
price_cents = EXCLUDED.price_cents,
volume = EXCLUDED.volume`;
}
async function ensureStream(source: string, currency: string, kind: string) {
const key = `${source}\0${currency}\0${kind}`;
if (knownStreams.has(key)) return;
await sql`INSERT INTO cs_price_streams (source, currency, kind)
VALUES (${source}, ${currency}, ${kind}) ON CONFLICT DO NOTHING`;
knownStreams.add(key);
}
async function saveHistoryRows(
item: { id: number; market_hash_name: string },
prices: [string, number, string][]
) {
await ensureStream('steam-market', 'USD', 'sale-median');
const rows = prices
.filter(([, price]) => Number.isFinite(price) && price >= 0)
.map(([date, price, volume]) => ({
item_id: item.id,
source: 'steam-market',
currency: 'USD',
observed_at: new Date(steamDate(date)),
price_cents: Math.round(price * 100),
volume: Number(volume.replaceAll(',', '')) || 0,
kind: 'sale-median'
}));
for (let offset = 0; offset < rows.length; offset += 500) {
await sql`INSERT INTO cs_price_observations ${sql(rows.slice(offset, offset + 500))}
ON CONFLICT (item_id, source, currency, observed_at, kind) DO UPDATE SET
price_cents = EXCLUDED.price_cents, volume = EXCLUDED.volume`;
}
}
async function steamJson<T>(url: URL, authenticated = false): Promise<T> {
for (let attempt = 0; attempt < 5; attempt++) {
const cookie = authenticated
? `sessionid=${process.env.STEAM_SESSIONID ?? ''}; steamLoginSecure=${process.env.STEAM_LOGIN_SECURE ?? ''}`
: undefined;
const response = await fetch(url, {
headers: {
'User-Agent': 'cs-prices/1.0 personal historical archive',
...(cookie ? { Cookie: cookie } : {})
}
});
if (response.ok) {
const json = await response.json();
if (!json || json.success === false) throw new Error(`Steam rejected ${url.pathname}`);
return json as T;
}
if (response.status !== 429 && response.status < 500)
throw new Error(`Steam HTTP ${response.status}`);
await new Promise((resolve) => setTimeout(resolve, delayMs * 2 ** attempt));
}
throw new Error(`Steam request failed after retries: ${url.pathname}`);
}
const pause = () => new Promise((resolve) => setTimeout(resolve, delayMs));
async function catalog() {
let offset = start;
let processed = 0;
while (processed < limit) {
const url = new URL('https://steamcommunity.com/market/search/render/');
url.search = new URLSearchParams({
appid: '730',
currency: '1',
country: 'US',
norender: '1',
count: String(Math.min(100, limit - processed)),
start: String(offset),
sort_column: 'name',
sort_dir: 'asc'
}).toString();
const page = await steamJson<{
total_count: number;
results: {
hash_name: string;
name: string;
sell_price?: number;
sell_listings?: number;
asset_description?: { icon_url?: string; type?: string };
}[];
}>(url);
if (!page.results?.length) break;
for (const entry of page.results) {
if (!entry.hash_name) continue;
const id = await itemId(
entry.hash_name,
entry.name || entry.hash_name,
entry.asset_description?.icon_url,
entry.asset_description?.type
);
if (Number.isSafeInteger(entry.sell_price) && (entry.sell_price ?? -1) >= 0) {
await saveObservation(
{
marketHashName: entry.hash_name,
source: 'steam-market',
currency: 'USD',
observedAt: sweepTime.toISOString(),
priceCents: entry.sell_price!,
volume: entry.sell_listings,
kind: 'lowest-listing'
},
id
);
}
}
processed += page.results.length;
offset += page.results.length;
console.log(`Catalog: ${offset}/${page.total_count} listings`);
if (offset >= page.total_count) break;
await pause();
}
}
async function history() {
if (!process.env.STEAM_SESSIONID || !process.env.STEAM_LOGIN_SECURE)
throw new Error('STEAM_SESSIONID and STEAM_LOGIN_SECURE are required for Steam history');
const items = await sql<{ id: number; market_hash_name: string }[]>`
SELECT id, market_hash_name FROM cs_market_items ORDER BY id LIMIT ${limit} OFFSET ${start}`;
let done = 0;
for (const item of items) {
const url = new URL('https://steamcommunity.com/market/pricehistory/');
url.search = new URLSearchParams({
appid: '730',
market_hash_name: item.market_hash_name,
currency: '1',
country: 'US'
}).toString();
const data = await steamJson<{ prices: [string, number, string][] }>(url, true);
await saveHistoryRows(item, data.prices ?? []);
console.log(
`History: ${++done}/${items.length} ${item.market_hash_name} (${data.prices?.length ?? 0} points)`
);
await pause();
}
}
async function importFile(path: string) {
const lines = (await readFile(path, 'utf8')).split(/\r?\n/);
let imported = 0;
for (const [index, line] of lines.entries()) {
if (!line.trim()) continue;
try {
await saveObservation(validObservation(JSON.parse(line)));
imported++;
} catch (cause) {
throw new Error(`Invalid observation on line ${index + 1}: ${String(cause)}`, { cause });
}
}
console.log(`Imported ${imported} observations`);
}
try {
if (mode === 'catalog') await catalog();
else if (mode === 'history') await history();
else if (mode === 'import' && process.argv[3]) await importFile(process.argv[3]);
else throw new Error('Usage: ingest-cs-prices.ts catalog|history|import <file>');
await sql`ANALYZE cs_market_items, cs_item_dimensions, cs_price_observations`;
} finally {
await sql.end();
}

View File

@@ -0,0 +1,13 @@
import { readFile } from 'node:fs/promises';
import postgres from 'postgres';
if (!process.env.DB_URL) throw new Error('DB_URL is required');
const client = postgres(process.env.DB_URL, { max: 1 });
try {
await client.begin(async (tx) => {
await tx.unsafe(await readFile(new URL('./cs-prices-migration.sql', import.meta.url), 'utf8'));
});
console.log('CS prices migration applied');
} finally {
await client.end();
}

View File

@@ -1,4 +1,9 @@
@import 'tailwindcss'; @import 'tailwindcss';
@import 'tw-animate-css';
@import 'shadcn-svelte/tailwind.css';
@import '@fontsource-variable/montserrat';
@custom-variant dark (&:is(.dark *));
@plugin '@tailwindcss/typography'; @plugin '@tailwindcss/typography';
@theme { @theme {
@@ -11,13 +16,50 @@
--color-text-muted: #a1a1a1; --color-text-muted: #a1a1a1;
--color-text-dim: #888; --color-text-dim: #888;
--color-text-ghost: #666; --color-text-ghost: #666;
--color-accent: #ededed; --color-accent: var(--accent);
--color-accent-hover: #fff; --color-accent-hover: #fff;
--color-danger: #b44; --color-danger: #b44;
--font-heading: Montserrat, Arial, Helvetica, sans-serif; --font-heading: 'Montserrat Variable', Arial, Helvetica, sans-serif;
--font-mono: 'IBM Plex Mono', 'Courier New', monospace; --font-mono: 'IBM Plex Mono', 'Courier New', monospace;
--font-body: Montserrat, Arial, Helvetica, sans-serif; --font-body: 'Montserrat Variable', Arial, Helvetica, sans-serif;
--font-sans: 'Montserrat Variable', sans-serif;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
} }
:root { :root {
@@ -28,6 +70,38 @@
--site-text: #ededed; --site-text: #ededed;
--site-muted: #a1a1a1; --site-muted: #a1a1a1;
--site-border: #292929; --site-border: #292929;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
} }
:root[data-theme='light'] { :root[data-theme='light'] {
@@ -116,10 +190,12 @@
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
padding: 0; padding: 0;
@apply border-border outline-ring/50;
} }
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
@apply font-sans;
} }
section[id] { section[id] {
@@ -134,6 +210,7 @@
line-height: 1.6; line-height: 1.6;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
@apply bg-background text-foreground;
} }
button, button,
@@ -143,6 +220,14 @@
font: inherit; font: inherit;
} }
input:not([data-slot]):not([type='checkbox']):not([type='radio']):not([type='file']):not(
[type='hidden']
),
textarea:not([data-slot]),
select:not([data-slot]) {
@apply rounded-md border border-input bg-background px-2.5 py-1 text-foreground shadow-xs;
}
:focus-visible { :focus-visible {
outline: 2px solid var(--site-text); outline: 2px solid var(--site-text);
outline-offset: 3px; outline-offset: 3px;
@@ -157,3 +242,28 @@
text-decoration: none; text-decoration: none;
} }
} }
/* Keep the site's black and white palette across every shadcn component. */
.dark {
--background: #000;
--foreground: #ededed;
--card: #0a0a0a;
--card-foreground: #ededed;
--popover: #0a0a0a;
--popover-foreground: #ededed;
--primary: #ededed;
--primary-foreground: #000;
--secondary: #171717;
--secondary-foreground: #ededed;
--muted: #171717;
--muted-foreground: #a1a1a1;
--accent: #171717;
--accent-foreground: #ededed;
--border: #292929;
--input: #292929;
--ring: #ededed;
--sidebar: #0a0a0a;
--sidebar-foreground: #ededed;
--sidebar-accent: #171717;
--sidebar-border: #292929;
}

View File

@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en" class="dark" data-theme="dark">
<head> <head>
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<script> <script>
@@ -17,6 +17,10 @@
? 'light' ? 'light'
: 'dark' : 'dark'
: themePreference; : themePreference;
document.documentElement.classList.toggle(
'dark',
document.documentElement.dataset.theme === 'dark'
);
document.querySelector('meta[name="theme-color"]').content = document.querySelector('meta[name="theme-color"]').content =
document.documentElement.dataset.theme === 'light' ? '#ffffff' : '#000000'; document.documentElement.dataset.theme === 'light' ? '#ffffff' : '#000000';
</script> </script>
@@ -37,12 +41,6 @@
<link rel="icon" type="image/png" sizes="32x32" href="%sveltekit.assets%/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href="%sveltekit.assets%/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="%sveltekit.assets%/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="%sveltekit.assets%/favicon-16x16.png" />
<link rel="manifest" href="%sveltekit.assets%/site.webmanifest" /> <link rel="manifest" href="%sveltekit.assets%/site.webmanifest" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head% %sveltekit.head%
</head> </head>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -3,6 +3,11 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { Button } from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { Separator } from '$lib/components/ui/separator/index.js';
let { let {
mode, mode,
providers, providers,
@@ -67,83 +72,90 @@
</script> </script>
<section class="mx-auto my-12 max-w-md px-4 sm:my-20"> <section class="mx-auto my-12 max-w-md px-4 sm:my-20">
<h1 class="mb-8 text-3xl font-semibold tracking-tight"> <Card.Root>
{mode === 'register' ? 'create account' : 'sign in'} <Card.Header
</h1> ><Card.Title class="text-2xl">{mode === 'register' ? 'create account' : 'sign in'}</Card.Title
{#if message || oauthError} ><Card.Description>Use your account to save and manage your work.</Card.Description
<p role="alert" class="mb-4 rounded bg-red-50 p-3 text-red-800"> ></Card.Header
{message ||
'Provider sign-in failed or was cancelled. Try again, or use your existing login and link the provider from your account.'}
</p>
{/if}
<form method="POST" onsubmit={submit} class="flex flex-col gap-4">
{#if mode === 'register'}
<label class="flex flex-col gap-1"
>Name<input
class="rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-3 py-2"
disabled={!ready || busy}
name="name"
autocomplete="name"
required
maxlength="100"
bind:value={name}
/></label
>
{/if}
<label class="flex flex-col gap-1"
>Email<input
class="rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-3 py-2"
disabled={!ready || busy}
name="email"
type="email"
autocomplete="email"
required
bind:value={email}
/></label
> >
<label class="flex flex-col gap-1" <Card.Content>
>Password<input {#if message || oauthError}
class="rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-3 py-2" <p
disabled={!ready || busy} role="alert"
name="password" class="mb-4 rounded-md border border-destructive/30 bg-destructive/10 p-3 text-sm text-destructive"
type="password" >
autocomplete={mode === 'register' ? 'new-password' : 'current-password'} {message ||
required 'Provider sign-in failed or was cancelled. Try again, or use your existing login and link the provider from your account.'}
minlength={mode === 'register' ? 12 : undefined} </p>
maxlength="128" {/if}
bind:value={password} <form method="POST" onsubmit={submit} class="flex flex-col gap-4">
/></label {#if mode === 'register'}
> <div class="space-y-2">
{#if mode === 'register'}<p class="text-sm text-gray-600">Use at least 12 characters.</p>{/if} <Label for="auth-name">Name</Label><Input
<button id="auth-name"
disabled={!ready || busy} disabled={!ready || busy}
class="rounded bg-[var(--site-text)] px-4 py-2 font-semibold text-[var(--site-bg)] disabled:opacity-50" name="name"
>{busy ? 'please wait…' : mode === 'register' ? 'create account' : 'sign in'}</button autocomplete="name"
> required
</form> maxlength={100}
{#if providers.google || providers.osu} bind:value={name}
<div class="my-6 flex flex-col gap-3 border-t border-[var(--site-border)] pt-4"> />
{#if providers.google}<button </div>
disabled={!ready || busy} {/if}
onclick={() => social('google')} <div class="space-y-2">
class="rounded border border-[var(--site-border)] px-4 py-2 disabled:opacity-50" <Label for="auth-email">Email</Label><Input
>Continue with Google</button id="auth-email"
>{/if} disabled={!ready || busy}
{#if providers.osu}<button name="email"
disabled={!ready || busy} type="email"
onclick={() => social('osu')} autocomplete="email"
class="rounded border border-[var(--site-border)] px-4 py-2 disabled:opacity-50" required
>Continue with osu!</button bind:value={email}
>{/if} />
</div> </div>
{/if} <div class="space-y-2">
<p class="mt-6 text-base"> <Label for="auth-password">Password</Label><Input
<a id="auth-password"
class="underline underline-offset-4" disabled={!ready || busy}
href={resolve( name="password"
`${mode === 'register' ? '/login' : '/register'}?returnTo=${encodeURIComponent(returnTo)}` type="password"
)}>{mode === 'register' ? 'sign in' : 'Create account'}</a autocomplete={mode === 'register' ? 'new-password' : 'current-password'}
> required
</p> minlength={mode === 'register' ? 12 : undefined}
<noscript><p class="mt-4">Enable JavaScript to securely sign in or register.</p></noscript> maxlength={128}
bind:value={password}
/>{#if mode === 'register'}<p class="text-sm text-muted-foreground">
Use at least 12 characters.
</p>{/if}
</div>
<Button type="submit" disabled={!ready || busy} class="w-full"
>{busy ? 'please wait…' : mode === 'register' ? 'create account' : 'sign in'}</Button
>
</form>
{#if providers.google || providers.osu}
<div class="my-6 flex flex-col gap-3">
<Separator />
{#if providers.google}<Button
variant="outline"
disabled={!ready || busy}
onclick={() => social('google')}>Continue with Google</Button
>{/if}
{#if providers.osu}<Button
variant="outline"
disabled={!ready || busy}
onclick={() => social('osu')}>Continue with osu!</Button
>{/if}
</div>
{/if}
<p class="mt-6 text-base">
<a
class="underline underline-offset-4"
href={resolve(
`${mode === 'register' ? '/login' : '/register'}?returnTo=${encodeURIComponent(returnTo)}`
)}>{mode === 'register' ? 'sign in' : 'Create account'}</a
>
</p>
<noscript><p class="mt-4">Enable JavaScript to securely sign in or register.</p></noscript>
</Card.Content>
</Card.Root>
</section> </section>

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { Component } from 'svelte'; import type { Component } from 'svelte';
import { Button } from '$lib/components/ui/button/index.js';
let { let {
icon: Icon, icon: Icon,
@@ -30,13 +31,9 @@
copied = false; copied = false;
}} }}
> >
<button <Button variant="ghost" size="icon-lg" onclick={copy} aria-label={`Copy ${label}: ${value}`}>
onclick={copy} <Icon class="size-6" />
aria-label={`Copy ${label}: ${value}`} </Button>
class="flex h-10 w-10 cursor-pointer items-center justify-center text-xl transition hover:text-[var(--site-text)]"
>
<Icon />
</button>
<span <span
role="tooltip" role="tooltip"

View File

@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Progress } from '$lib/components/ui/progress/index.js';
let { let {
value, value,
max, max,
@@ -12,32 +14,4 @@
} = $props(); } = $props();
</script> </script>
<progress class:small {value} max={max || 1} aria-label={label}></progress> <Progress {value} max={max || 1} aria-label={label} class={small ? 'h-1.5' : 'h-2.5'} />
<style>
progress {
display: block;
width: 100%;
height: 0.625rem;
appearance: none;
border: 0;
border-radius: 9999px;
overflow: hidden;
background: var(--site-border);
}
progress.small {
height: 0.375rem;
}
progress::-webkit-progress-bar {
background: var(--site-border);
border-radius: 9999px;
}
progress::-webkit-progress-value {
background: var(--site-text);
border-radius: 9999px;
}
progress::-moz-progress-bar {
background: var(--site-text);
border-radius: 9999px;
}
</style>

View File

@@ -0,0 +1,54 @@
<script lang="ts" module>
const images = import.meta.glob<string>('../assets/projects/*.{webp,png,jpg,jpeg,avif,svg}', {
eager: true,
query: '?url',
import: 'default'
});
const extensions = ['webp', 'png', 'jpg', 'jpeg', 'avif', 'svg'];
</script>
<script lang="ts">
import CandlestickChart from 'lucide-svelte/icons/candlestick-chart';
import ChartNoAxesCombined from 'lucide-svelte/icons/chart-no-axes-combined';
import FolderKanban from 'lucide-svelte/icons/folder-kanban';
import Gamepad2 from 'lucide-svelte/icons/gamepad-2';
import Globe2 from 'lucide-svelte/icons/globe-2';
import Languages from 'lucide-svelte/icons/languages';
import Music2 from 'lucide-svelte/icons/music-2';
import PawPrint from 'lucide-svelte/icons/paw-print';
import Trophy from 'lucide-svelte/icons/trophy';
import Wallet from 'lucide-svelte/icons/wallet';
let { name, class: className = 'size-5' }: { name: string; class?: string } = $props();
const icons = {
'vash esports': Trophy,
'game tracker': Gamepad2,
animaldex: PawPrint,
'osu! map finder': Music2,
'derby lens': ChartNoAxesCombined,
'world opinions': Globe2,
'language learner': Languages,
'money tracker': Wallet,
'cs prices': CandlestickChart
};
const Icon = $derived(icons[name as keyof typeof icons] ?? FolderKanban);
const filename = $derived(
name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
);
const image = $derived(
extensions
.map((extension) => images[`../assets/projects/${filename}.${extension}`])
.find(Boolean)
);
</script>
{#if image}
<img src={image} alt="" class="size-full rounded-sm object-cover" />
{:else}
<Icon class={className} aria-hidden="true" />
{/if}

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-action"
class={cn('absolute top-2.5 right-3', className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-description"
class={cn(
'text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground',
className
)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-title"
class={cn(
'font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground',
className
)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,44 @@
<script lang="ts" module>
import { type VariantProps, tv } from 'tailwind-variants';
export const alertVariants = tv({
base: "grid gap-0.5 rounded-lg border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 group/alert relative w-full",
variants: {
variant: {
default: 'bg-card text-card-foreground',
destructive:
'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current'
}
},
defaultVariants: {
variant: 'default'
}
});
export type AlertVariant = VariantProps<typeof alertVariants>['variant'];
</script>
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
variant = 'default',
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
variant?: AlertVariant;
} = $props();
</script>
<div
bind:this={ref}
data-slot="alert"
role="alert"
class={cn(alertVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,17 @@
import Action from './alert-action.svelte';
import Description from './alert-description.svelte';
import Title from './alert-title.svelte';
import Root from './alert.svelte';
export { alertVariants, type AlertVariant } from './alert.svelte';
export {
Root,
Description,
Title,
Action,
//
Root as Alert,
Description as AlertDescription,
Title as AlertTitle,
Action as AlertAction
};

View File

@@ -0,0 +1,50 @@
<script lang="ts" module>
import { type VariantProps, tv } from 'tailwind-variants';
export const badgeVariants = tv({
base: 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none',
variants: {
variant: {
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
destructive:
'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20',
outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
link: 'text-primary underline-offset-4 hover:underline'
}
},
defaultVariants: {
variant: 'default'
}
});
export type BadgeVariant = VariantProps<typeof badgeVariants>['variant'];
</script>
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAnchorAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
href,
class: className,
variant = 'default',
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant;
} = $props();
</script>
<svelte:element
this={href ? 'a' : 'span'}
bind:this={ref}
data-slot="badge"
{href}
class={cn(badgeVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</svelte:element>

View File

@@ -0,0 +1,2 @@
export { default as Badge } from './badge.svelte';
export { badgeVariants, type BadgeVariant } from './badge.svelte';

View File

@@ -1,25 +1,91 @@
<script lang="ts"> <script lang="ts" module>
import { Button as ButtonPrimitive } from 'bits-ui'; import { type VariantProps, tv } from 'tailwind-variants';
import { type Events, type Props, buttonVariants } from './index.js'; import { cn, type WithElementRef } from '$lib/utils.js';
import { cn } from '$lib/utils.js'; import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
type $$Props = Props; export const buttonVariants = tv({
type $$Events = Events; base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
outline:
'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs',
secondary:
'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost:
'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',
destructive:
'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default:
'h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: 'h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5',
lg: 'h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
icon: 'size-9',
'icon-xs':
"size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
'icon-sm':
'size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md',
'icon-lg': 'size-10'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
});
let className: $$Props['class'] = undefined; export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
export let variant: $$Props['variant'] = 'default'; export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
export let size: $$Props['size'] = 'default';
export let builders: $$Props['builders'] = []; export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
export { className as class }; WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script> </script>
<ButtonPrimitive.Root <script lang="ts">
{builders} let {
class={cn(buttonVariants({ variant, size, className }))} class: className,
type="button" variant = 'default',
{...$$restProps} size = 'default',
on:click ref = $bindable(null),
on:keydown href = undefined,
> type = 'button',
<slot /> disabled,
</ButtonPrimitive.Root> children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<!-- Callers resolve internal routes before passing href; external links are valid here too. -->
<!-- eslint-disable svelte/no-navigation-without-resolve -->
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? 'link' : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
{type}
{disabled}
{...restProps}
>
{@render children?.()}
</button>
{/if}

View File

@@ -1,48 +1,17 @@
import { type VariantProps, tv } from 'tailwind-variants'; import Root, {
import type { Button as ButtonPrimitive } from 'bits-ui'; type ButtonProps,
import Root from './button.svelte'; type ButtonSize,
type ButtonVariant,
const buttonVariants = tv({ buttonVariants
base: 'ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', } from './button.svelte';
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border-input bg-background hover:bg-accent hover:text-accent-foreground border',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
});
type Variant = VariantProps<typeof buttonVariants>['variant'];
type Size = VariantProps<typeof buttonVariants>['size'];
type Props = ButtonPrimitive.Props & {
variant?: Variant;
size?: Size;
};
type Events = ButtonPrimitive.Events;
export { export {
Root, Root,
type Props, type ButtonProps as Props,
type Events,
// //
Root as Button, Root as Button,
type Props as ButtonProps, buttonVariants,
type Events as ButtonEvents, type ButtonProps,
buttonVariants type ButtonSize,
type ButtonVariant
}; };

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-action"
class={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-content"
class={cn('flex flex-col gap-3 px-(--card-spacing)', className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="card-description"
class={cn('text-muted-foreground text-sm', className)}
{...restProps}
>
{@render children?.()}
</p>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-footer"
class={cn(
'rounded-b-xl px-(--card-spacing) [.border-t]:pt-(--card-spacing) flex items-center',
className
)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-header"
class={cn(
'gap-1 rounded-t-xl px-(--card-spacing) [.border-b]:pb-(--card-spacing) group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]',
className
)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-title"
class={cn(
'font-heading text-base leading-normal font-medium group-data-[size=sm]/card:text-sm',
className
)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
size = 'default',
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: 'default' | 'sm' } = $props();
</script>
<div
bind:this={ref}
data-slot="card"
data-size={size}
class={cn(
'ring-foreground/10 bg-card text-card-foreground gap-(--card-spacing) overflow-hidden rounded-xl py-(--card-spacing) text-sm shadow-xs ring-1 [--card-spacing:--spacing(6)] has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(4)] *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col',
className
)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,25 @@
import Action from './card-action.svelte';
import Content from './card-content.svelte';
import Description from './card-description.svelte';
import Footer from './card-footer.svelte';
import Header from './card-header.svelte';
import Title from './card-title.svelte';
import Root from './card.svelte';
export {
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction
};

View File

@@ -0,0 +1,39 @@
<script lang="ts">
import { Checkbox as CheckboxPrimitive } from 'bits-ui';
import CheckIcon from '@lucide/svelte/icons/check';
import MinusIcon from '@lucide/svelte/icons/minus';
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
...restProps
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
</script>
<CheckboxPrimitive.Root
bind:ref
data-slot="checkbox"
class={cn(
'border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border shadow-xs transition-shadow group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input group-has-[:focus-visible]/field-label:data-checked:border-primary peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
bind:checked
bind:indeterminate
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<div
data-slot="checkbox-indicator"
class="[&>svg]:size-3.5 grid place-content-center text-current transition-none"
>
{#if checked}
<CheckIcon />
{:else if indeterminate}
<MinusIcon />
{/if}
</div>
{/snippet}
</CheckboxPrimitive.Root>

View File

@@ -0,0 +1,6 @@
import Root from './checkbox.svelte';
export {
Root,
//
Root as Checkbox
};

View File

@@ -1,23 +1,42 @@
<script lang="ts"> <script lang="ts">
import type { Dialog as DialogPrimitive } from 'bits-ui';
import type { Command as CommandPrimitive } from 'cmdk-sv';
import Command from './command.svelte';
import * as Dialog from '$lib/components/ui/dialog/index.js'; import * as Dialog from '$lib/components/ui/dialog/index.js';
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
import Command from './command.svelte';
import type { Command as CommandPrimitive, Dialog as DialogPrimitive } from 'bits-ui';
import type { Snippet } from 'svelte';
type $$Props = DialogPrimitive.Props & CommandPrimitive.CommandProps; let {
open = $bindable(false),
export let open: $$Props['open'] = false; ref = $bindable(null),
export let value: $$Props['value'] = undefined; value = $bindable(''),
title = 'Command Palette',
description = 'Search for a command to run...',
showCloseButton = false,
portalProps,
children,
class: className,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.RootProps> &
WithoutChildrenOrChild<CommandPrimitive.RootProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
title?: string;
description?: string;
showCloseButton?: boolean;
class?: string;
} = $props();
</script> </script>
<Dialog.Root bind:open {...$$restProps}> <Dialog.Root bind:open {...restProps}>
<Dialog.Content class="overflow-hidden p-0 shadow-lg"> <Dialog.Header class="sr-only">
<Command <Dialog.Title>{title}</Dialog.Title>
class="[&_[data-cmdk-group-heading]]:text-muted-foreground [&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:font-medium [&_[data-cmdk-group]:not([hidden])_~[data-cmdk-group]]:pt-0 [&_[data-cmdk-group]]:px-2 [&_[data-cmdk-input-wrapper]_svg]:h-5 [&_[data-cmdk-input-wrapper]_svg]:w-5 [&_[data-cmdk-input]]:h-12 [&_[data-cmdk-item]]:px-2 [&_[data-cmdk-item]]:py-3 [&_[data-cmdk-item]_svg]:h-5 [&_[data-cmdk-item]_svg]:w-5" <Dialog.Description>{description}</Dialog.Description>
{...$$restProps} </Dialog.Header>
bind:value <Dialog.Content
> class={cn('rounded-xl! top-1/3 translate-y-0 overflow-hidden p-0', className)}
<slot /> {showCloseButton}
</Command> {portalProps}
>
<Command {...restProps} bind:value bind:ref {children} />
</Dialog.Content> </Dialog.Content>
</Dialog.Root> </Dialog.Root>

View File

@@ -1,12 +1,17 @@
<script lang="ts"> <script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv'; import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
type $$Props = CommandPrimitive.EmptyProps; let {
let className: string | undefined | null = undefined; ref = $bindable(null),
export { className as class }; class: className,
...restProps
}: CommandPrimitive.EmptyProps = $props();
</script> </script>
<CommandPrimitive.Empty class={cn('py-6 text-center text-sm', className)} {...$$restProps}> <CommandPrimitive.Empty
<slot /> bind:ref
</CommandPrimitive.Empty> data-slot="command-empty"
class={cn('py-6 text-center text-sm', className)}
{...restProps}
/>

View File

@@ -1,18 +1,33 @@
<script lang="ts"> <script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv'; import { Command as CommandPrimitive, useId } from 'bits-ui';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
type $$Props = CommandPrimitive.GroupProps;
let className: string | undefined | null = undefined; let {
export { className as class }; ref = $bindable(null),
class: className,
children,
heading,
value,
...restProps
}: CommandPrimitive.GroupProps & {
heading?: string;
} = $props();
</script> </script>
<CommandPrimitive.Group <CommandPrimitive.Group
bind:ref
data-slot="command-group"
class={cn( class={cn(
'text-foreground [&_[data-cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:py-1.5 [&_[data-cmdk-group-heading]]:text-xs [&_[data-cmdk-group-heading]]:font-medium', 'text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium',
className className
)} )}
{...$$restProps} value={value ?? heading ?? `----${useId()}`}
{...restProps}
> >
<slot /> {#if heading}
<CommandPrimitive.GroupHeading class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
{heading}
</CommandPrimitive.GroupHeading>
{/if}
<CommandPrimitive.GroupItems {children} />
</CommandPrimitive.Group> </CommandPrimitive.Group>

View File

@@ -1,23 +1,36 @@
<script lang="ts"> <script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv'; import { Command as CommandPrimitive } from 'bits-ui';
import Search from 'lucide-svelte/icons/search'; import * as InputGroup from '$lib/components/ui/input-group/index.js';
import SearchIcon from '@lucide/svelte/icons/search';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
type $$Props = CommandPrimitive.InputProps; let {
ref = $bindable(null),
let className: string | undefined | null = undefined; class: className,
export { className as class }; value = $bindable(''),
export let value: string = ''; ...restProps
}: CommandPrimitive.InputProps = $props();
</script> </script>
<div class="flex items-center border-b px-2" data-cmdk-input-wrapper=""> <div data-slot="command-input-wrapper" class="p-1 pb-0">
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" /> <InputGroup.Root
<CommandPrimitive.Input class="bg-input/30 border-input/30 h-8! rounded-lg! shadow-none! *:data-[slot=input-group-addon]:pl-2!"
class={cn( >
'placeholder:text-muted-foreground flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50', <CommandPrimitive.Input
className {value}
)} data-slot="command-input"
{...$$restProps} class={cn(
bind:value 'w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
/> className
)}
{...restProps}
>
{#snippet child({ props })}
<InputGroup.Input {...props} bind:value bind:ref />
{/snippet}
</CommandPrimitive.Input>
<InputGroup.Addon>
<SearchIcon class="size-4 shrink-0 opacity-50" />
</InputGroup.Addon>
</InputGroup.Root>
</div> </div>

View File

@@ -1,24 +1,27 @@
<script lang="ts"> <script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv'; import { Command as CommandPrimitive } from 'bits-ui';
import CheckIcon from '@lucide/svelte/icons/check';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
type $$Props = CommandPrimitive.ItemProps; let {
ref = $bindable(null),
export let asChild = false; class: className,
children,
let className: string | undefined | null = undefined; ...restProps
export { className as class }; }: CommandPrimitive.ItemProps = $props();
</script> </script>
<CommandPrimitive.Item <CommandPrimitive.Item
{asChild} bind:ref
data-slot="command-item"
class={cn( class={cn(
'aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50', "data-selected:bg-muted data-selected:text-foreground data-selected:**:[svg]:text-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! [&_svg:not([class*='size-'])]:size-4 group/command-item data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className className
)} )}
{...$$restProps} {...restProps}
let:action
let:attrs
> >
<slot {action} {attrs} /> {@render children?.()}
<CheckIcon
class="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100"
/>
</CommandPrimitive.Item> </CommandPrimitive.Item>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.LinkItemProps = $props();
</script>
<CommandPrimitive.LinkItem
bind:ref
data-slot="command-item"
class={cn(
"data-selected:bg-muted data-selected:text-foreground data-selected:**:[svg]:text-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! [&_svg:not([class*='size-'])]:size-4 group/command-item data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
/>

View File

@@ -1,15 +1,20 @@
<script lang="ts"> <script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv'; import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
type $$Props = CommandPrimitive.ListProps; let {
let className: string | undefined | null = undefined; ref = $bindable(null),
export { className as class }; class: className,
...restProps
}: CommandPrimitive.ListProps = $props();
</script> </script>
<CommandPrimitive.List <CommandPrimitive.List
class={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)} bind:ref
{...$$restProps} data-slot="command-list"
> class={cn(
<slot /> 'no-scrollbar max-h-72 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto',
</CommandPrimitive.List> className
)}
{...restProps}
/>

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
let { ref = $bindable(null), ...restProps }: CommandPrimitive.LoadingProps = $props();
</script>
<CommandPrimitive.Loading bind:ref {...restProps} />

View File

@@ -1,10 +1,17 @@
<script lang="ts"> <script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv'; import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
type $$Props = CommandPrimitive.SeparatorProps; let {
let className: string | undefined | null = undefined; ref = $bindable(null),
export { className as class }; class: className,
...restProps
}: CommandPrimitive.SeparatorProps = $props();
</script> </script>
<CommandPrimitive.Separator class={cn('bg-border -mx-1 h-px', className)} {...$$restProps} /> <CommandPrimitive.Separator
bind:ref
data-slot="command-separator"
class={cn('bg-border -mx-1 h-px w-auto', className)}
{...restProps}
/>

View File

@@ -1,16 +1,23 @@
<script lang="ts"> <script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
type $$Props = HTMLAttributes<HTMLSpanElement>; let {
ref = $bindable(null),
let className: string | undefined | null = undefined; class: className,
export { className as class }; children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script> </script>
<span <span
class={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)} bind:this={ref}
{...$$restProps} data-slot="command-shortcut"
class={cn(
'text-muted-foreground group-data-selected/command-item:text-foreground ml-auto text-xs tracking-widest',
className
)}
{...restProps}
> >
<slot /> {@render children?.()}
</span> </span>

View File

@@ -1,22 +1,28 @@
<script lang="ts"> <script lang="ts">
import { Command as CommandPrimitive } from 'cmdk-sv'; import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
type $$Props = CommandPrimitive.CommandProps; export type CommandRootApi = CommandPrimitive.Root;
export let value: $$Props['value'] = undefined; let {
api = $bindable(null),
let className: string | undefined | null = undefined; ref = $bindable(null),
export { className as class }; value = $bindable(''),
class: className,
...restProps
}: CommandPrimitive.RootProps & {
api?: CommandRootApi | null;
} = $props();
</script> </script>
<CommandPrimitive.Root <CommandPrimitive.Root
bind:this={api}
bind:value
bind:ref
data-slot="command"
class={cn( class={cn(
'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md', 'bg-popover text-popover-foreground rounded-xl! p-1 flex size-full flex-col overflow-hidden',
className className
)} )}
bind:value {...restProps}
{...$$restProps} />
>
<slot />
</CommandPrimitive.Root>

View File

@@ -1,16 +1,14 @@
import { Command as CommandPrimitive } from 'cmdk-sv';
import Root from './command.svelte';
import Dialog from './command-dialog.svelte'; import Dialog from './command-dialog.svelte';
import Empty from './command-empty.svelte'; import Empty from './command-empty.svelte';
import Group from './command-group.svelte'; import Group from './command-group.svelte';
import Item from './command-item.svelte';
import Input from './command-input.svelte'; import Input from './command-input.svelte';
import Item from './command-item.svelte';
import LinkItem from './command-link-item.svelte';
import List from './command-list.svelte'; import List from './command-list.svelte';
import Loading from './command-loading.svelte';
import Separator from './command-separator.svelte'; import Separator from './command-separator.svelte';
import Shortcut from './command-shortcut.svelte'; import Shortcut from './command-shortcut.svelte';
import Root from './command.svelte';
const Loading = CommandPrimitive.Loading;
export { export {
Root, Root,
@@ -18,6 +16,7 @@ export {
Empty, Empty,
Group, Group,
Item, Item,
LinkItem,
Input, Input,
List, List,
Separator, Separator,
@@ -29,6 +28,7 @@ export {
Empty as CommandEmpty, Empty as CommandEmpty,
Group as CommandGroup, Group as CommandGroup,
Item as CommandItem, Item as CommandItem,
LinkItem as CommandLinkItem,
Input as CommandInput, Input as CommandInput,
List as CommandList, List as CommandList,
Separator as CommandSeparator, Separator as CommandSeparator,

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
type = 'button',
...restProps
}: DialogPrimitive.CloseProps = $props();
</script>
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {type} {...restProps} />

View File

@@ -1,36 +1,48 @@
<script lang="ts"> <script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui'; import { Dialog as DialogPrimitive } from 'bits-ui';
import X from 'lucide-svelte/icons/x'; import XIcon from '@lucide/svelte/icons/x';
import { Button } from '$lib/components/ui/button/index.js';
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
import * as Dialog from './index.js'; import * as Dialog from './index.js';
import { cn, flyAndScale } from '$lib/utils.js'; import DialogPortal from './dialog-portal.svelte';
import type { Snippet } from 'svelte';
import type { ComponentProps } from 'svelte';
type $$Props = DialogPrimitive.ContentProps; let {
ref = $bindable(null),
let className: $$Props['class'] = undefined; class: className,
export let transition: $$Props['transition'] = flyAndScale; portalProps,
export let transitionConfig: $$Props['transitionConfig'] = { children,
duration: 200 showCloseButton = true,
}; ...restProps
export { className as class }; }: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
children: Snippet;
showCloseButton?: boolean;
} = $props();
</script> </script>
<Dialog.Portal> <DialogPortal {...portalProps}>
<Dialog.Overlay /> <Dialog.Overlay />
<DialogPrimitive.Content <DialogPrimitive.Content
{transition} bind:ref
{transitionConfig} data-slot="dialog-content"
class={cn( class={cn(
'bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg md:w-full', 'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%_-_2rem)] gap-6 rounded-xl p-6 text-sm ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none',
className className
)} )}
{...$$restProps} {...restProps}
> >
<slot /> {@render children?.()}
<DialogPrimitive.Close {#if showCloseButton}
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none" <DialogPrimitive.Close data-slot="dialog-close">
> {#snippet child({ props })}
<X class="h-4 w-4" /> <Button variant="ghost" class="absolute top-4 right-4" size="icon-sm" {...props}>
<span class="sr-only">Close</span> <XIcon />
</DialogPrimitive.Close> <span class="sr-only">Close</span>
</Button>
{/snippet}
</DialogPrimitive.Close>
{/if}
</DialogPrimitive.Content> </DialogPrimitive.Content>
</Dialog.Portal> </DialogPortal>

View File

@@ -2,15 +2,19 @@
import { Dialog as DialogPrimitive } from 'bits-ui'; import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
type $$Props = DialogPrimitive.DescriptionProps; let {
ref = $bindable(null),
let className: $$Props['class'] = undefined; class: className,
export { className as class }; ...restProps
}: DialogPrimitive.DescriptionProps = $props();
</script> </script>
<DialogPrimitive.Description <DialogPrimitive.Description
class={cn('text-muted-foreground text-sm', className)} bind:ref
{...$$restProps} data-slot="dialog-description"
> class={cn(
<slot /> 'text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3',
</DialogPrimitive.Description> className
)}
{...restProps}
/>

View File

@@ -1,16 +1,32 @@
<script lang="ts"> <script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { Button } from '$lib/components/ui/button/index.js';
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
type $$Props = HTMLAttributes<HTMLDivElement>; let {
ref = $bindable(null),
let className: $$Props['class'] = undefined; class: className,
export { className as class }; children,
showCloseButton = false,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
showCloseButton?: boolean;
} = $props();
</script> </script>
<div <div
class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} bind:this={ref}
{...$$restProps} data-slot="dialog-footer"
class={cn('gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...restProps}
> >
<slot /> {@render children?.()}
{#if showCloseButton}
<DialogPrimitive.Close>
{#snippet child({ props })}
<Button variant="outline" {...props}>Close</Button>
{/snippet}
</DialogPrimitive.Close>
{/if}
</div> </div>

View File

@@ -1,13 +1,20 @@
<script lang="ts"> <script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from 'svelte/elements';
import { cn } from '$lib/utils.js';
type $$Props = HTMLAttributes<HTMLDivElement>; let {
ref = $bindable(null),
let className: $$Props['class'] = undefined; class: className,
export { className as class }; children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script> </script>
<div class={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...$$restProps}> <div
<slot /> bind:this={ref}
data-slot="dialog-header"
class={cn('gap-2 flex flex-col', className)}
{...restProps}
>
{@render children?.()}
</div> </div>

View File

@@ -1,21 +1,20 @@
<script lang="ts"> <script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui'; import { Dialog as DialogPrimitive } from 'bits-ui';
import { fade } from 'svelte/transition';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
type $$Props = DialogPrimitive.OverlayProps; let {
ref = $bindable(null),
let className: $$Props['class'] = undefined; class: className,
export let transition: $$Props['transition'] = fade; ...restProps
export let transitionConfig: $$Props['transitionConfig'] = { }: DialogPrimitive.OverlayProps = $props();
duration: 150
};
export { className as class };
</script> </script>
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
{transition} bind:ref
{transitionConfig} data-slot="dialog-overlay"
class={cn('bg-background/80 fixed inset-0 z-50 backdrop-blur-sm', className)} class={cn(
{...$$restProps} 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50',
className
)}
{...restProps}
/> />

View File

@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui'; import { Dialog as DialogPrimitive } from 'bits-ui';
type $$Props = DialogPrimitive.PortalProps;
let { ...restProps }: DialogPrimitive.PortalProps = $props();
</script> </script>
<DialogPrimitive.Portal {...$$restProps}> <DialogPrimitive.Portal {...restProps} />
<slot />
</DialogPrimitive.Portal>

View File

@@ -2,15 +2,16 @@
import { Dialog as DialogPrimitive } from 'bits-ui'; import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
type $$Props = DialogPrimitive.TitleProps; let {
ref = $bindable(null),
let className: $$Props['class'] = undefined; class: className,
export { className as class }; ...restProps
}: DialogPrimitive.TitleProps = $props();
</script> </script>
<DialogPrimitive.Title <DialogPrimitive.Title
class={cn('text-lg font-semibold leading-none tracking-tight', className)} bind:ref
{...$$restProps} data-slot="dialog-title"
> class={cn('font-heading leading-none font-medium', className)}
<slot /> {...restProps}
</DialogPrimitive.Title> />

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
type = 'button',
...restProps
}: DialogPrimitive.TriggerProps = $props();
</script>
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {type} {...restProps} />

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props();
</script>
<DialogPrimitive.Root bind:open {...restProps} />

View File

@@ -1,16 +1,13 @@
import { Dialog as DialogPrimitive } from 'bits-ui'; import Close from './dialog-close.svelte';
import Content from './dialog-content.svelte';
import Title from './dialog-title.svelte'; import Description from './dialog-description.svelte';
import Portal from './dialog-portal.svelte';
import Footer from './dialog-footer.svelte'; import Footer from './dialog-footer.svelte';
import Header from './dialog-header.svelte'; import Header from './dialog-header.svelte';
import Overlay from './dialog-overlay.svelte'; import Overlay from './dialog-overlay.svelte';
import Content from './dialog-content.svelte'; import Portal from './dialog-portal.svelte';
import Description from './dialog-description.svelte'; import Title from './dialog-title.svelte';
import Trigger from './dialog-trigger.svelte';
const Root = DialogPrimitive.Root; import Root from './dialog.svelte';
const Trigger = DialogPrimitive.Trigger;
const Close = DialogPrimitive.Close;
export { export {
Root, Root,

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
value = $bindable([]),
...restProps
}: DropdownMenuPrimitive.CheckboxGroupProps = $props();
</script>
<DropdownMenuPrimitive.CheckboxGroup
bind:ref
bind:value
data-slot="dropdown-menu-checkbox-group"
{...restProps}
/>

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import MinusIcon from '@lucide/svelte/icons/minus';
import CheckIcon from '@lucide/svelte/icons/check';
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
import type { Snippet } from 'svelte';
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
children?: Snippet;
} = $props();
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:ref
bind:checked
bind:indeterminate
data-slot="dropdown-menu-checkbox-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<span
class="absolute right-2 flex items-center justify-center pointer-events-none"
data-slot="dropdown-menu-checkbox-item-indicator"
>
{#if indeterminate}
<MinusIcon />
{:else if checked}
<CheckIcon />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
import DropdownMenuPortal from './dropdown-menu-portal.svelte';
import type { ComponentProps } from 'svelte';
let {
ref = $bindable(null),
sideOffset = 4,
align = 'start',
portalProps,
class: className,
...restProps
}: DropdownMenuPrimitive.ContentProps & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>;
} = $props();
</script>
<DropdownMenuPortal {...portalProps}>
<DropdownMenuPrimitive.Content
bind:ref
data-slot="dropdown-menu-content"
{sideOffset}
{align}
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-md p-1 shadow-md ring-1 duration-100 z-50 max-h-(--bits-dropdown-menu-content-available-height) w-(--bits-dropdown-menu-anchor-width) origin-(--bits-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto data-closed:overflow-hidden',
className
)}
{...restProps}
/>
</DropdownMenuPortal>

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
import type { ComponentProps } from 'svelte';
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.GroupHeading
bind:ref
data-slot="dropdown-menu-group-heading"
data-inset={inset}
class={cn('text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8', className)}
{...restProps}
/>

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props();
</script>
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
inset,
variant = 'default',
...restProps
}: DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
variant?: 'default' | 'destructive';
} = $props();
</script>
<DropdownMenuPrimitive.Item
bind:ref
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
class={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
inset?: boolean;
} = $props();
</script>
<div
bind:this={ref}
data-slot="dropdown-menu-label"
data-inset={inset}
class={cn('text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8', className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let { ...restProps }: DropdownMenuPrimitive.PortalProps = $props();
</script>
<DropdownMenuPrimitive.Portal {...restProps} />

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
value = $bindable(),
...restProps
}: DropdownMenuPrimitive.RadioGroupProps = $props();
</script>
<DropdownMenuPrimitive.RadioGroup
bind:ref
bind:value
data-slot="dropdown-menu-radio-group"
{...restProps}
/>

View File

@@ -0,0 +1,34 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import CheckIcon from '@lucide/svelte/icons/check';
import { cn, type WithoutChild } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
</script>
<DropdownMenuPrimitive.RadioItem
bind:ref
data-slot="dropdown-menu-radio-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span
class="absolute right-2 flex items-center justify-center pointer-events-none"
data-slot="dropdown-menu-radio-item-indicator"
>
{#if checked}
<CheckIcon />
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props();
</script>
<DropdownMenuPrimitive.Separator
bind:ref
data-slot="dropdown-menu-separator"
class={cn('bg-border -mx-1 my-1 h-px', className)}
{...restProps}
/>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="dropdown-menu-shortcut"
class={cn(
'text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
className
)}
{...restProps}
>
{@render children?.()}
</span>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SubContentProps = $props();
</script>
<DropdownMenuPrimitive.SubContent
bind:ref
data-slot="dropdown-menu-sub-content"
class={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-md p-1 shadow-lg ring-1 duration-100 z-50 origin-(--bits-dropdown-menu-content-transform-origin) overflow-hidden',
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
bind:ref
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
class={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronRightIcon class="cn-rtl-flip ml-auto" />
</DropdownMenuPrimitive.SubTrigger>

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.SubProps = $props();
</script>
<DropdownMenuPrimitive.Sub bind:open {...restProps} />

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props();
</script>
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.RootProps = $props();
</script>
<DropdownMenuPrimitive.Root bind:open {...restProps} />

View File

@@ -0,0 +1,54 @@
import CheckboxGroup from './dropdown-menu-checkbox-group.svelte';
import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
import Content from './dropdown-menu-content.svelte';
import GroupHeading from './dropdown-menu-group-heading.svelte';
import Group from './dropdown-menu-group.svelte';
import Item from './dropdown-menu-item.svelte';
import Label from './dropdown-menu-label.svelte';
import Portal from './dropdown-menu-portal.svelte';
import RadioGroup from './dropdown-menu-radio-group.svelte';
import RadioItem from './dropdown-menu-radio-item.svelte';
import Separator from './dropdown-menu-separator.svelte';
import Shortcut from './dropdown-menu-shortcut.svelte';
import SubContent from './dropdown-menu-sub-content.svelte';
import SubTrigger from './dropdown-menu-sub-trigger.svelte';
import Sub from './dropdown-menu-sub.svelte';
import Trigger from './dropdown-menu-trigger.svelte';
import Root from './dropdown-menu.svelte';
export {
CheckboxGroup,
CheckboxItem,
Content,
Portal,
Root as DropdownMenu,
CheckboxGroup as DropdownMenuCheckboxGroup,
CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent,
Portal as DropdownMenuPortal,
Group as DropdownMenuGroup,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
RadioGroup as DropdownMenuRadioGroup,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
Shortcut as DropdownMenuShortcut,
Sub as DropdownMenuSub,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
Trigger as DropdownMenuTrigger,
GroupHeading as DropdownMenuGroupHeading,
Group,
GroupHeading,
Item,
Label,
RadioGroup,
RadioItem,
Root,
Separator,
Shortcut,
Sub,
SubContent,
SubTrigger,
Trigger
};

View File

@@ -0,0 +1,22 @@
import Addon from './input-group-addon.svelte';
import Button from './input-group-button.svelte';
import Input from './input-group-input.svelte';
import Text from './input-group-text.svelte';
import Textarea from './input-group-textarea.svelte';
import Root from './input-group.svelte';
export {
Root,
Addon,
Button,
Input,
Text,
Textarea,
//
Root as InputGroup,
Addon as InputGroupAddon,
Button as InputGroupButton,
Input as InputGroupInput,
Text as InputGroupText,
Textarea as InputGroupTextarea
};

View File

@@ -0,0 +1,53 @@
<script lang="ts" module>
import { tv, type VariantProps } from 'tailwind-variants';
export const inputGroupAddonVariants = tv({
base: "text-muted-foreground h-auto gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)_-_5px)] [&>svg:not([class*='size-'])]:size-4 flex cursor-text items-center justify-center select-none",
variants: {
align: {
'inline-start': 'pl-2 has-[>button]:-ml-1 has-[>kbd]:ml-[-0.15rem] order-first',
'inline-end': 'pr-2 has-[>button]:-mr-1 has-[>kbd]:mr-[-0.15rem] order-last',
'block-start':
'px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start',
'block-end':
'px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start'
}
},
defaultVariants: {
align: 'inline-start'
}
});
export type InputGroupAddonAlign = VariantProps<typeof inputGroupAddonVariants>['align'];
</script>
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
align = 'inline-start',
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
align?: InputGroupAddonAlign;
} = $props();
</script>
<div
bind:this={ref}
role="group"
data-slot="input-group-addon"
data-align={align}
class={cn(inputGroupAddonVariants({ align }), className)}
onclick={(e) => {
if ((e.target as HTMLElement).closest('button')) {
return;
}
e.currentTarget.parentElement?.querySelector('input')?.focus();
}}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,49 @@
<script lang="ts" module>
import { tv, type VariantProps } from 'tailwind-variants';
const inputGroupButtonVariants = tv({
base: 'gap-2 text-sm flex items-center shadow-none',
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)_-_5px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: '',
'icon-xs': 'size-6 rounded-[calc(var(--radius)_-_5px)] p-0 has-[>svg]:p-0',
'icon-sm': 'size-8 p-0 has-[>svg]:p-0'
}
},
defaultVariants: {
size: 'xs'
}
});
export type InputGroupButtonSize = VariantProps<typeof inputGroupButtonVariants>['size'];
</script>
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import { cn } from '$lib/utils.js';
import type { ComponentProps } from 'svelte';
let {
ref = $bindable(null),
class: className,
children,
type = 'button',
variant = 'ghost',
size = 'xs',
...restProps
}: Omit<ComponentProps<typeof Button>, 'href' | 'size'> & {
size?: InputGroupButtonSize;
} = $props();
</script>
<Button
bind:ref
{type}
data-size={size}
{variant}
class={cn(inputGroupButtonVariants({ size }), className)}
{...restProps}
>
{@render children?.()}
</Button>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input/index.js';
import { cn } from '$lib/utils.js';
import type { ComponentProps } from 'svelte';
let {
ref = $bindable(null),
value = $bindable(),
class: className,
...props
}: ComponentProps<typeof Input> = $props();
</script>
<Input
bind:ref
data-slot="input-group-control"
class={cn(
'rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent flex-1',
className
)}
bind:value
{...props}
/>

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
class={cn(
"text-muted-foreground gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none",
className
)}
{...restProps}
>
{@render children?.()}
</span>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { Textarea } from '$lib/components/ui/textarea/index.js';
import { cn } from '$lib/utils.js';
import type { ComponentProps } from 'svelte';
let {
ref = $bindable(null),
value = $bindable(),
class: className,
...props
}: ComponentProps<typeof Textarea> = $props();
</script>
<Textarea
bind:ref
data-slot="input-group-control"
class={cn(
'rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent flex-1 resize-none',
className
)}
bind:value
{...props}
/>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...props
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="input-group"
role="group"
class={cn(
'group/input-group border-input dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 h-9 rounded-md border shadow-xs transition-[color,box-shadow] in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-3 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto',
className
)}
{...props}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,7 @@
import Root from './input.svelte';
export {
Root,
//
Root as Input
};

View File

@@ -0,0 +1,48 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
type Props = WithElementRef<
Omit<HTMLInputAttributes, 'type'> &
({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined })
>;
let {
ref = $bindable(null),
value = $bindable(),
type,
files = $bindable(),
class: className,
'data-slot': dataSlot = 'input',
...restProps
}: Props = $props();
</script>
{#if type === 'file'}
<input
bind:this={ref}
data-slot={dataSlot}
class={cn(
'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs transition-[color,box-shadow] file:h-7 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
className
)}
type="file"
bind:files
bind:value
{...restProps}
/>
{:else}
<input
bind:this={ref}
data-slot={dataSlot}
class={cn(
'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs transition-[color,box-shadow] file:h-7 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{type}
bind:value
{...restProps}
/>
{/if}

View File

@@ -0,0 +1,7 @@
import Root from './label.svelte';
export {
Root,
//
Root as Label
};

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { Label as LabelPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: LabelPrimitive.RootProps = $props();
</script>
<LabelPrimitive.Root
bind:ref
data-slot="label"
class={cn(
'gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed',
className
)}
{...restProps}
/>

View File

@@ -1,17 +1,28 @@
import { Popover as PopoverPrimitive } from 'bits-ui'; import Close from './popover-close.svelte';
import Content from './popover-content.svelte'; import Content from './popover-content.svelte';
const Root = PopoverPrimitive.Root; import Description from './popover-description.svelte';
const Trigger = PopoverPrimitive.Trigger; import Header from './popover-header.svelte';
const Close = PopoverPrimitive.Close; import Portal from './popover-portal.svelte';
import Title from './popover-title.svelte';
import Trigger from './popover-trigger.svelte';
import Root from './popover.svelte';
export { export {
Root, Root,
Content, Content,
Description,
Header,
Title,
Trigger, Trigger,
Close, Close,
Portal,
// //
Root as Popover, Root as Popover,
Content as PopoverContent, Content as PopoverContent,
Description as PopoverDescription,
Header as PopoverHeader,
Title as PopoverTitle,
Trigger as PopoverTrigger, Trigger as PopoverTrigger,
Close as PopoverClose Close as PopoverClose,
Portal as PopoverPortal
}; };

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from 'bits-ui';
let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();
</script>
<PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} />

View File

@@ -1,22 +1,31 @@
<script lang="ts"> <script lang="ts">
import { Popover as PopoverPrimitive } from 'bits-ui'; import { Popover as PopoverPrimitive } from 'bits-ui';
import { cn, flyAndScale } from '$lib/utils.js'; import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
import PopoverPortal from './popover-portal.svelte';
import type { ComponentProps } from 'svelte';
type $$Props = PopoverPrimitive.ContentProps; let {
let className: $$Props['class'] = undefined; ref = $bindable(null),
export let transition: $$Props['transition'] = flyAndScale; class: className,
export let transitionConfig: $$Props['transitionConfig'] = undefined; sideOffset = 4,
export { className as class }; align = 'center',
portalProps,
...restProps
}: PopoverPrimitive.ContentProps & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>;
} = $props();
</script> </script>
<PopoverPrimitive.Content <PopoverPortal {...portalProps}>
{transition} <PopoverPrimitive.Content
{transitionConfig} bind:ref
class={cn( data-slot="popover-content"
'bg-popover text-popover-foreground z-50 w-72 rounded-md border p-4 shadow-md outline-none', {sideOffset}
className {align}
)} class={cn(
{...$$restProps} 'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 flex flex-col gap-4 rounded-md p-4 text-sm shadow-md ring-1 duration-100 z-50 w-72 origin-(--bits-popover-content-transform-origin) outline-hidden',
> className
<slot /> )}
</PopoverPrimitive.Content> {...restProps}
/>
</PopoverPortal>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="popover-description"
class={cn('text-muted-foreground', className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="popover-header"
class={cn('flex flex-col gap-1 text-sm', className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from 'bits-ui';
let { ...restProps }: PopoverPrimitive.PortalProps = $props();
</script>
<PopoverPrimitive.Portal {...restProps} />

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="popover-title" class={cn('font-medium', className)} {...restProps}>
{@render children?.()}
</div>

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: PopoverPrimitive.TriggerProps = $props();
</script>
<PopoverPrimitive.Trigger
bind:ref
data-slot="popover-trigger"
class={cn(className)}
{...restProps}
/>

Some files were not shown because too many files have changed in this diff Show More