ui changes and cs prices
All checks were successful
Test and deploy staging / staging (push) Successful in 11m52s
33
bun.lock
@@ -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=="],
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
45
scripts/cs-prices-migration.sql
Normal 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
@@ -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();
|
||||||
|
}
|
||||||
13
scripts/migrate-cs-prices.ts
Normal 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();
|
||||||
|
}
|
||||||
116
src/app.css
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
12
src/app.html
@@ -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>
|
||||||
|
|||||||
BIN
src/lib/assets/experience/3webapps.webp
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
src/lib/assets/experience/cargill.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src/lib/assets/experience/finance-plus.webp
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src/lib/assets/experience/h5mag.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/lib/assets/experience/the-hague-university.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/lib/assets/projects/cs-prices.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/lib/assets/projects/osu-map-finder.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
src/lib/assets/projects/vash-esports.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -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,73 +72,78 @@
|
|||||||
</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
|
||||||
|
><Card.Description>Use your account to save and manage your work.</Card.Description
|
||||||
|
></Card.Header
|
||||||
|
>
|
||||||
|
<Card.Content>
|
||||||
{#if message || oauthError}
|
{#if message || oauthError}
|
||||||
<p role="alert" class="mb-4 rounded bg-red-50 p-3 text-red-800">
|
<p
|
||||||
|
role="alert"
|
||||||
|
class="mb-4 rounded-md border border-destructive/30 bg-destructive/10 p-3 text-sm text-destructive"
|
||||||
|
>
|
||||||
{message ||
|
{message ||
|
||||||
'Provider sign-in failed or was cancelled. Try again, or use your existing login and link the provider from your account.'}
|
'Provider sign-in failed or was cancelled. Try again, or use your existing login and link the provider from your account.'}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<form method="POST" onsubmit={submit} class="flex flex-col gap-4">
|
<form method="POST" onsubmit={submit} class="flex flex-col gap-4">
|
||||||
{#if mode === 'register'}
|
{#if mode === 'register'}
|
||||||
<label class="flex flex-col gap-1"
|
<div class="space-y-2">
|
||||||
>Name<input
|
<Label for="auth-name">Name</Label><Input
|
||||||
class="rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-3 py-2"
|
id="auth-name"
|
||||||
disabled={!ready || busy}
|
disabled={!ready || busy}
|
||||||
name="name"
|
name="name"
|
||||||
autocomplete="name"
|
autocomplete="name"
|
||||||
required
|
required
|
||||||
maxlength="100"
|
maxlength={100}
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
/></label
|
/>
|
||||||
>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<label class="flex flex-col gap-1"
|
<div class="space-y-2">
|
||||||
>Email<input
|
<Label for="auth-email">Email</Label><Input
|
||||||
class="rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-3 py-2"
|
id="auth-email"
|
||||||
disabled={!ready || busy}
|
disabled={!ready || busy}
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
autocomplete="email"
|
autocomplete="email"
|
||||||
required
|
required
|
||||||
bind:value={email}
|
bind:value={email}
|
||||||
/></label
|
/>
|
||||||
>
|
</div>
|
||||||
<label class="flex flex-col gap-1"
|
<div class="space-y-2">
|
||||||
>Password<input
|
<Label for="auth-password">Password</Label><Input
|
||||||
class="rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-3 py-2"
|
id="auth-password"
|
||||||
disabled={!ready || busy}
|
disabled={!ready || busy}
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete={mode === 'register' ? 'new-password' : 'current-password'}
|
autocomplete={mode === 'register' ? 'new-password' : 'current-password'}
|
||||||
required
|
required
|
||||||
minlength={mode === 'register' ? 12 : undefined}
|
minlength={mode === 'register' ? 12 : undefined}
|
||||||
maxlength="128"
|
maxlength={128}
|
||||||
bind:value={password}
|
bind:value={password}
|
||||||
/></label
|
/>{#if mode === 'register'}<p class="text-sm text-muted-foreground">
|
||||||
>
|
Use at least 12 characters.
|
||||||
{#if mode === 'register'}<p class="text-sm text-gray-600">Use at least 12 characters.</p>{/if}
|
</p>{/if}
|
||||||
<button
|
</div>
|
||||||
disabled={!ready || busy}
|
<Button type="submit" disabled={!ready || busy} class="w-full"
|
||||||
class="rounded bg-[var(--site-text)] px-4 py-2 font-semibold text-[var(--site-bg)] disabled:opacity-50"
|
>{busy ? 'please wait…' : mode === 'register' ? 'create account' : 'sign in'}</Button
|
||||||
>{busy ? 'please wait…' : mode === 'register' ? 'create account' : 'sign in'}</button
|
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
{#if providers.google || providers.osu}
|
{#if providers.google || providers.osu}
|
||||||
<div class="my-6 flex flex-col gap-3 border-t border-[var(--site-border)] pt-4">
|
<div class="my-6 flex flex-col gap-3">
|
||||||
{#if providers.google}<button
|
<Separator />
|
||||||
|
{#if providers.google}<Button
|
||||||
|
variant="outline"
|
||||||
disabled={!ready || busy}
|
disabled={!ready || busy}
|
||||||
onclick={() => social('google')}
|
onclick={() => social('google')}>Continue with Google</Button
|
||||||
class="rounded border border-[var(--site-border)] px-4 py-2 disabled:opacity-50"
|
|
||||||
>Continue with Google</button
|
|
||||||
>{/if}
|
>{/if}
|
||||||
{#if providers.osu}<button
|
{#if providers.osu}<Button
|
||||||
|
variant="outline"
|
||||||
disabled={!ready || busy}
|
disabled={!ready || busy}
|
||||||
onclick={() => social('osu')}
|
onclick={() => social('osu')}>Continue with osu!</Button
|
||||||
class="rounded border border-[var(--site-border)] px-4 py-2 disabled:opacity-50"
|
|
||||||
>Continue with osu!</button
|
|
||||||
>{/if}
|
>{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -146,4 +156,6 @@
|
|||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<noscript><p class="mt-4">Enable JavaScript to securely sign in or register.</p></noscript>
|
<noscript><p class="mt-4">Enable JavaScript to securely sign in or register.</p></noscript>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
54
src/lib/components/ProjectIcon.svelte
Normal 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}
|
||||||
20
src/lib/components/ui/alert/alert-action.svelte
Normal 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>
|
||||||
23
src/lib/components/ui/alert/alert-description.svelte
Normal 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>
|
||||||
23
src/lib/components/ui/alert/alert-title.svelte
Normal 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>
|
||||||
44
src/lib/components/ui/alert/alert.svelte
Normal 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>
|
||||||
17
src/lib/components/ui/alert/index.ts
Normal 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
|
||||||
|
};
|
||||||
50
src/lib/components/ui/badge/badge.svelte
Normal 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>
|
||||||
2
src/lib/components/ui/badge/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Badge } from './badge.svelte';
|
||||||
|
export { badgeVariants, type BadgeVariant } from './badge.svelte';
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
20
src/lib/components/ui/card/card-action.svelte
Normal 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>
|
||||||
20
src/lib/components/ui/card/card-content.svelte
Normal 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>
|
||||||
20
src/lib/components/ui/card/card-description.svelte
Normal 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>
|
||||||
23
src/lib/components/ui/card/card-footer.svelte
Normal 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>
|
||||||
23
src/lib/components/ui/card/card-header.svelte
Normal 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>
|
||||||
23
src/lib/components/ui/card/card-title.svelte
Normal 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>
|
||||||
25
src/lib/components/ui/card/card.svelte
Normal 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>
|
||||||
25
src/lib/components/ui/card/index.ts
Normal 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
|
||||||
|
};
|
||||||
39
src/lib/components/ui/checkbox/checkbox.svelte
Normal 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>
|
||||||
6
src/lib/components/ui/checkbox/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import Root from './checkbox.svelte';
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Checkbox
|
||||||
|
};
|
||||||
@@ -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)}
|
||||||
|
{showCloseButton}
|
||||||
|
{portalProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<Command {...restProps} bind:value bind:ref {children} />
|
||||||
</Command>
|
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
class="bg-input/30 border-input/30 h-8! rounded-lg! shadow-none! *:data-[slot=input-group-addon]:pl-2!"
|
||||||
|
>
|
||||||
<CommandPrimitive.Input
|
<CommandPrimitive.Input
|
||||||
|
{value}
|
||||||
|
data-slot="command-input"
|
||||||
class={cn(
|
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',
|
'w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...restProps}
|
||||||
bind:value
|
>
|
||||||
/>
|
{#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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
20
src/lib/components/ui/command/command-link-item.svelte
Normal 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}
|
||||||
|
/>
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
|||||||
7
src/lib/components/ui/command/command-loading.svelte
Normal 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} />
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
11
src/lib/components/ui/dialog/dialog-close.svelte
Normal 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} />
|
||||||
@@ -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}>
|
||||||
|
<XIcon />
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
|
{/if}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</Dialog.Portal>
|
</DialogPortal>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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>
|
/>
|
||||||
|
|||||||
11
src/lib/components/ui/dialog/dialog-trigger.svelte
Normal 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} />
|
||||||
7
src/lib/components/ui/dialog/dialog.svelte
Normal 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} />
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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} />
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let { ...restProps }: DropdownMenuPrimitive.PortalProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Portal {...restProps} />
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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>
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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>
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
@@ -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>
|
||||||
@@ -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} />
|
||||||
@@ -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} />
|
||||||
7
src/lib/components/ui/dropdown-menu/dropdown-menu.svelte
Normal 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} />
|
||||||
54
src/lib/components/ui/dropdown-menu/index.ts
Normal 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
|
||||||
|
};
|
||||||
22
src/lib/components/ui/input-group/index.ts
Normal 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
|
||||||
|
};
|
||||||
53
src/lib/components/ui/input-group/input-group-addon.svelte
Normal 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>
|
||||||
49
src/lib/components/ui/input-group/input-group-button.svelte
Normal 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>
|
||||||
23
src/lib/components/ui/input-group/input-group-input.svelte
Normal 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}
|
||||||
|
/>
|
||||||
22
src/lib/components/ui/input-group/input-group-text.svelte
Normal 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>
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
24
src/lib/components/ui/input-group/input-group.svelte
Normal 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>
|
||||||
7
src/lib/components/ui/input/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from './input.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Input
|
||||||
|
};
|
||||||
48
src/lib/components/ui/input/input.svelte
Normal 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}
|
||||||
7
src/lib/components/ui/label/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from './label.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Label
|
||||||
|
};
|
||||||
20
src/lib/components/ui/label/label.svelte
Normal 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}
|
||||||
|
/>
|
||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
7
src/lib/components/ui/popover/popover-close.svelte
Normal 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} />
|
||||||
@@ -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
|
||||||
|
data-slot="popover-content"
|
||||||
|
{sideOffset}
|
||||||
|
{align}
|
||||||
class={cn(
|
class={cn(
|
||||||
'bg-popover text-popover-foreground z-50 w-72 rounded-md border p-4 shadow-md outline-none',
|
'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
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...restProps}
|
||||||
>
|
/>
|
||||||
<slot />
|
</PopoverPortal>
|
||||||
</PopoverPrimitive.Content>
|
|
||||||
|
|||||||
20
src/lib/components/ui/popover/popover-description.svelte
Normal 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>
|
||||||
20
src/lib/components/ui/popover/popover-header.svelte
Normal 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>
|
||||||
7
src/lib/components/ui/popover/popover-portal.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let { ...restProps }: PopoverPrimitive.PortalProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PopoverPrimitive.Portal {...restProps} />
|
||||||
15
src/lib/components/ui/popover/popover-title.svelte
Normal 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>
|
||||||
17
src/lib/components/ui/popover/popover-trigger.svelte
Normal 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}
|
||||||
|
/>
|
||||||