1 Commits

Author SHA1 Message Date
7f5bfc7a71 ai-vibe 2026-02-05 14:44:54 +07:00
68 changed files with 2658 additions and 2860 deletions

273
bun.lock
View File

@@ -5,36 +5,37 @@
"": {
"name": "holistream",
"dependencies": {
"@aws-sdk/client-s3": "^3.971.0",
"@aws-sdk/s3-presigned-post": "^3.971.0",
"@aws-sdk/s3-request-presigner": "^3.971.0",
"@hiogawa/tiny-rpc": "^0.2.3-pre.18",
"@aws-sdk/client-s3": "^3.983.0",
"@aws-sdk/s3-presigned-post": "^3.983.0",
"@aws-sdk/s3-request-presigner": "^3.983.0",
"@hiogawa/utils": "^1.7.0",
"@pinia/colada": "^0.21.2",
"@tanstack/vue-form": "^1.28.0",
"@tanstack/vue-table": "^8.21.3",
"@tanstack/zod-form-adapter": "^0.42.1",
"@unhead/vue": "^2.1.2",
"@vueuse/core": "^14.1.0",
"@vueuse/core": "^14.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"hono": "^4.11.4",
"hono": "^4.11.7",
"is-mobile": "^5.0.0",
"pinia": "^3.0.4",
"tailwind-merge": "^3.4.0",
"vue": "^3.5.27",
"vue-router": "^5.0.2",
"zod": "^4.3.5",
"zod": "^3.25.76",
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.21.0",
"@primevue/auto-import-resolver": "^4.5.4",
"@types/node": "^25.0.9",
"@vitejs/plugin-vue": "^6.0.3",
"@vitejs/plugin-vue-jsx": "^5.1.3",
"@cloudflare/vite-plugin": "^1.23.0",
"@types/node": "^25.2.0",
"@vitejs/plugin-vue": "^6.0.4",
"@vitejs/plugin-vue-jsx": "^5.1.4",
"unocss": "^66.6.0",
"unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^31.0.0",
"vite": "^7.3.1",
"vite-ssr-components": "^0.5.2",
"wrangler": "^4.59.2",
"wrangler": "^4.62.0",
},
},
},
@@ -55,87 +56,87 @@
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
"@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.971.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.970.0", "@aws-sdk/credential-provider-node": "3.971.0", "@aws-sdk/middleware-bucket-endpoint": "3.969.0", "@aws-sdk/middleware-expect-continue": "3.969.0", "@aws-sdk/middleware-flexible-checksums": "3.971.0", "@aws-sdk/middleware-host-header": "3.969.0", "@aws-sdk/middleware-location-constraint": "3.969.0", "@aws-sdk/middleware-logger": "3.969.0", "@aws-sdk/middleware-recursion-detection": "3.969.0", "@aws-sdk/middleware-sdk-s3": "3.970.0", "@aws-sdk/middleware-ssec": "3.971.0", "@aws-sdk/middleware-user-agent": "3.970.0", "@aws-sdk/region-config-resolver": "3.969.0", "@aws-sdk/signature-v4-multi-region": "3.970.0", "@aws-sdk/types": "3.969.0", "@aws-sdk/util-endpoints": "3.970.0", "@aws-sdk/util-user-agent-browser": "3.969.0", "@aws-sdk/util-user-agent-node": "3.971.0", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.20.6", "@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/eventstream-serde-config-resolver": "^4.3.8", "@smithy/eventstream-serde-node": "^4.2.8", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-blob-browser": "^4.2.9", "@smithy/hash-node": "^4.2.8", "@smithy/hash-stream-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/md5-js": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.7", "@smithy/middleware-retry": "^4.4.23", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.22", "@smithy/util-defaults-mode-node": "^4.2.25", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-BBUne390fKa4C4QvZlUZ5gKcu+Uyid4IyQ20N4jl0vS7SK2xpfXlJcgKqPW5ts6kx6hWTQBk6sH5Lf12RvuJxg=="],
"@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.983.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.6", "@aws-sdk/credential-provider-node": "^3.972.5", "@aws-sdk/middleware-bucket-endpoint": "^3.972.3", "@aws-sdk/middleware-expect-continue": "^3.972.3", "@aws-sdk/middleware-flexible-checksums": "^3.972.4", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-location-constraint": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-sdk-s3": "^3.972.6", "@aws-sdk/middleware-ssec": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.6", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/signature-v4-multi-region": "3.983.0", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.983.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.4", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.22.0", "@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/eventstream-serde-config-resolver": "^4.3.8", "@smithy/eventstream-serde-node": "^4.2.8", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-blob-browser": "^4.2.9", "@smithy/hash-node": "^4.2.8", "@smithy/hash-stream-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/md5-js": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.12", "@smithy/middleware-retry": "^4.4.29", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.28", "@smithy/util-defaults-mode-node": "^4.2.31", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-V40PT2irPh3lj+Z95tZI6batVrjaTrWEOXRNVBuoZSgpM3Ak1jiE9ZXwVLkMcbb9/GH4xVpB3EsGM7gbxmgFLQ=="],
"@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.971.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.970.0", "@aws-sdk/middleware-host-header": "3.969.0", "@aws-sdk/middleware-logger": "3.969.0", "@aws-sdk/middleware-recursion-detection": "3.969.0", "@aws-sdk/middleware-user-agent": "3.970.0", "@aws-sdk/region-config-resolver": "3.969.0", "@aws-sdk/types": "3.969.0", "@aws-sdk/util-endpoints": "3.970.0", "@aws-sdk/util-user-agent-browser": "3.969.0", "@aws-sdk/util-user-agent-node": "3.971.0", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.20.6", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.7", "@smithy/middleware-retry": "^4.4.23", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.22", "@smithy/util-defaults-mode-node": "^4.2.25", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Xx+w6DQqJxDdymYyIxyKJnRzPvVJ4e/Aw0czO7aC9L/iraaV7AG8QtRe93OGW6aoHSh72CIiinnpJJfLsQqP4g=="],
"@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.982.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.6", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.6", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.982.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.4", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.22.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.12", "@smithy/middleware-retry": "^4.4.29", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.28", "@smithy/util-defaults-mode-node": "^4.2.31", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qJrIiivmvujdGqJ0ldSUvhN3k3N7GtPesoOI1BSt0fNXovVnMz4C/JmnkhZihU7hJhDvxJaBROLYTU+lpild4w=="],
"@aws-sdk/core": ["@aws-sdk/core@3.970.0", "", { "dependencies": { "@aws-sdk/types": "3.969.0", "@aws-sdk/xml-builder": "3.969.0", "@smithy/core": "^3.20.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-klpzObldOq8HXzDjDlY6K8rMhYZU6mXRz6P9F9N+tWnjoYFfeBMra8wYApydElTUYQKP1O7RLHwH1OKFfKcqIA=="],
"@aws-sdk/core": ["@aws-sdk/core@3.973.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.4", "@smithy/core": "^3.22.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-pz4ZOw3BLG0NdF25HoB9ymSYyPbMiIjwQJ2aROXRhAzt+b+EOxStfFv8s5iZyP6Kiw7aYhyWxj5G3NhmkoOTKw=="],
"@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.969.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-IGNkP54HD3uuLnrPCYsv3ZD478UYq+9WwKrIVJ9Pdi3hxPg8562CH3ZHf8hEgfePN31P9Kj+Zu9kq2Qcjjt61A=="],
"@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw=="],
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.970.0", "", { "dependencies": { "@aws-sdk/core": "3.970.0", "@aws-sdk/types": "3.969.0", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-rtVzXzEtAfZBfh+lq3DAvRar4c3jyptweOAJR2DweyXx71QSMY+O879hjpMwES7jl07a3O1zlnFIDo4KP/96kQ=="],
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.4", "", { "dependencies": { "@aws-sdk/core": "^3.973.6", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-/8dnc7+XNMmViEom2xsNdArQxQPSgy4Z/lm6qaFPTrMFesT1bV3PsBhb19n09nmxHdrtQskYmViddUIjUQElXg=="],
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.970.0", "", { "dependencies": { "@aws-sdk/core": "3.970.0", "@aws-sdk/types": "3.969.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.10", "tslib": "^2.6.2" } }, "sha512-CjDbWL7JxjLc9ZxQilMusWSw05yRvUJKRpz59IxDpWUnSMHC9JMMUUkOy5Izk8UAtzi6gupRWArp4NG4labt9Q=="],
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.6", "", { "dependencies": { "@aws-sdk/core": "^3.973.6", "@aws-sdk/types": "^3.973.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.10", "tslib": "^2.6.2" } }, "sha512-5ERWqRljiZv44AIdvIRQ3k+EAV0Sq2WeJHvXuK7gL7bovSxOf8Al7MLH7Eh3rdovH4KHFnlIty7J71mzvQBl5Q=="],
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.971.0", "", { "dependencies": { "@aws-sdk/core": "3.970.0", "@aws-sdk/credential-provider-env": "3.970.0", "@aws-sdk/credential-provider-http": "3.970.0", "@aws-sdk/credential-provider-login": "3.971.0", "@aws-sdk/credential-provider-process": "3.970.0", "@aws-sdk/credential-provider-sso": "3.971.0", "@aws-sdk/credential-provider-web-identity": "3.971.0", "@aws-sdk/nested-clients": "3.971.0", "@aws-sdk/types": "3.969.0", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-c0TGJG4xyfTZz3SInXfGU8i5iOFRrLmy4Bo7lMyH+IpngohYMYGYl61omXqf2zdwMbDv+YJ9AviQTcCaEUKi8w=="],
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.4", "", { "dependencies": { "@aws-sdk/core": "^3.973.6", "@aws-sdk/credential-provider-env": "^3.972.4", "@aws-sdk/credential-provider-http": "^3.972.6", "@aws-sdk/credential-provider-login": "^3.972.4", "@aws-sdk/credential-provider-process": "^3.972.4", "@aws-sdk/credential-provider-sso": "^3.972.4", "@aws-sdk/credential-provider-web-identity": "^3.972.4", "@aws-sdk/nested-clients": "3.982.0", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-eRUg+3HaUKuXWn/lEMirdiA5HOKmEl8hEHVuszIDt2MMBUKgVX5XNGmb3XmbgU17h6DZ+RtjbxQpjhz3SbTjZg=="],
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.971.0", "", { "dependencies": { "@aws-sdk/core": "3.970.0", "@aws-sdk/nested-clients": "3.971.0", "@aws-sdk/types": "3.969.0", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-yhbzmDOsk0RXD3rTPhZra4AWVnVAC4nFWbTp+sUty1hrOPurUmhuz8bjpLqYTHGnlMbJp+UqkQONhS2+2LzW2g=="],
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.4", "", { "dependencies": { "@aws-sdk/core": "^3.973.6", "@aws-sdk/nested-clients": "3.982.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-nLGjXuvWWDlQAp505xIONI7Gam0vw2p7Qu3P6on/W2q7rjJXtYjtpHbcsaOjJ/pAju3eTvEQuSuRedcRHVQIAQ=="],
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.971.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.970.0", "@aws-sdk/credential-provider-http": "3.970.0", "@aws-sdk/credential-provider-ini": "3.971.0", "@aws-sdk/credential-provider-process": "3.970.0", "@aws-sdk/credential-provider-sso": "3.971.0", "@aws-sdk/credential-provider-web-identity": "3.971.0", "@aws-sdk/types": "3.969.0", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-epUJBAKivtJqalnEBRsYIULKYV063o/5mXNJshZfyvkAgNIzc27CmmKRXTN4zaNOZg8g/UprFp25BGsi19x3nQ=="],
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.5", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.4", "@aws-sdk/credential-provider-http": "^3.972.6", "@aws-sdk/credential-provider-ini": "^3.972.4", "@aws-sdk/credential-provider-process": "^3.972.4", "@aws-sdk/credential-provider-sso": "^3.972.4", "@aws-sdk/credential-provider-web-identity": "^3.972.4", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-VWXKgSISQCI2GKN3zakTNHSiZ0+mux7v6YHmmbLQp/o3fvYUQJmKGcLZZzg2GFA+tGGBStplra9VFNf/WwxpYg=="],
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.970.0", "", { "dependencies": { "@aws-sdk/core": "3.970.0", "@aws-sdk/types": "3.969.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-0XeT8OaT9iMA62DFV9+m6mZfJhrD0WNKf4IvsIpj2Z7XbaYfz3CoDDvNoALf3rPY9NzyMHgDxOspmqdvXP00mw=="],
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.4", "", { "dependencies": { "@aws-sdk/core": "^3.973.6", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-TCZpWUnBQN1YPk6grvd5x419OfXjHvhj5Oj44GYb84dOVChpg/+2VoEj+YVA4F4E/6huQPNnX7UYbTtxJqgihw=="],
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.971.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.971.0", "@aws-sdk/core": "3.970.0", "@aws-sdk/token-providers": "3.971.0", "@aws-sdk/types": "3.969.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-dY0hMQ7dLVPQNJ8GyqXADxa9w5wNfmukgQniLxGVn+dMRx3YLViMp5ZpTSQpFhCWNF0oKQrYAI5cHhUJU1hETw=="],
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.4", "", { "dependencies": { "@aws-sdk/client-sso": "3.982.0", "@aws-sdk/core": "^3.973.6", "@aws-sdk/token-providers": "3.982.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-wzsGwv9mKlwJ3vHLyembBvGE/5nPUIwRR2I51B1cBV4Cb4ql9nIIfpmHzm050XYTY5fqTOKJQnhLj7zj89VG8g=="],
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.971.0", "", { "dependencies": { "@aws-sdk/core": "3.970.0", "@aws-sdk/nested-clients": "3.971.0", "@aws-sdk/types": "3.969.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-F1AwfNLr7H52T640LNON/h34YDiMuIqW/ZreGzhRR6vnFGaSPtNSKAKB2ssAMkLM8EVg8MjEAYD3NCUiEo+t/w=="],
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.4", "", { "dependencies": { "@aws-sdk/core": "^3.973.6", "@aws-sdk/nested-clients": "3.982.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-hIzw2XzrG8jzsUSEatehmpkd5rWzASg5IHUfA+m01k/RtvfAML7ZJVVohuKdhAYx+wV2AThLiQJVzqn7F0khrw=="],
"@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.969.0", "", { "dependencies": { "@aws-sdk/types": "3.969.0", "@aws-sdk/util-arn-parser": "3.968.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-MlbrlixtkTVhYhoasblKOkr7n2yydvUZjjxTnBhIuHmkyBS1619oGnTfq/uLeGYb4NYXdeQ5OYcqsRGvmWSuTw=="],
"@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-arn-parser": "^3.972.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-fmbgWYirF67YF1GfD7cg5N6HHQ96EyRNx/rDIrTF277/zTWVuPI2qS/ZHgofwR1NZPe/NWvoppflQY01LrbVLg=="],
"@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.969.0", "", { "dependencies": { "@aws-sdk/types": "3.969.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-qXygzSi8osok7tH9oeuS3HoKw6jRfbvg5Me/X5RlHOvSSqQz8c5O9f3MjUApaCUSwbAU92KrbZWasw2PKiaVHg=="],
"@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-4msC33RZsXQpUKR5QR4HnvBSNCPLGHmB55oDiROqqgyOc+TOfVu2xgi5goA7ms6MdZLeEh2905UfWMnMMF4mRg=="],
"@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.971.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "3.970.0", "@aws-sdk/crc64-nvme": "3.969.0", "@aws-sdk/types": "3.969.0", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-+hGUDUxeIw8s2kkjfeXym0XZxdh0cqkHkDpEanWYdS1gnWkIR+gf9u/DKbKqGHXILPaqHXhWpLTQTVlaB4sI7Q=="],
"@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.972.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.6", "@aws-sdk/crc64-nvme": "3.972.0", "@aws-sdk/types": "^3.973.1", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-xOxsUkF3O3BtIe3tf54OpPo94eZepjFm3z0Dd2TZKbsPxMiRTFXurC04wJ58o/wPW9YHVO9VqZik3MfoPfrKlw=="],
"@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.969.0", "", { "dependencies": { "@aws-sdk/types": "3.969.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-AWa4rVsAfBR4xqm7pybQ8sUNJYnjyP/bJjfAw34qPuh3M9XrfGbAHG0aiAfQGrBnmS28jlO6Kz69o+c6PRw1dw=="],
"@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA=="],
"@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.969.0", "", { "dependencies": { "@aws-sdk/types": "3.969.0", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-zH7pDfMLG/C4GWMOpvJEoYcSpj7XsNP9+irlgqwi667sUQ6doHQJ3yyDut3yiTk0maq1VgmriPFELyI9lrvH/g=="],
"@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-nIg64CVrsXp67vbK0U1/Is8rik3huS3QkRHn2DRDx4NldrEFMgdkZGI/+cZMKD9k4YOS110Dfu21KZLHrFA/1g=="],
"@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.969.0", "", { "dependencies": { "@aws-sdk/types": "3.969.0", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-xwrxfip7Y2iTtCMJ+iifN1E1XMOuhxIHY9DreMCvgdl4r7+48x2S1bCYPWH3eNY85/7CapBWdJ8cerpEl12sQQ=="],
"@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA=="],
"@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.969.0", "", { "dependencies": { "@aws-sdk/types": "3.969.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-2r3PuNquU3CcS1Am4vn/KHFwLi8QFjMdA/R+CRDXT4AFO/0qxevF/YStW3gAKntQIgWgQV8ZdEtKAoJvLI4UWg=="],
"@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q=="],
"@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.970.0", "", { "dependencies": { "@aws-sdk/core": "3.970.0", "@aws-sdk/types": "3.969.0", "@aws-sdk/util-arn-parser": "3.968.0", "@smithy/core": "^3.20.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-v/Y5F1lbFFY7vMeG5yYxuhnn0CAshz6KMxkz1pDyPxejNE9HtA0w8R6OTBh/bVdIm44QpjhbI7qeLdOE/PLzXQ=="],
"@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.6", "", { "dependencies": { "@aws-sdk/core": "^3.973.6", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-arn-parser": "^3.972.2", "@smithy/core": "^3.22.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Xq7wM6kbgJN1UO++8dvH/efPb1nTwWqFCpZCR7RCLOETP7xAUAhVo7JmsCnML5Di/iC4Oo5VrJ4QmkYcMZniLw=="],
"@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.971.0", "", { "dependencies": { "@aws-sdk/types": "3.969.0", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-QGVhvRveYG64ZhnS/b971PxXM6N2NU79Fxck4EfQ7am8v1Br0ctoeDDAn9nXNblLGw87we9Z65F7hMxxiFHd3w=="],
"@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-dU6kDuULN3o3jEHcjm0c4zWJlY1zWVkjG9NPe9qxYLLpcbdj5kRYBS2DdWYD+1B9f910DezRuws7xDEqKkHQIg=="],
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.970.0", "", { "dependencies": { "@aws-sdk/core": "3.970.0", "@aws-sdk/types": "3.969.0", "@aws-sdk/util-endpoints": "3.970.0", "@smithy/core": "^3.20.6", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-dnSJGGUGSFGEX2NzvjwSefH+hmZQ347AwbLhAsi0cdnISSge+pcGfOFrJt2XfBIypwFe27chQhlfuf/gWdzpZg=="],
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.6", "", { "dependencies": { "@aws-sdk/core": "^3.973.6", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.982.0", "@smithy/core": "^3.22.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-TehLN8W/kivl0U9HcS+keryElEWORROpghDXZBLfnb40DXM7hx/i+7OOjkogXQOF3QtUraJVRkHQ07bPhrWKlw=="],
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.971.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.970.0", "@aws-sdk/middleware-host-header": "3.969.0", "@aws-sdk/middleware-logger": "3.969.0", "@aws-sdk/middleware-recursion-detection": "3.969.0", "@aws-sdk/middleware-user-agent": "3.970.0", "@aws-sdk/region-config-resolver": "3.969.0", "@aws-sdk/types": "3.969.0", "@aws-sdk/util-endpoints": "3.970.0", "@aws-sdk/util-user-agent-browser": "3.969.0", "@aws-sdk/util-user-agent-node": "3.971.0", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.20.6", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.7", "@smithy/middleware-retry": "^4.4.23", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.22", "@smithy/util-defaults-mode-node": "^4.2.25", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-TWaILL8GyYlhGrxxnmbkazM4QsXatwQgoWUvo251FXmUOsiXDFDVX3hoGIfB3CaJhV2pJPfebHUNJtY6TjZ11g=="],
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.982.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.6", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.6", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.982.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.4", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.22.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.12", "@smithy/middleware-retry": "^4.4.29", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.28", "@smithy/util-defaults-mode-node": "^4.2.31", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-VVkaH27digrJfdVrT64rjkllvOp4oRiZuuJvrylLXAKl18ujToJR7AqpDldL/LS63RVne3QWIpkygIymxFtliQ=="],
"@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.969.0", "", { "dependencies": { "@aws-sdk/types": "3.969.0", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-scj9OXqKpcjJ4jsFLtqYWz3IaNvNOQTFFvEY8XMJXTv+3qF5I7/x9SJtKzTRJEBF3spjzBUYPtGFbs9sj4fisQ=="],
"@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow=="],
"@aws-sdk/s3-presigned-post": ["@aws-sdk/s3-presigned-post@3.971.0", "", { "dependencies": { "@aws-sdk/client-s3": "3.971.0", "@aws-sdk/types": "3.969.0", "@aws-sdk/util-format-url": "3.969.0", "@smithy/middleware-endpoint": "^4.4.7", "@smithy/signature-v4": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ww5DGQKa/LCIP2q8icpnKBXjAYpHvvhoj2vdETqfSLKBBytAy76OSwsaAKUNFDnHJiSEs4+XSCcvxntC6c/hIQ=="],
"@aws-sdk/s3-presigned-post": ["@aws-sdk/s3-presigned-post@3.983.0", "", { "dependencies": { "@aws-sdk/client-s3": "3.983.0", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-format-url": "^3.972.3", "@smithy/middleware-endpoint": "^4.4.12", "@smithy/signature-v4": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-bzJJMLleqsCAOXCTldBweB2+crpfeTies0lwrTjX9q6o23rbfkmsp/OYzWGolWS+y/c2MqAp07f27NCeFY3Ayw=="],
"@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.971.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.970.0", "@aws-sdk/types": "3.969.0", "@aws-sdk/util-format-url": "3.969.0", "@smithy/middleware-endpoint": "^4.4.7", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-j4wCCoQ//xm03JQn7/Jq6BJ0HV3VzlI/HrIQSQupWWjZTrdxyqa9PXBhcYNNtvZtF1adA/cRpYTMS+2SUsZGRg=="],
"@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.983.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.983.0", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-format-url": "^3.972.3", "@smithy/middleware-endpoint": "^4.4.12", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-O/PMdYcCVuBnOyuYglNvaNbh8RWMSE0zBwA8H6m+2VZbtLdALvyXgW5m6Q7wbnKdj+a4bJXh3AStDq6a82TA6Q=="],
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.970.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.970.0", "@aws-sdk/types": "3.969.0", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-z3syXfuK/x/IsKf/AeYmgc2NT7fcJ+3fHaGO+fkghkV9WEba3fPyOwtTBX4KpFMNb2t50zDGZwbzW1/5ighcUQ=="],
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.983.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.6", "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-11FCcxI/WKRufKDdPgKPXtrhjDArhkOPb4mf66rICZUnPHlD8Cb7cjZZS/eFC+iuwoHBosrxo0hYsvK3s7DxGw=="],
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.971.0", "", { "dependencies": { "@aws-sdk/core": "3.970.0", "@aws-sdk/nested-clients": "3.971.0", "@aws-sdk/types": "3.969.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-4hKGWZbmuDdONMJV0HJ+9jwTDb0zLfKxcCLx2GEnBY31Gt9GeyIQ+DZ97Bb++0voawj6pnZToFikXTyrEq2x+w=="],
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.982.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.6", "@aws-sdk/nested-clients": "3.982.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-v3M0KYp2TVHYHNBT7jHD9lLTWAdS9CaWJ2jboRKt0WAB65bA7iUEpR+k4VqKYtpQN4+8kKSc4w+K6kUNZkHKQw=="],
"@aws-sdk/types": ["@aws-sdk/types@3.969.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-7IIzM5TdiXn+VtgPdVLjmE6uUBUtnga0f4RiSEI1WW10RPuNvZ9U+pL3SwDiRDAdoGrOF9tSLJOFZmfuwYuVYQ=="],
"@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="],
"@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.968.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-gqqvYcitIIM2K4lrDX9de9YvOfXBcVdxfT/iLnvHJd4YHvSXlt+gs+AsL4FfPCxG4IG9A+FyulP9Sb1MEA75vw=="],
"@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg=="],
"@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.970.0", "", { "dependencies": { "@aws-sdk/types": "3.969.0", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-TZNZqFcMUtjvhZoZRtpEGQAdULYiy6rcGiXAbLU7e9LSpIYlRqpLa207oMNfgbzlL2PnHko+eVg8rajDiSOYCg=="],
"@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.983.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-t/VbL2X3gvDEjC4gdySOeFFOZGQEBKwa23pRHeB7hBLBZ119BB/2OEFtTFWKyp3bnMQgxpeVeGS7/hxk6wpKJw=="],
"@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.969.0", "", { "dependencies": { "@aws-sdk/types": "3.969.0", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-C7ZiE8orcrEF9In+XDlIKrZhMjp0HCPUH6u74pgadE3T2LRre5TmOQcTt785/wVS2G0we9cxkjlzMrfDsfPvFw=="],
"@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-n7F2ycckcKFXa01vAsT/SJdjFHfKH9s96QHcs5gn8AaaigASICeME8WdUL9uBp8XV/OVwEt8+6gzn6KFUgQa8g=="],
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-qKgO7wAYsXzhwCHhdbaKFyxd83Fgs8/1Ka+jjSPrv2Ll7mB55Wbwlo0kkfMLh993/yEc8aoDIAc1Fz9h4Spi4Q=="],
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.4", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog=="],
"@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.969.0", "", { "dependencies": { "@aws-sdk/types": "3.969.0", "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-bpJGjuKmFr0rA6UKUCmN8D19HQFMLXMx5hKBXqBlPFdalMhxJSjcxzX9DbQh0Fn6bJtxCguFmRGOBdQqNOt49g=="],
"@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw=="],
"@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.971.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.970.0", "@aws-sdk/types": "3.969.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-Eygjo9mFzQYjbGY3MYO6CsIhnTwAMd3WmuFalCykqEmj2r5zf0leWrhPaqvA5P68V5JdGfPYgj7vhNOd6CtRBQ=="],
"@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.972.4", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.6", "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-3WFCBLiM8QiHDfosQq3Py+lIMgWlFWwFQliUHUqwEiRqLnKyhgbU3AKa7AWJF7lW2Oc/2kFNY4MlAYVnVc0i8A=="],
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.969.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-BSe4Lx/qdRQQdX8cSSI7Et20vqBspzAjBy8ZmXVoyLkol3y4sXBXzn+BiLtR+oh60ExQn6o2DU4QjdOZbXaKIQ=="],
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.4", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.4", "tslib": "^2.6.2" } }, "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q=="],
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="],
"@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="],
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="],
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
"@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="],
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
"@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="],
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
@@ -167,7 +168,7 @@
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
"@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="],
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="],
@@ -177,25 +178,25 @@
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
"@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="],
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
"@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="],
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="],
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.10.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20251221.0" }, "optionalPeers": ["workerd"] }, "sha512-/uII4vLQXhzCAZzEVeYAjFLBNg2nqTJ1JGzd2lRF6ItYe6U2zVoYGfeKpGx/EkBF6euiU+cyBXgMdtJih+nQ6g=="],
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.12.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20260115.0" }, "optionalPeers": ["workerd"] }, "sha512-NK4vN+2Z/GbfGS4BamtbbVk1rcu5RmqaYGiyHJQrA09AoxdZPHDF3W/EhgI0YSK8p3vRo/VNCtbSJFPON7FWMQ=="],
"@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.21.0", "", { "dependencies": { "@cloudflare/unenv-preset": "2.10.0", "miniflare": "4.20260114.0", "unenv": "2.0.0-rc.24", "wrangler": "4.59.2", "ws": "8.18.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0" } }, "sha512-3VXtkfjOQL+k3Plj+t0BHRyw8iIIRBQ8RJU6KJHJQKdYHA6rJE/WlSa/lRd0A8MMhvP8e8QiMLuDqveEN8gCZg=="],
"@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.23.0", "", { "dependencies": { "@cloudflare/unenv-preset": "2.12.0", "miniflare": "4.20260131.0", "unenv": "2.0.0-rc.24", "wrangler": "4.62.0", "ws": "8.18.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0" } }, "sha512-Pz3kF5wxUx99NOOYPq/jgaknKQuamN52FQkc8WBmLfbzBd9fWu+4NaJeZjDtFTXUBA0FEA7bOROuV52YFOA2TA=="],
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260114.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-HNlsRkfNgardCig2P/5bp/dqDECsZ4+NU5XewqArWxMseqt3C5daSuptI620s4pn7Wr0ZKg7jVLH0PDEBkA+aA=="],
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260131.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-+1X4qErc715NUhJZNhtlpuCxajhD5YNre7Cz50WPMmj+BMUrh9h7fntKEadtrUo5SM2YONY7CDzK7wdWbJJBVA=="],
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260114.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qyE1UdFnAlxzb+uCfN/d9c8icch7XRiH49/DjoqEa+bCDihTuRS7GL1RmhVIqHJhb3pX3DzxmKgQZBDBL83Inw=="],
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260131.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M84mXR8WEMEBuX4/dL2IQ4wHV/ALwYjx9if5ePZR8rdbD7if/fkEEoMBq0bGS/1gMLRqqCZLstabxHV+g92NNg=="],
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260114.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0BLvAj/JPOabzads2ddDEfgExWTlD22pnwsuNbPwZAGTSZeQa3Y47eGUWyHk+rSGngknk++S7zHTGbKuG7RRg=="],
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260131.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SWzr48bCL9y5wjkj23tXS6t/6us99EAH9T5TAscMV0hfJFZQt97RY/gaHKyRRjFv6jfJZvk7d4g+OmGeYBnwcg=="],
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260114.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-kPUmEtUxUWlr9PQ64kuhdK0qyo8idPe5IIXUgi7xCD7mDd6EOe5J7ugDpbfvfbYKEjx4DpLvN2t45izyI/Sodw=="],
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260131.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-mL0kLPGIBJRPeHS3+erJ2t5dJT3ODhsKvR9aA4BcsY7M30/QhlgJIF6wsgwNisTJ23q8PbobZNHBUKIe8l/E9A=="],
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260114.0", "", { "os": "win32", "cpu": "x64" }, "sha512-MJnKgm6i1jZGyt2ZHQYCnRlpFTEZcK2rv9y7asS3KdVEXaDgGF8kOns5u6YL6/+eMogfZuHRjfDS+UqRTUYIFA=="],
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260131.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hoQqTFBpP1zntP2OQSpt5dEWbd9vSBliK+G7LmDXjKitPkmkRFo2PB4P9aBRE1edPAIO/fpdoJv928k2HaAn4A=="],
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
@@ -253,8 +254,6 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
"@hiogawa/tiny-rpc": ["@hiogawa/tiny-rpc@0.2.3-pre.18", "", {}, "sha512-BiNHrutG9G9yV622QvkxZxF+PhkaH2Aspp4/X1KYTfnaQTcg4fFUTBWf5Kf533swon2SuVJwi6U6H1LQbhVOQQ=="],
"@hiogawa/utils": ["@hiogawa/utils@1.7.0", "", {}, "sha512-ghiEFWBR1NENoHn+lSuW7liicTIzVPN+8Srm5UedCTw43gus0mlse6Wp2lz6GmbOXJ/CalMPp/0Tz2X8tajkAg=="],
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
@@ -321,6 +320,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@pinia/colada": ["@pinia/colada@0.21.2", "", { "peerDependencies": { "pinia": "^2.2.6 || ^3.0.0", "vue": "^3.5.17" } }, "sha512-k2epk1jed5cTmNA7l00UtsFRyqw9HfyU6WO4cV0BMUT3sSE4CMLCilprbLAL5h2bxD76WSiglciI/6o+Uh7Vzw=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
"@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="],
@@ -329,63 +330,59 @@
"@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="],
"@primevue/auto-import-resolver": ["@primevue/auto-import-resolver@4.5.4", "", { "dependencies": { "@primevue/metadata": "4.5.4" } }, "sha512-YQHrZ9PQSG/4K2BwthA2Xuna4WyS0JMHajiHD9PljaDyQtBVwCadX5ZpKcrAUWR8E/1gjva8x/si0RYxxYrRJw=="],
"@primevue/metadata": ["@primevue/metadata@4.5.4", "", {}, "sha512-jJFD0KYm8bPYgFo0JP3Dc2RkyXzrMI1XHQGsEKTysx9Jx2d1XdxtFji/ZsQeoo/RmwUNof5ciZ72URq37rnK+g=="],
"@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
"@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="],
@@ -397,7 +394,7 @@
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ=="],
"@smithy/core": ["@smithy/core@3.20.6", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-BpAffW1mIyRZongoKBbh3RgHG+JDHJek/8hjA/9LnPunM+ejorO6axkxCgwxCe4K//g/JdPeR9vROHDYr/hfnQ=="],
"@smithy/core": ["@smithy/core@3.22.1", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.11", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g=="],
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw=="],
@@ -427,9 +424,9 @@
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A=="],
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.7", "", { "dependencies": { "@smithy/core": "^3.20.6", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-SCmhUG1UwtnEhF5Sxd8qk7bJwkj1BpFzFlHkXqKCEmDPLrRjJyTGM0EhqT7XBtDaDJjCfjRJQodgZcKDR843qg=="],
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.13", "", { "dependencies": { "@smithy/core": "^3.22.1", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w=="],
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.23", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.10.8", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-lLEmkQj7I7oKfvZ1wsnToGJouLOtfkMXDKRA1Hi6F+mMp5O1N8GcVWmVeNgTtgZtd0OTXDTI2vpVQmeutydGew=="],
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.30", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg=="],
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ=="],
@@ -437,7 +434,7 @@
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.8", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg=="],
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.8", "", { "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg=="],
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.9", "", { "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w=="],
"@smithy/property-provider": ["@smithy/property-provider@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w=="],
@@ -453,7 +450,7 @@
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.8", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg=="],
"@smithy/smithy-client": ["@smithy/smithy-client@4.10.8", "", { "dependencies": { "@smithy/core": "^3.20.6", "@smithy/middleware-endpoint": "^4.4.7", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.10", "tslib": "^2.6.2" } }, "sha512-wcr3UEL26k7lLoyf9eVDZoD1nNY3Fa1gbNuOXvfxvVWLGkOVW+RYZgUUp/bXHryJfycIOQnBq9o1JAE00ax8HQ=="],
"@smithy/smithy-client": ["@smithy/smithy-client@4.11.2", "", { "dependencies": { "@smithy/core": "^3.22.1", "@smithy/middleware-endpoint": "^4.4.13", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.11", "tslib": "^2.6.2" } }, "sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A=="],
"@smithy/types": ["@smithy/types@4.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="],
@@ -469,9 +466,9 @@
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="],
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.22", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.10.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-O2WXr6ZRqPnbyoepb7pKcLt1QL6uRfFzGYJ9sGb5hMJQi7v/4RjRmCQa9mNjA0YiXqsc5lBmLXqJPhjM1Vjv5A=="],
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.29", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q=="],
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.25", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.10.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-7uMhppVNRbgNIpyUBVRfjGHxygP85wpXalRvn9DvUlCx4qgy1AB/uxOPSiDx/jFyrwD3/BypQhx1JK7f3yxrAw=="],
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.32", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q=="],
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw=="],
@@ -481,7 +478,7 @@
"@smithy/util-retry": ["@smithy/util-retry@4.2.8", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg=="],
"@smithy/util-stream": ["@smithy/util-stream@4.5.10", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g=="],
"@smithy/util-stream": ["@smithy/util-stream@4.5.11", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.9", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA=="],
"@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="],
@@ -509,9 +506,11 @@
"@tanstack/vue-table": ["@tanstack/vue-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "vue": ">=3.2" } }, "sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw=="],
"@tanstack/zod-form-adapter": ["@tanstack/zod-form-adapter@0.42.1", "", { "dependencies": { "@tanstack/form-core": "0.42.1" }, "peerDependencies": { "zod": "^3.x" } }, "sha512-hPRM0lawVKP64yurW4c6KHZH6altMo2MQN14hfi+GMBTKjO9S7bW1x5LPZ5cayoJE3mBvdlahpSGT5rYZtSbXQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
"@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
@@ -565,9 +564,9 @@
"@unocss/vite": ["@unocss/vite@66.6.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@unocss/config": "66.6.0", "@unocss/core": "66.6.0", "@unocss/inspector": "66.6.0", "chokidar": "^5.0.0", "magic-string": "^0.30.21", "pathe": "^2.0.3", "tinyglobby": "^0.2.15", "unplugin-utils": "^0.3.1" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0" } }, "sha512-SC0/rX0xSjdu8Jaj98XztHOuvXHWDVk0YaHKRAQks2Oj3yyqAOrhzhDUH0zzFaQWf5bsKVYK40H+h4rMk9vm5Q=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.53" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="],
"@vitejs/plugin-vue-jsx": ["@vitejs/plugin-vue-jsx@5.1.3", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5", "@rolldown/pluginutils": "^1.0.0-beta.56", "@vue/babel-plugin-jsx": "^2.0.1" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.0.0" } }, "sha512-I6Zr8cYVr5WHMW5gNOP09DNqW9rgO8RX73Wa6Czgq/0ndpTfJM4vfDChfOT1+3KtdrNqilNBtNlFwVeB02ZzGw=="],
"@vitejs/plugin-vue-jsx": ["@vitejs/plugin-vue-jsx@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-syntax-typescript": "^7.28.6", "@babel/plugin-transform-typescript": "^7.28.6", "@rolldown/pluginutils": "^1.0.0-rc.2", "@vue/babel-plugin-jsx": "^2.0.1" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.0.0" } }, "sha512-70LmoVk9riR7qc4W2CpjsbNMWTPnuZb9dpFKX1emru0yP57nsc9k8nhLA6U93ngQapv5VDIUq2JatNfLbBIkrA=="],
"@vue-macros/common": ["@vue-macros/common@3.1.2", "", { "dependencies": { "@vue/compiler-sfc": "^3.5.22", "ast-kit": "^2.1.2", "local-pkg": "^1.1.2", "magic-string-ast": "^1.0.2", "unplugin-utils": "^0.3.0" }, "peerDependencies": { "vue": "^2.7.0 || ^3.2.25" }, "optionalPeers": ["vue"] }, "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng=="],
@@ -601,11 +600,11 @@
"@vue/shared": ["@vue/shared@3.5.27", "", {}, "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ=="],
"@vueuse/core": ["@vueuse/core@14.1.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.1.0", "@vueuse/shared": "14.1.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw=="],
"@vueuse/core": ["@vueuse/core@14.2.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.2.0", "@vueuse/shared": "14.2.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-tpjzVl7KCQNVd/qcaCE9XbejL38V6KJAEq/tVXj7mDPtl6JtzmUdnXelSS+ULRkkrDgzYVK7EerQJvd2jR794Q=="],
"@vueuse/metadata": ["@vueuse/metadata@14.1.0", "", {}, "sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA=="],
"@vueuse/metadata": ["@vueuse/metadata@14.2.0", "", {}, "sha512-i3axTGjU8b13FtyR4Keeama+43iD+BwX9C2TmzBVKqjSHArF03hjkp2SBZ1m72Jk2UtrX0aYCugBq2R1fhkuAQ=="],
"@vueuse/shared": ["@vueuse/shared@14.1.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw=="],
"@vueuse/shared": ["@vueuse/shared@14.2.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-Z0bmluZTlAXgUcJ4uAFaML16JcD8V0QG00Db3quR642I99JXIDRa2MI2LGxiLVhcBjVnL1jOzIvT5TT2lqJlkA=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
@@ -613,7 +612,7 @@
"ast-walker-scope": ["ast-walker-scope@0.8.3", "", { "dependencies": { "@babel/parser": "^7.28.4", "ast-kit": "^2.1.3" } }, "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="],
"birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
@@ -625,10 +624,12 @@
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001764", "", {}, "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g=="],
"caniuse-lite": ["caniuse-lite@1.0.30001767", "", {}, "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ=="],
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
@@ -657,9 +658,9 @@
"duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="],
"electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
"electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="],
"entities": ["entities@7.0.0", "", {}, "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ=="],
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="],
@@ -673,7 +674,7 @@
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
"fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
"fast-xml-parser": ["fast-xml-parser@5.3.4", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
@@ -685,7 +686,7 @@
"gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="],
"hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
"hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="],
@@ -713,7 +714,7 @@
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
"miniflare": ["miniflare@4.20260114.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.14.0", "workerd": "1.20260114.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "^3.25.76" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-QwHT7S6XqGdQxIvql1uirH/7/i3zDEt0B/YBXTYzMfJtVCR4+ue3KPkU+Bl0zMxvpgkvjh9+eCHhJbKEqya70A=="],
"miniflare": ["miniflare@4.20260131.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260131.0", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-CtObRzlAzOUpCFH+MgImykxmDNKthrgIYtC+oLC3UGpve6bGLomKUW4u4EorTvzlQFHe66/9m/+AYbBbpzG0mQ=="],
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
@@ -741,7 +742,7 @@
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"perfect-debounce": ["perfect-debounce@2.0.0", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="],
"perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
@@ -759,7 +760,7 @@
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="],
"rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
"scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="],
@@ -797,7 +798,7 @@
"unconfig-core": ["unconfig-core@7.4.2", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg=="],
"undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="],
"undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
@@ -833,9 +834,9 @@
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
"workerd": ["workerd@1.20260114.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260114.0", "@cloudflare/workerd-darwin-arm64": "1.20260114.0", "@cloudflare/workerd-linux-64": "1.20260114.0", "@cloudflare/workerd-linux-arm64": "1.20260114.0", "@cloudflare/workerd-windows-64": "1.20260114.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kTJ+jNdIllOzWuVA3NRQRvywP0T135zdCjAE2dAUY1BFbxM6fmMZV8BbskEoQ4hAODVQUfZQmyGctcwvVCKxFA=="],
"workerd": ["workerd@1.20260131.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260131.0", "@cloudflare/workerd-darwin-arm64": "1.20260131.0", "@cloudflare/workerd-linux-64": "1.20260131.0", "@cloudflare/workerd-linux-arm64": "1.20260131.0", "@cloudflare/workerd-windows-64": "1.20260131.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-4zZxOdWeActbRfydQQlj7vZ2ay01AjjNC4K3stjmWC3xZHeXeN3EAROwsWE83SZHhtw4rn18srrhtXoQvQMw3Q=="],
"wrangler": ["wrangler@4.59.2", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.10.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.0", "miniflare": "4.20260114.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260114.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260114.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-Z4xn6jFZTaugcOKz42xvRAYKgkVUERHVbuCJ5+f+gK+R6k12L02unakPGOA0L0ejhUl16dqDjKe4tmL9sedHcw=="],
"wrangler": ["wrangler@4.62.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.12.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.0", "miniflare": "4.20260131.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260131.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260131.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-DogP9jifqw85g33BqwF6m21YBW5J7+Ep9IJLgr6oqHU0RkA79JMN5baeWXdmnIWZl+VZh6bmtNtR+5/Djd32tg=="],
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
@@ -847,7 +848,7 @@
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
"zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
@@ -855,20 +856,22 @@
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-sdk/client-sso/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.982.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ=="],
"@aws-sdk/middleware-user-agent/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.982.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ=="],
"@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.982.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ=="],
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
"@quansync/fs/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
"@tanstack/zod-form-adapter/@tanstack/form-core": ["@tanstack/form-core@0.42.1", "", { "dependencies": { "@tanstack/store": "^0.7.0" } }, "sha512-jTU0jyHqFceujdtPNv3jPVej1dTqBwa8TYdIyWB5BCwRVUBZEp1PiYEBkC9r92xu5fMpBiKc+JKud3eeVjuMiA=="],
"@unocss/transformer-attributify-jsx/@babel/parser": ["@babel/parser@7.27.7", "", { "dependencies": { "@babel/types": "^7.27.7" }, "bin": "./bin/babel-parser.js" }, "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q=="],
"@unocss/transformer-attributify-jsx/@babel/traverse": ["@babel/traverse@7.27.7", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/template": "^7.27.2", "@babel/types": "^7.27.7", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw=="],
"@vitejs/plugin-vue-jsx/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5", "", {}, "sha512-8sExkWRK+zVybw3+2/kBkYBFeLnEUWz1fT7BLHplpzmtqkOfTbAQ9gkt4pzwGIIZmg4Qn5US5ACjUBenrhezwQ=="],
"@vue/babel-plugin-jsx/@vue/shared": ["@vue/shared@3.5.26", "", {}, "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A=="],
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.26", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/compiler-core": "3.5.26", "@vue/compiler-dom": "3.5.26", "@vue/compiler-ssr": "3.5.26", "@vue/shared": "3.5.26", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA=="],
"@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
@@ -877,8 +880,6 @@
"@vue/devtools-kit/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"miniflare/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
@@ -901,17 +902,7 @@
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@unocss/transformer-attributify-jsx/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="],
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-core": ["@vue/compiler-core@3.5.26", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.26", "entities": "^7.0.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w=="],
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-dom": ["@vue/compiler-dom@3.5.26", "", { "dependencies": { "@vue/compiler-core": "3.5.26", "@vue/shared": "3.5.26" } }, "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A=="],
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.26", "", { "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/shared": "3.5.26" } }, "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw=="],
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/shared": ["@vue/shared@3.5.26", "", {}, "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A=="],
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@unocss/transformer-attributify-jsx/@babel/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],

60
components.d.ts vendored
View File

@@ -16,12 +16,12 @@ declare module 'vue' {
AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default']
ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
Avatar: typeof import('./src/components/ui/Avatar.vue')['default']
Avatar: typeof import('./src/components/ui/form/Avatar.vue')['default']
Bell: typeof import('./src/components/icons/Bell.vue')['default']
Button: typeof import('./src/components/ui/Button.vue')['default']
Card: typeof import('./src/components/ui/Card.vue')['default']
Button: typeof import('./src/components/ui/form/Button.vue')['default']
Card: typeof import('./src/components/ui/form/Card.vue')['default']
Chart: typeof import('./src/components/icons/Chart.vue')['default']
Checkbox: typeof import('./src/components/ui/Checkbox.vue')['default']
Checkbox: typeof import('./src/components/ui/form/Checkbox.vue')['default']
CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
@@ -30,33 +30,33 @@ declare module 'vue' {
CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
DashboardNav: typeof import('./src/components/DashboardNav.vue')['default']
DataTable: typeof import('./src/components/table/DataTable.vue')['default']
Dialog: typeof import('./src/components/ui/Dialog.vue')['default']
Dialog: typeof import('./src/components/ui/form/Dialog.vue')['default']
EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default']
Field: typeof import('./src/components/form/Field.vue')['default']
Form: typeof import('./src/components/form/Form.vue')['default']
Field: typeof import('./src/components/ui/form/Field.vue')['default']
Form: typeof import('./src/components/ui/form/Form.vue')['default']
GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default']
HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
Home: typeof import('./src/components/icons/Home.vue')['default']
InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
Input: typeof import('./src/components/ui/Input.vue')['default']
InputPassword: typeof import('./src/components/ui/InputPassword.vue')['default']
Input: typeof import('./src/components/ui/form/Input.vue')['default']
Layout: typeof import('./src/components/icons/Layout.vue')['default']
LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
Message: typeof import('./src/components/form/Message.vue')['default']
NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default']
ProgressBar: typeof import('./src/components/ui/ProgressBar.vue')['default']
ProgressBar: typeof import('./src/components/ui/form/ProgressBar.vue')['default']
RootLayout: typeof import('./src/components/RootLayout.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
Skeleton: typeof import('./src/components/ui/Skeleton.vue')['default']
Skeleton: typeof import('./src/components/ui/form/Skeleton.vue')['default']
StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
Tag: typeof import('./src/components/ui/Tag.vue')['default']
Table: typeof import('./src/components/ui/form/Table.vue')['default']
Tag: typeof import('./src/components/ui/form/Tag.vue')['default']
TanStackForm: typeof import('./src/components/ui/form/TanStackForm.vue')['default']
TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
Toast: typeof import('./src/components/ui/Toast.vue')['default']
Textarea: typeof import('./src/components/ui/form/Textarea.vue')['default']
Toast: typeof import('./src/components/ui/form/Toast.vue')['default']
TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
Upload: typeof import('./src/components/icons/Upload.vue')['default']
Video: typeof import('./src/components/icons/Video.vue')['default']
@@ -72,12 +72,12 @@ declare global {
const AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default']
const ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
const ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
const Avatar: typeof import('./src/components/ui/Avatar.vue')['default']
const Avatar: typeof import('./src/components/ui/form/Avatar.vue')['default']
const Bell: typeof import('./src/components/icons/Bell.vue')['default']
const Button: typeof import('./src/components/ui/Button.vue')['default']
const Card: typeof import('./src/components/ui/Card.vue')['default']
const Button: typeof import('./src/components/ui/form/Button.vue')['default']
const Card: typeof import('./src/components/ui/form/Card.vue')['default']
const Chart: typeof import('./src/components/icons/Chart.vue')['default']
const Checkbox: typeof import('./src/components/ui/Checkbox.vue')['default']
const Checkbox: typeof import('./src/components/ui/form/Checkbox.vue')['default']
const CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
const CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
const CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
@@ -86,33 +86,33 @@ declare global {
const CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
const DashboardNav: typeof import('./src/components/DashboardNav.vue')['default']
const DataTable: typeof import('./src/components/table/DataTable.vue')['default']
const Dialog: typeof import('./src/components/ui/Dialog.vue')['default']
const Dialog: typeof import('./src/components/ui/form/Dialog.vue')['default']
const EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default']
const Field: typeof import('./src/components/form/Field.vue')['default']
const Form: typeof import('./src/components/form/Form.vue')['default']
const Field: typeof import('./src/components/ui/form/Field.vue')['default']
const Form: typeof import('./src/components/ui/form/Form.vue')['default']
const GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default']
const HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
const Home: typeof import('./src/components/icons/Home.vue')['default']
const InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
const Input: typeof import('./src/components/ui/Input.vue')['default']
const InputPassword: typeof import('./src/components/ui/InputPassword.vue')['default']
const Input: typeof import('./src/components/ui/form/Input.vue')['default']
const Layout: typeof import('./src/components/icons/Layout.vue')['default']
const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
const Message: typeof import('./src/components/form/Message.vue')['default']
const NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
const PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
const PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default']
const ProgressBar: typeof import('./src/components/ui/ProgressBar.vue')['default']
const ProgressBar: typeof import('./src/components/ui/form/ProgressBar.vue')['default']
const RootLayout: typeof import('./src/components/RootLayout.vue')['default']
const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView']
const SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
const Skeleton: typeof import('./src/components/ui/Skeleton.vue')['default']
const Skeleton: typeof import('./src/components/ui/form/Skeleton.vue')['default']
const StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
const Tag: typeof import('./src/components/ui/Tag.vue')['default']
const Table: typeof import('./src/components/ui/form/Table.vue')['default']
const Tag: typeof import('./src/components/ui/form/Tag.vue')['default']
const TanStackForm: typeof import('./src/components/ui/form/TanStackForm.vue')['default']
const TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
const Toast: typeof import('./src/components/ui/Toast.vue')['default']
const Textarea: typeof import('./src/components/ui/form/Textarea.vue')['default']
const Toast: typeof import('./src/components/ui/form/Toast.vue')['default']
const TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
const Upload: typeof import('./src/components/icons/Upload.vue')['default']
const Video: typeof import('./src/components/icons/Video.vue')['default']

View File

@@ -10,34 +10,35 @@
"tail": "wrangler tail"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.971.0",
"@aws-sdk/s3-presigned-post": "^3.971.0",
"@aws-sdk/s3-request-presigner": "^3.971.0",
"@hiogawa/tiny-rpc": "^0.2.3-pre.18",
"@aws-sdk/client-s3": "^3.983.0",
"@aws-sdk/s3-presigned-post": "^3.983.0",
"@aws-sdk/s3-request-presigner": "^3.983.0",
"@hiogawa/utils": "^1.7.0",
"@pinia/colada": "^0.21.2",
"@tanstack/vue-form": "^1.28.0",
"@tanstack/vue-table": "^8.21.3",
"@unhead/vue": "^2.1.2",
"@vueuse/core": "^14.1.0",
"@vueuse/core": "^14.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"hono": "^4.11.4",
"hono": "^4.11.7",
"is-mobile": "^5.0.0",
"pinia": "^3.0.4",
"tailwind-merge": "^3.4.0",
"vue": "^3.5.27",
"vue-router": "^5.0.2",
"zod": "^4.3.5"
"zod": "^3.25.76"
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.21.0",
"@types/node": "^25.0.9",
"@vitejs/plugin-vue": "^6.0.3",
"@vitejs/plugin-vue-jsx": "^5.1.3",
"@cloudflare/vite-plugin": "^1.23.0",
"@types/node": "^25.2.0",
"@vitejs/plugin-vue": "^6.0.4",
"@vitejs/plugin-vue-jsx": "^5.1.4",
"unocss": "^66.6.0",
"unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^31.0.0",
"vite": "^7.3.1",
"vite-ssr-components": "^0.5.2",
"wrangler": "^4.59.2"
"wrangler": "^4.62.0"
}
}

View File

@@ -1,12 +1,12 @@
<script lang="ts" setup>
import Bell from "@/components/icons/Bell.vue";
import Credit from "@/components/icons/Credit.vue";
import Home from "@/components/icons/Home.vue";
import Upload from "@/components/icons/Upload.vue";
import Video from "@/components/icons/Video.vue";
import Credit from "@/components/icons/Credit.vue";
import Upload from "@/components/icons/Upload.vue";
import NotificationDrawer from "./NotificationDrawer.vue";
import { cn } from "@/lib/utils";
import { createStaticVNode, ref } from "vue";
import NotificationDrawer from "./NotificationDrawer.vue";
const className = ":uno: w-12 h-12 p-2 rounded-2xl hover:bg-primary/15 flex press-animated items-center justify-center shrink-0";
const homeHoist = createStaticVNode(`<img class="h-8 w-8" src="/apple-touch-icon.png" alt="Logo" />`, 1);
@@ -40,7 +40,7 @@ const links = [
<template v-for="i in links" :key="i.label">
<component :name="i.label" :is="i.type === 'a' ? 'router-link' : 'div'"
v-bind="i.type === 'a' ? { to: i.href } : {}" :title="i.label" @click="i.action && i.action($event)"
v-bind="i.type === 'a' ? { to: i.href } : {}" v-tooltip="i.label" @click="i.action && i.action($event)"
:class="cn(
i.className,
($route.path === i.href || i.isActive?.value) && 'bg-primary/15'

View File

@@ -1,3 +1,9 @@
<script setup lang="ts">
import Toast from './ui/form/Toast.vue';
</script>
<template>
<Toast />
<router-view/>
</template>

View File

@@ -1,22 +0,0 @@
<script setup lang="ts">
import { useField } from '@tanstack/vue-form';
interface Props {
name: string
form?: any
class?: string
}
const props = defineProps<Props>()
const field = useField({
name: props.name,
form: props.form
})
</script>
<template>
<div :class="props.class">
<slot :field="field" />
</div>
</template>

View File

@@ -1,55 +0,0 @@
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
import { type ZodType } from 'zod'
interface Props {
initialValues?: Record<string, any>
onSubmit?: (values: any) => void | Promise<void>
resolver?: ZodType<any>
class?: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
submit: [values: any]
}>()
const form = useForm({
defaultValues: props.initialValues || {},
onSubmit: async ({ value }) => {
if (props.onSubmit) {
await props.onSubmit(value as Record<string, any>)
}
emit('submit', value)
},
validators: props.resolver
? {
onChange: ({ value }) => {
const result = props.resolver!.safeParse(value)
if (result.success) {
return undefined
}
return result.error.issues.map(issue => ({
path: issue.path.join('.'),
message: issue.message
}))
}
}
: undefined
})
const handleSubmit = (e: Event) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}
</script>
<template>
<form
:class="props.class"
@submit="handleSubmit"
>
<slot :form="form" />
</form>
</template>

View File

@@ -1,36 +0,0 @@
<script setup lang="ts">
interface Props {
severity?: 'error' | 'success' | 'info' | 'warn'
size?: 'sm' | 'md'
class?: string
}
const props = withDefaults(defineProps<Props>(), {
severity: 'error',
size: 'sm'
})
const severityClasses = {
error: 'text-red-600',
success: 'text-green-600',
info: 'text-blue-600',
warn: 'text-yellow-600'
}
const sizeClasses = {
sm: 'text-xs',
md: 'text-sm'
}
</script>
<template>
<span
:class="[
severityClasses[severity],
sizeClasses[size],
props.class
]"
>
<slot />
</span>
</template>

View File

@@ -1,36 +0,0 @@
import { createColumnHelper, type ColumnDef } from '@tanstack/vue-table'
export { createColumnHelper }
export type { ColumnDef }
// Helper function to create a simple column
export function createColumn<T>(
accessorKey: keyof T,
header: string,
options?: {
cell?: (value: any, row: T) => any
enableSorting?: boolean
size?: number
}
): ColumnDef<T, any> {
return {
accessorKey: accessorKey as string,
header,
enableSorting: options?.enableSorting ?? true,
size: options?.size,
cell: options?.cell
? ({ getValue, row }) => options.cell!(getValue(), row.original)
: ({ getValue }) => getValue()
}
}
// Helper for selection column
export function createSelectionColumn<T>(): ColumnDef<T, any> {
return {
id: 'select',
header: ({ table }) => null,
cell: () => null,
size: 50,
enableSorting: false
}
}

View File

@@ -1,116 +0,0 @@
<script setup lang="ts">
import {
FlexRender,
getCoreRowModel,
getSortedRowModel,
useVueTable,
type ColumnDef,
type SortingState
} from '@tanstack/vue-table'
import { ref } from 'vue'
interface Props<T> {
data: T[]
columns: ColumnDef<T, any>[]
sorting?: SortingState
enableSorting?: boolean
class?: string
}
const props = withDefaults(defineProps<Props<any>>(), {
sorting: () => [],
enableSorting: false
})
const emit = defineEmits<{
'update:sorting': [value: SortingState]
}>()
const sortingState = ref<SortingState>(props.sorting)
const table = useVueTable({
get data() {
return props.data
},
get columns() {
return props.columns
},
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: props.enableSorting ? getSortedRowModel() : undefined,
onSortingChange: (updater) => {
if (typeof updater === 'function') {
sortingState.value = updater(sortingState.value)
} else {
sortingState.value = updater
}
emit('update:sorting', sortingState.value)
},
state: {
get sorting() {
return sortingState.value
}
}
})
</script>
<template>
<div :class="['overflow-x-auto', props.class]">
<table class="w-full text-sm text-left">
<thead class="text-xs text-gray-500 uppercase bg-gray-50 border-b border-gray-200">
<tr
v-for="headerGroup in table.getHeaderGroups()"
:key="headerGroup.id"
>
<th
v-for="header in headerGroup.headers"
:key="header.id"
:colSpan="header.colSpan"
:class="[
'px-6 py-3 font-medium',
header.column.getCanSort() ? 'cursor-pointer select-none hover:bg-gray-100' : ''
]"
@click="header.column.getToggleSortingHandler()?.($event)"
>
<FlexRender
v-if="!header.isPlaceholder"
:render="header.column.columnDef.header"
:props="header.getContext()"
/>
<span
v-if="header.column.getIsSorted()"
class="ml-1"
>
{{ header.column.getIsSorted() === 'asc' ? '↑' : '↓' }}
</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
<tr
v-for="row in table.getRowModel().rows"
:key="row.id"
class="hover:bg-gray-50"
>
<td
v-for="cell in row.getVisibleCells()"
:key="cell.id"
class="px-6 py-4"
>
<FlexRender
:render="cell.column.columnDef.cell"
:props="cell.getContext()"
/>
</td>
</tr>
<tr v-if="table.getRowModel().rows.length === 0">
<td
:colSpan="table.getAllColumns().length"
class="px-6 py-8 text-center text-gray-500"
>
No data available
</td>
</tr>
</tbody>
</table>
</div>
</template>

View File

@@ -1,71 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
image?: string
label?: string
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
shape?: 'circle' | 'square'
class?: string
}
const props = withDefaults(defineProps<Props>(), {
size: 'md',
shape: 'circle'
})
const sizeClasses = {
xs: 'w-6 h-6 text-xs',
sm: 'w-8 h-8 text-sm',
md: 'w-10 h-10 text-base',
lg: 'w-12 h-12 text-lg',
xl: 'w-16 h-16 text-xl'
}
const initials = computed(() => {
if (!props.label) return ''
return props.label
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase()
.slice(0, 2)
})
const bgColor = computed(() => {
const colors = [
'bg-red-500', 'bg-orange-500', 'bg-amber-500', 'bg-yellow-500',
'bg-lime-500', 'bg-green-500', 'bg-emerald-500', 'bg-teal-500',
'bg-cyan-500', 'bg-sky-500', 'bg-blue-500', 'bg-indigo-500',
'bg-violet-500', 'bg-purple-500', 'bg-fuchsia-500', 'bg-pink-500',
'bg-rose-500'
]
if (!props.label) return 'bg-gray-400'
let hash = 0
for (let i = 0; i < props.label.length; i++) {
hash = props.label.charCodeAt(i) + ((hash << 5) - hash)
}
return colors[Math.abs(hash) % colors.length]
})
</script>
<template>
<div
:class="[
'inline-flex items-center justify-center overflow-hidden font-medium text-white',
sizeClasses[size],
shape === 'circle' ? 'rounded-full' : 'rounded-lg',
!image ? bgColor : '',
props.class
]"
>
<img
v-if="image"
:src="image"
:alt="label || 'Avatar'"
class="w-full h-full object-cover"
/>
<span v-else-if="initials">{{ initials }}</span>
<span v-else class="i-heroicons-user w-1/2 h-1/2" />
</div>
</template>

View File

@@ -1,59 +0,0 @@
<script setup lang="ts">
interface Props {
type?: 'button' | 'submit' | 'reset'
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
fluid?: boolean
class?: string
}
const props = withDefaults(defineProps<Props>(), {
type: 'button',
variant: 'primary',
size: 'md',
disabled: false,
loading: false,
fluid: false
})
const emit = defineEmits<{
click: [event: MouseEvent]
}>()
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500',
outline: 'border-2 border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 focus:ring-gray-500',
ghost: 'bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500'
}
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base'
}
</script>
<template>
<button
:type="type"
:disabled="disabled || loading"
:class="[
'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed',
variantClasses[variant],
sizeClasses[size],
fluid ? 'w-full' : '',
props.class
]"
@click="emit('click', $event)"
>
<span
v-if="loading"
class="i-heroicons-arrow-path mr-2 animate-spin w-4 h-4"
/>
<slot />
</button>
</template>

View File

@@ -1,28 +0,0 @@
<script setup lang="ts">
interface Props {
class?: string
}
const props = defineProps<Props>()
</script>
<template>
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden" :class="props.class">
<!-- Header slot -->
<div v-if="$slots.header" class="border-b border-gray-100">
<slot name="header" />
</div>
<!-- Content -->
<div>
<slot name="content">
<slot />
</slot>
</div>
<!-- Footer slot -->
<div v-if="$slots.footer" class="border-t border-gray-100">
<slot name="footer" />
</div>
</div>
</template>

View File

@@ -1,87 +0,0 @@
<script setup lang="ts">
interface Props {
modelValue: any[] | boolean | undefined
value?: any
name?: string
disabled?: boolean
size?: 'sm' | 'md'
binary?: boolean
inputId?: string
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
size: 'md',
binary: false
})
const emit = defineEmits<{
'update:modelValue': [value: any[] | boolean]
click: [event: MouseEvent]
}>()
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-5 h-5'
}
const isChecked = (): boolean => {
if (props.binary) {
return !!(props.modelValue as boolean)
}
return Array.isArray(props.modelValue) && props.value !== undefined
? props.modelValue.includes(props.value)
: false
}
const toggle = (event?: MouseEvent) => {
if (props.binary) {
emit('update:modelValue', !props.modelValue)
} else {
const currentValue = Array.isArray(props.modelValue) ? props.modelValue : []
if (props.value !== undefined) {
if (currentValue.includes(props.value)) {
emit('update:modelValue', currentValue.filter(v => v !== props.value))
} else {
emit('update:modelValue', [...currentValue, props.value])
}
}
}
if (event) {
emit('click', event)
}
}
</script>
<template>
<div
class="inline-flex items-center"
:class="disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'"
@click="!disabled && toggle($event)"
>
<div
:class="[
sizeClasses[size],
'rounded border-2 flex items-center justify-center transition-colors',
isChecked()
? 'bg-blue-600 border-blue-600'
: 'bg-white border-gray-300 hover:border-gray-400'
]"
>
<span
v-if="isChecked()"
class="i-heroicons-check text-white"
:class="size === 'sm' ? 'w-3 h-3' : 'w-4 h-4'"
/>
</div>
<input
type="checkbox"
:name="name"
:id="inputId"
:checked="isChecked()"
:disabled="disabled"
class="sr-only"
@change="toggle()"
/>
</div>
</template>

View File

@@ -1,124 +0,0 @@
<script setup lang="ts">
import { onMounted, onUnmounted, watch } from 'vue'
interface Props {
visible: boolean
header?: string
width?: string
closable?: boolean
draggable?: boolean
modal?: boolean
class?: string
}
const props = withDefaults(defineProps<Props>(), {
header: '',
width: '28rem',
closable: true,
draggable: false,
modal: true
})
const emit = defineEmits<{
'update:visible': [value: boolean]
}>()
const handleClose = () => {
emit('update:visible', false)
}
const handleBackdropClick = () => {
if (props.closable) {
handleClose()
}
}
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && props.visible && props.closable) {
handleClose()
}
}
onMounted(() => {
document.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
})
watch(() => props.visible, (visible) => {
if (visible) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
})
</script>
<template>
<Teleport to="body">
<Transition
enter-active-class="transition ease-out duration-200"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition ease-in duration-150"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-if="visible"
class="fixed inset-0 z-50"
:class="[modal ? 'bg-black/50' : '']"
@click="handleBackdropClick"
>
<div class="flex min-h-full items-center justify-center p-4">
<Transition
enter-active-class="transition ease-out duration-200"
enter-from-class="opacity-0 scale-95"
enter-to-class="opacity-100 scale-100"
leave-active-class="transition ease-in duration-150"
leave-from-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-95"
>
<div
v-if="visible"
class="relative bg-white rounded-xl shadow-xl"
:style="{ width, maxWidth: 'calc(100vw - 2rem)' }"
:class="props.class"
@click.stop
>
<!-- Header -->
<div
v-if="header || $slots.header || closable"
class="flex items-center justify-between px-6 py-4 border-b border-gray-200"
>
<slot name="header">
<h3 class="text-lg font-semibold text-gray-900">{{ header }}</h3>
</slot>
<button
v-if="closable"
type="button"
class="text-gray-400 hover:text-gray-600 transition-colors"
@click="handleClose"
>
<span class="i-heroicons-x-mark w-5 h-5" />
</button>
</div>
<!-- Content -->
<div class="px-6 py-4">
<slot />
</div>
<!-- Footer -->
<div v-if="$slots.footer" class="px-6 py-4 border-t border-gray-200">
<slot name="footer" />
</div>
</div>
</Transition>
</div>
</div>
</Transition>
</Teleport>
</template>

View File

@@ -1,75 +0,0 @@
<script setup lang="ts">
import { twMerge } from 'tailwind-merge'
import { computed } from 'vue'
interface Props {
modelValue?: string | number
name?: string
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url'
placeholder?: string
disabled?: boolean
fluid?: boolean
size?: 'sm' | 'md' | 'lg'
invalid?: boolean
class?: string | Record<string, boolean>
}
const props = withDefaults(defineProps<Props>(), {
type: 'text',
disabled: false,
fluid: false,
size: 'md',
invalid: false
})
const emit = defineEmits<{
'update:modelValue': [value: string | number]
blur: [event: FocusEvent]
focus: [event: FocusEvent]
input: [event: Event]
}>()
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-4 py-3 text-base'
}
const baseClasses = computed(() => [
'block w-full rounded-lg border border-gray-300 bg-white text-gray-900 placeholder-gray-400',
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
'disabled:bg-gray-100 disabled:cursor-not-allowed',
props.invalid ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : '',
sizeClasses[props.size]
])
const inputClasses = computed(() => {
if (typeof props.class === 'string') {
return twMerge(baseClasses.value.join(' '), props.class)
}
return twMerge(baseClasses.value.join(' '))
})
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement
const value = props.type === 'number'
? (target.valueAsNumber || 0)
: target.value
emit('update:modelValue', value)
emit('input', event)
}
</script>
<template>
<input
:name="name"
:type="type"
:value="modelValue"
:placeholder="placeholder"
:disabled="disabled"
:class="inputClasses"
@input="handleInput"
@blur="emit('blur', $event)"
@focus="emit('focus', $event)"
/>
</template>

View File

@@ -1,105 +0,0 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
interface Props {
modelValue?: string
name?: string
placeholder?: string
disabled?: boolean
fluid?: boolean
size?: 'sm' | 'md' | 'lg'
invalid?: boolean
feedback?: boolean
class?: string
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
fluid: false,
size: 'md',
invalid: false,
feedback: false
})
const emit = defineEmits<{
'update:modelValue': [value: string]
blur: [event: FocusEvent]
focus: [event: FocusEvent]
}>()
const showPassword = ref(false)
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-4 py-3 text-base'
}
const inputClasses = computed(() => [
'block w-full rounded-lg border border-gray-300 bg-white text-gray-900 placeholder-gray-400',
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
'disabled:bg-gray-100 disabled:cursor-not-allowed pr-10',
props.invalid ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : '',
sizeClasses[props.size]
])
const passwordStrength = computed(() => {
if (!props.modelValue) return 0
let strength = 0
if (props.modelValue.length >= 8) strength++
if (/[A-Z]/.test(props.modelValue)) strength++
if (/[0-9]/.test(props.modelValue)) strength++
if (/[^A-Za-z0-9]/.test(props.modelValue)) strength++
return strength
})
const strengthText = computed(() => {
const texts = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong']
return texts[passwordStrength.value]
})
const strengthColor = computed(() => {
const colors = ['bg-red-500', 'bg-red-400', 'bg-yellow-400', 'bg-blue-400', 'bg-green-500']
return colors[passwordStrength.value]
})
</script>
<template>
<div :class="[fluid ? 'w-full' : '', props.class]">
<div class="relative">
<input
:name="name"
:type="showPassword ? 'text' : 'password'"
:value="modelValue"
:placeholder="placeholder"
:disabled="disabled"
:class="inputClasses"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
@blur="emit('blur', $event)"
@focus="emit('focus', $event)"
/>
<button
type="button"
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
@click="showPassword = !showPassword"
>
<span
:class="showPassword ? 'i-heroicons-eye-slash' : 'i-heroicons-eye'"
class="w-5 h-5"
/>
</button>
</div>
<div v-if="feedback && modelValue" class="mt-2">
<div class="flex gap-1 h-1 mb-1">
<div
v-for="i in 4"
:key="i"
class="flex-1 rounded-full transition-colors"
:class="i <= passwordStrength ? strengthColor : 'bg-gray-200'"
/>
</div>
<p class="text-xs text-gray-500">{{ strengthText }}</p>
</div>
</div>
</template>

View File

@@ -1,53 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
value?: number
showValue?: boolean
unit?: string
mode?: 'determinate' | 'indeterminate'
color?: 'primary' | 'success' | 'warning' | 'danger'
class?: string
}
const props = withDefaults(defineProps<Props>(), {
value: 0,
showValue: true,
unit: '%',
mode: 'determinate',
color: 'primary'
})
const normalizedValue = computed(() => {
return Math.max(0, Math.min(100, props.value))
})
const colorClasses = {
primary: 'bg-blue-600',
success: 'bg-green-500',
warning: 'bg-yellow-500',
danger: 'bg-red-500'
}
</script>
<template>
<div :class="['w-full', props.class]">
<div class="flex items-center gap-2">
<div class="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
<div
v-if="mode === 'determinate'"
:class="['h-full rounded-full transition-all duration-300 ease-out', colorClasses[color]]"
:style="{ width: `${normalizedValue}%` }"
/>
<div
v-else
:class="['h-full rounded-full animate-pulse', colorClasses[color]]"
style="width: 50%"
/>
</div>
<span v-if="showValue && mode === 'determinate'" class="text-xs font-medium text-gray-600 min-w-[3rem] text-right">
{{ normalizedValue }}{{ unit }}
</span>
</div>
</div>
</template>

View File

@@ -1,27 +0,0 @@
<script setup lang="ts">
interface Props {
width?: string
height?: string
borderRadius?: string
circle?: boolean
class?: string
}
const props = withDefaults(defineProps<Props>(), {
width: '100%',
height: '1rem',
borderRadius: '0.375rem',
circle: false
})
</script>
<template>
<div
:class="['animate-pulse bg-gray-200', props.class]"
:style="{
width,
height,
borderRadius: circle ? '50%' : borderRadius
}"
/>
</template>

View File

@@ -1,34 +0,0 @@
<script setup lang="ts">
interface Props {
value?: string
severity?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger'
icon?: string
class?: string
}
const props = withDefaults(defineProps<Props>(), {
severity: 'primary'
})
const severityClasses = {
primary: 'bg-blue-100 text-blue-800 border-blue-200',
secondary: 'bg-gray-100 text-gray-800 border-gray-200',
success: 'bg-green-100 text-green-800 border-green-200',
info: 'bg-cyan-100 text-cyan-800 border-cyan-200',
warning: 'bg-yellow-100 text-yellow-800 border-yellow-200',
danger: 'bg-red-100 text-red-800 border-red-200'
}
</script>
<template>
<span
:class="[
'inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium border',
severityClasses[severity],
props.class
]"
>
<span v-if="icon" :class="[icon, 'w-3 h-3']" />
<slot>{{ value }}</slot>
</span>
</template>

View File

@@ -1,73 +0,0 @@
<script setup lang="ts">
import { useToast, type ToastSeverity } from '@/composables/useToast'
const toast = useToast()
const severityIcons: Record<ToastSeverity, string> = {
success: 'i-heroicons-check-circle',
error: 'i-heroicons-x-circle',
info: 'i-heroicons-information-circle',
warn: 'i-heroicons-exclamation-triangle'
}
const severityClasses: Record<ToastSeverity, string> = {
success: 'bg-green-50 border-green-200 text-green-800',
error: 'bg-red-50 border-red-200 text-red-800',
info: 'bg-blue-50 border-blue-200 text-blue-800',
warn: 'bg-yellow-50 border-yellow-200 text-yellow-800'
}
const severityIconColors: Record<ToastSeverity, string> = {
success: 'text-green-500',
error: 'text-red-500',
info: 'text-blue-500',
warn: 'text-yellow-500'
}
const handleClose = (id: string) => {
toast.remove(id)
}
</script>
<template>
<Teleport to="body">
<div class="fixed top-4 right-4 z-50 flex flex-col gap-2 max-w-sm">
<TransitionGroup
enter-active-class="transition ease-out duration-300"
enter-from-class="translate-x-full opacity-0"
enter-to-class="translate-x-0 opacity-100"
leave-active-class="transition ease-in duration-200"
leave-from-class="translate-x-0 opacity-100"
leave-to-class="translate-x-full opacity-0"
>
<div
v-for="message in toast.messages"
:key="message.id"
:class="[
'flex items-start gap-3 p-4 rounded-lg border shadow-lg min-w-[300px]',
severityClasses[message.severity]
]"
>
<span
:class="[
severityIcons[message.severity],
severityIconColors[message.severity],
'w-5 h-5 flex-shrink-0 mt-0.5'
]"
/>
<div class="flex-1 min-w-0">
<p class="font-medium text-sm">{{ message.summary }}</p>
<p v-if="message.detail" class="text-sm opacity-90 mt-0.5">{{ message.detail }}</p>
</div>
<button
type="button"
class="flex-shrink-0 opacity-60 hover:opacity-100 transition-opacity"
@click="handleClose(message.id)"
>
<span class="i-heroicons-x-mark w-4 h-4" />
</button>
</div>
</TransitionGroup>
</div>
</Teleport>
</template>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
interface AvatarProps {
label?: string;
shape?: 'circle' | 'square';
size?: 'small' | 'medium' | 'large';
}
const props = withDefaults(defineProps<AvatarProps>(), {
shape: 'circle',
size: 'medium',
});
const sizeClasses = {
small: 'w-8 h-8 text-xs',
medium: 'w-10 h-10 text-sm',
large: 'w-12 h-12 text-base',
};
const shapeClasses = {
circle: 'rounded-full',
square: 'rounded-lg',
};
</script>
<template>
<div
:class="[
'inline-flex items-center justify-center font-medium bg-gray-200 text-gray-600',
sizeClasses[size],
shapeClasses[shape],
]"
>
<slot>
{{ label?.charAt(0).toUpperCase() || '?' }}
</slot>
</div>
</template>

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
interface ButtonProps {
type?: 'button' | 'submit' | 'reset';
variant?: 'primary' | 'secondary' | 'outlined' | 'text';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
loading?: boolean;
label?: string;
}
const props = withDefaults(defineProps<ButtonProps>(), {
type: 'button',
variant: 'primary',
size: 'medium',
disabled: false,
loading: false,
});
const variantClasses = {
primary: 'bg-primary text-white hover:opacity-90',
secondary: 'bg-gray-600 text-white hover:bg-gray-700',
outlined: 'border border-gray-300 bg-transparent hover:bg-gray-50',
text: 'bg-transparent hover:bg-gray-100',
};
const sizeClasses = {
small: 'px-3 py-1.5 text-xs',
medium: 'px-4 py-2 text-sm',
large: 'px-6 py-3 text-base',
};
</script>
<template>
<button
:type="type"
:disabled="disabled || loading"
:class="[
'inline-flex items-center justify-center font-medium rounded-lg transition-colors',
'focus:outline-none focus:ring-2 focus:ring-primary/20',
'disabled:opacity-50 disabled:cursor-not-allowed',
variantClasses[variant],
sizeClasses[size],
loading ? 'cursor-wait' : '',
]"
>
<svg
v-if="loading"
class="animate-spin -ml-1 mr-2 h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<slot>{{ label }}</slot>
</button>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
interface CardProps {
cardClass?: string;
}
defineProps<CardProps>();
</script>
<template>
<div :class="['bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden', cardClass]">
<slot name="header" />
<div>
<slot />
</div>
</div>
</template>

View File

@@ -0,0 +1,66 @@
<script setup lang="ts">
import { computed, inject } from 'vue';
interface CheckboxProps {
name: string;
value?: any;
binary?: boolean;
disabled?: boolean;
}
const props = withDefaults(defineProps<CheckboxProps>(), {
binary: false,
disabled: false,
});
const formContext = inject<{
values: Record<string, any>;
errors: Record<string, string>;
touched: Record<string, boolean>;
handleBlur: (name: string) => void;
handleChange: (name: string, value: any) => void;
} | null>('form-context', null);
const error = computed(() => formContext?.errors[props.name]);
const isInvalid = computed(() => formContext?.touched[props.name] && error.value);
const modelValue = computed({
get: () => {
const val = formContext?.values[props.name];
if (props.binary) return !!val;
return val?.includes(props.value);
},
set: (val) => {
if (props.binary) {
formContext?.handleChange(props.name, val);
} else {
const current = formContext?.values[props.name] || [];
if (val) {
formContext?.handleChange(props.name, [...current, props.value]);
} else {
formContext?.handleChange(props.name, current.filter((v: any) => v !== props.value));
}
}
},
});
</script>
<template>
<div class="flex items-center gap-2">
<input
:id="name + '-' + (value ?? 'binary')"
type="checkbox"
v-model="modelValue"
:disabled="disabled"
:class="[
'w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary/20',
'disabled:opacity-50 disabled:cursor-not-allowed',
isInvalid ? 'border-red-500' : '',
]"
@blur="formContext?.handleBlur(name)"
/>
<label v-if="$slots.default" :for="name + '-' + (value ?? 'binary')" class="text-sm text-gray-700">
<slot />
</label>
</div>
</template>

View File

@@ -0,0 +1,115 @@
<script setup lang="ts">
import { onMounted, onUnmounted, watch } from 'vue';
interface DialogProps {
visible: boolean;
header?: string;
style?: Record<string, string>;
closable?: boolean;
modal?: boolean;
}
const props = withDefaults(defineProps<DialogProps>(), {
closable: true,
modal: true,
});
const emit = defineEmits<{
'update:visible': [value: boolean];
}>();
const handleClose = () => {
if (props.closable) {
emit('update:visible', false);
}
};
const handleBackdropClick = () => {
if (props.modal) {
handleClose();
}
};
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && props.visible) {
handleClose();
}
};
onMounted(() => {
document.addEventListener('keydown', handleEscape);
});
onUnmounted(() => {
document.removeEventListener('keydown', handleEscape);
});
watch(() => props.visible, (val) => {
if (val) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
});
</script>
<template>
<Teleport to="body">
<Transition name="dialog">
<div v-if="visible" class="fixed inset-0 z-50 flex items-center justify-center">
<!-- Backdrop -->
<div
class="fixed inset-0 bg-black/50"
@click="handleBackdropClick"
/>
<!-- Dialog -->
<div
class="relative bg-white rounded-xl shadow-xl w-full max-h-[90vh] overflow-auto"
:style="style || { width: '28rem' }"
>
<!-- Header -->
<div v-if="header" class="flex items-center justify-between px-6 py-4 border-b">
<h3 class="text-lg font-semibold text-gray-900">{{ header }}</h3>
<button
v-if="closable"
@click="handleClose"
class="p-1 rounded-lg hover:bg-gray-100 transition-colors"
>
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Content -->
<div class="p-6 pt-4">
<slot />
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.dialog-enter-active,
.dialog-leave-active {
transition: opacity 0.2s ease;
}
.dialog-enter-from,
.dialog-leave-to {
opacity: 0;
}
.dialog-enter-active .relative,
.dialog-leave-active .relative {
transition: transform 0.2s ease;
}
.dialog-enter-from .relative,
.dialog-leave-to .relative {
transform: scale(0.95);
}
</style>

View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import { computed, inject } from 'vue';
interface FieldProps {
name: string;
label?: string;
}
const props = defineProps<FieldProps>();
const formContext = inject<{
values: Record<string, any>;
errors: Record<string, string>;
touched: Record<string, boolean>;
validators: Record<string, ((value: any) => string | undefined)[]>;
handleBlur: (name: string) => void;
handleChange: (name: string, value: any) => void;
} | null>('form-context', null);
const error = computed(() => props.name ? formContext?.errors[props.name] : undefined);
const isInvalid = computed(() => props.name ? formContext?.touched[props.name] && !!error.value : false);
const fieldValue = computed(() => props.name ? formContext?.values[props.name] ?? '' : '');
const onChange = (value: any) => {
if (props.name && formContext) {
formContext.handleChange(props.name, value);
}
};
const onBlur = () => {
if (props.name && formContext) {
formContext.handleBlur(props.name);
}
};
// Provide values to slot
const slotProps = {
value: fieldValue,
error: error,
errorMessage: error,
isInvalid,
name: props.name,
onChange,
onBlur,
};
</script>
<template>
<div class="field flex flex-col gap-1">
<label v-if="label" :for="name" class="text-sm font-medium text-gray-700">
{{ label }}
</label>
<slot v-bind="slotProps" />
<div v-if="isInvalid && error" class="text-xs text-red-600 mt-1">
{{ error }}
</div>
</div>
</template>

View File

@@ -0,0 +1,91 @@
<script setup lang="ts">
import { provide, reactive } from 'vue';
const props = defineProps<{
initialValues?: Record<string, any>;
validators?: Record<string, ((value: any) => string | undefined)[]>;
formClass?: string;
}>();
const emit = defineEmits<{
submit: [values: Record<string, any>];
}>();
const errors = reactive<Record<string, string>>({});
const touched = reactive<Record<string, boolean>>({});
const values = reactive<Record<string, any>>({...props.initialValues});
// Initialize values
if (props.initialValues) {
Object.assign(values, props.initialValues);
}
const validateField = (name: string) => {
const value = values[name];
const fieldValidators = props.validators?.[name] || [];
for (const validator of fieldValidators) {
const error = validator(value);
if (error) {
errors[name] = error;
return false;
}
}
delete errors[name];
return true;
};
const validateAll = () => {
const fieldNames = Object.keys(props.validators || {});
let isValid = true;
for (const name of fieldNames) {
if (!validateField(name)) {
isValid = false;
}
}
return isValid;
};
const handleSubmit = () => {
// Mark all fields as touched
const fieldNames = Object.keys(props.validators || {});
for (const name of fieldNames) {
touched[name] = true;
}
if (validateAll()) {
emit('submit', {...values});
}
};
const handleBlur = (name: string) => {
touched[name] = true;
validateField(name);
};
const handleChange = (name: string, value: any) => {
values[name] = value;
if (touched[name]) {
validateField(name);
}
};
// Provide form context to child components
provide('form-context', {
values,
errors,
touched,
validators: props.validators || {},
handleBlur,
handleChange,
validateField,
});
</script>
<template>
<form @submit.prevent="handleSubmit" :class="[formClass, 'flex flex-col gap-4 w-full']">
<slot />
</form>
</template>

View File

@@ -0,0 +1,55 @@
<script setup lang="ts">
import { computed } from 'vue';
interface InputProps {
name?: string;
type?: string;
placeholder?: string;
disabled?: boolean;
fluid?: boolean;
modelValue?: string | any;
}
const props = withDefaults(defineProps<InputProps>(), {
type: 'text',
disabled: false,
fluid: true,
});
const emit = defineEmits<{
'update:modelValue': [value: string];
}>();
// Handle the v-model binding - support both string and computed ref
const inputValue = computed(() => {
const val = props.modelValue;
// Check if it's a ref/computed
if (val && typeof val === 'object' && 'value' in val) {
return val.value;
}
return val ?? '';
});
const onInput = (event: Event) => {
const target = event.target as HTMLInputElement;
emit('update:modelValue', target.value);
};
</script>
<template>
<input
:id="name"
:value="inputValue"
@input="onInput"
:type="type"
:placeholder="placeholder"
:disabled="disabled"
:class="[
'px-3 py-2 text-sm border rounded-lg outline-none transition-colors',
'focus:ring-2 focus:ring-primary/20 focus:border-primary',
'disabled:bg-gray-100 disabled:text-gray-500 disabled:cursor-not-allowed',
fluid ? 'w-full' : '',
'border-gray-300',
]"
/>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
interface ProgressBarProps {
value?: number;
showValue?: boolean;
}
const props = withDefaults(defineProps<ProgressBarProps>(), {
value: 0,
showValue: false,
});
</script>
<template>
<div class="w-full">
<div
v-if="showValue"
class="flex justify-between mb-1"
>
<span class="text-sm font-medium text-gray-700">
<slot name="value">{{ Math.round(value) }}%</slot>
</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div
class="bg-primary h-2 rounded-full transition-all duration-300"
:style="{ width: `${Math.min(100, Math.max(0, value))}%` }"
/>
</div>
</div>
</template>

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
interface SkeletonProps {
width?: string;
height?: string;
borderRadius?: string;
}
const props = withDefaults(defineProps<SkeletonProps>(), {
width: '100%',
height: '1rem',
borderRadius: '0.375rem',
});
</script>
<template>
<div
class="animate-pulse bg-gray-200"
:style="{
width,
height,
borderRadius,
}"
/>
</template>

View File

@@ -0,0 +1,35 @@
<script setup lang="ts" generic="T">
interface TableProps<T> {
value: T[];
dataKey: string;
selection?: T[];
tableStyle?: string;
}
const props = defineProps<TableProps<T>>();
const emit = defineEmits<{
'update:selection': [value: T[]];
}>();
</script>
<template>
<div class="overflow-x-auto">
<table :class="['w-full', tableStyle]">
<thead>
<tr class="border-b border-gray-200 bg-gray-50">
<slot name="header" />
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in value"
:key="String((item as any)[dataKey])"
class="border-b border-gray-100 hover:bg-gray-50 transition-colors"
>
<slot name="body" :data="item" :index="index" />
</tr>
</tbody>
</table>
</div>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
interface TagProps {
value?: string;
severity?: 'success' | 'error' | 'warn' | 'info' | 'secondary';
rounded?: boolean;
}
const props = withDefaults(defineProps<TagProps>(), {
rounded: true,
});
const severityClasses = {
success: 'bg-green-100 text-green-800',
error: 'bg-red-100 text-red-800',
warn: 'bg-yellow-100 text-yellow-800',
info: 'bg-blue-100 text-blue-800',
secondary: 'bg-gray-100 text-gray-800',
};
</script>
<template>
<span
:class="[
'inline-flex items-center px-2.5 py-0.5 text-xs font-medium',
rounded ? 'rounded-full' : 'rounded',
severity ? severityClasses[severity] : 'bg-gray-100 text-gray-800',
]"
>
<slot>{{ value }}</slot>
</span>
</template>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts" generic="T">
import { useForm } from '@tanstack/vue-form';
import { provide } from 'vue';
interface FormProps {
initialValues?: T;
onSubmit?: (values: T) => void | Promise<void>;
validators?: Record<string, (value: any) => string | undefined>;
}
const props = defineProps<FormProps>();
const form = useForm({
initialValues: props.initialValues,
onSubmit: async (values) => {
await props.onSubmit?.(values.value);
},
});
// Provide form context to child components
provide('tanstack-form', form);
provide('tanstack-form-validators', props.validators || {});
const handleSubmit = (e: Event) => {
e.preventDefault();
form.handleSubmit();
};
</script>
<template>
<form @submit="handleSubmit" class="flex flex-col gap-4 w-full">
<slot :form="form" />
</form>
</template>

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import { computed, inject } from 'vue';
interface TextareaProps {
name: string;
rows?: number;
placeholder?: string;
disabled?: boolean;
fluid?: boolean;
}
const props = withDefaults(defineProps<TextareaProps>(), {
rows: 3,
disabled: false,
fluid: true,
});
const formContext = inject<{
values: Record<string, any>;
errors: Record<string, string>;
touched: Record<string, boolean>;
handleBlur: (name: string) => void;
handleChange: (name: string, value: any) => void;
} | null>('form-context', null);
const error = computed(() => formContext?.errors[props.name]);
const isInvalid = computed(() => formContext?.touched[props.name] && error.value);
const modelValue = computed({
get: () => formContext?.values[props.name] ?? '',
set: (val) => formContext?.handleChange(props.name, val),
});
</script>
<template>
<textarea
:id="name"
v-model="modelValue"
:rows="rows"
:placeholder="placeholder"
:disabled="disabled"
:class="[
'px-3 py-2 text-sm border rounded-lg outline-none transition-colors resize-none',
'focus:ring-2 focus:ring-primary/20 focus:border-primary',
'disabled:bg-gray-100 disabled:text-gray-500 disabled:cursor-not-allowed',
fluid ? 'w-full' : '',
isInvalid ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : 'border-gray-300',
]"
@blur="formContext?.handleBlur(name)"
/>
</template>

View File

@@ -0,0 +1,104 @@
<script setup lang="ts">
import { provide, ref } from 'vue';
interface ToastItem {
id: string;
severity: 'success' | 'error' | 'warn' | 'info';
summary: string;
detail?: string;
life?: number;
}
const toasts = ref<ToastItem[]>([]);
const addToast = (toast: Omit<ToastItem, 'id'>) => {
const id = crypto.randomUUID();
const newToast = { ...toast, id };
toasts.value.push(newToast);
const life = toast.life || 5000;
setTimeout(() => {
removeToast(id);
}, life);
};
const removeToast = (id: string) => {
const index = toasts.value.findIndex(t => t.id === id);
if (index > -1) {
toasts.value.splice(index, 1);
}
};
const toast = {
add: addToast,
remove: removeToast,
};
provide('toast', toast);
const severityClasses = {
success: 'bg-green-50 text-green-800 border-green-200',
error: 'bg-red-50 text-red-800 border-red-200',
warn: 'bg-yellow-50 text-yellow-800 border-yellow-200',
info: 'bg-blue-50 text-blue-800 border-blue-200',
};
const severityIcons = {
success: '✓',
error: '✕',
warn: '⚠',
info: '',
};
</script>
<template>
<Teleport to="body">
<div class="fixed top-4 right-4 z-[100] flex flex-col gap-2 max-w-sm">
<TransitionGroup name="toast">
<div
v-for="toast in toasts"
:key="toast.id"
:class="[
'flex items-start gap-3 p-4 rounded-lg border shadow-lg',
severityClasses[toast.severity],
]"
>
<span class="text-lg">{{ severityIcons[toast.severity] }}</span>
<div class="flex-1 min-w-0">
<p class="font-medium">{{ toast.summary }}</p>
<p v-if="toast.detail" class="text-sm opacity-90 mt-1">{{ toast.detail }}</p>
</div>
<button
@click="removeToast(toast.id)"
class="p-1 hover:bg-black/5 rounded transition-colors"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</TransitionGroup>
</div>
</Teleport>
</template>
<style scoped>
.toast-enter-active,
.toast-leave-active {
transition: all 0.3s ease;
}
.toast-enter-from {
opacity: 0;
transform: translateX(100%);
}
.toast-leave-to {
opacity: 0;
transform: translateX(100%);
}
.toast-move {
transition: transform 0.3s ease;
}
</style>

View File

@@ -0,0 +1,15 @@
export { default as Avatar } from './Avatar.vue';
export { default as Button } from './Button.vue';
export { default as Card } from './Card.vue';
export { default as Checkbox } from './Checkbox.vue';
export { default as Dialog } from './Dialog.vue';
export { default as Field } from './Field.vue';
export { default as Form } from './Form.vue';
export { default as Input } from './Input.vue';
export { default as ProgressBar } from './ProgressBar.vue';
export { default as Skeleton } from './Skeleton.vue';
export { default as Table } from './Table.vue';
export { default as Tag } from './Tag.vue';
export { default as Textarea } from './Textarea.vue';
export { default as Toast } from './Toast.vue';

View File

@@ -1,174 +0,0 @@
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
interface DataLoaderOptions<T> {
key: string
fetcher: () => Promise<T>
revalidateOnFocus?: boolean
revalidateOnReconnect?: boolean
refreshInterval?: number
dedupingInterval?: number
fallbackData?: T
}
interface DataLoaderState<T> {
data: T | undefined
error: Error | null
isLoading: boolean
isValidating: boolean
}
// Global cache
const cache = new Map<string, { data: any; timestamp: number }>()
const dedupeTimers = new Map<string, number>()
const DEDUPING_INTERVAL = 2000
export function useDataLoader<T>(options: DataLoaderOptions<T>) {
const route = useRoute()
const {
key,
fetcher,
revalidateOnFocus = false,
revalidateOnReconnect = true,
refreshInterval,
fallbackData
} = options
const data = ref<T | undefined>(fallbackData)
const error = ref<Error | null>(null)
const isLoading = ref(false)
const isValidating = ref(false)
let refreshTimer: number | null = null
let isMounted = false
const mutate = async (newData?: T): Promise<T | undefined> => {
if (newData !== undefined) {
data.value = newData
cache.set(key, { data: newData, timestamp: Date.now() })
return newData
}
// Dedupe requests
if (dedupeTimers.has(key)) {
return data.value
}
const dedupeKey = key
dedupeTimers.set(dedupeKey, window.setTimeout(() => {
dedupeTimers.delete(dedupeKey)
}, DEDUPING_INTERVAL))
isValidating.value = true
if (!data.value) {
isLoading.value = true
}
try {
const result = await fetcher()
data.value = result
error.value = null
cache.set(key, { data: result, timestamp: Date.now() })
return result
} catch (err) {
error.value = err as Error
throw err
} finally {
isLoading.value = false
isValidating.value = false
}
}
// Initial load
const load = async () => {
const cached = cache.get(key)
if (cached && Date.now() - cached.timestamp < DEDUPING_INTERVAL) {
data.value = cached.data
return
}
await mutate()
}
// Revalidate on focus
const handleFocus = () => {
if (revalidateOnFocus && document.visibilityState === 'visible') {
mutate()
}
}
// Revalidate on reconnect
const handleOnline = () => {
if (revalidateOnReconnect) {
mutate()
}
}
// Setup refresh interval
const setupRefreshInterval = () => {
if (refreshInterval && refreshInterval > 0) {
refreshTimer = window.setInterval(() => {
mutate()
}, refreshInterval)
}
}
// Cleanup refresh interval
const cleanupRefreshInterval = () => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
}
onMounted(() => {
isMounted = true
load()
if (revalidateOnFocus) {
document.addEventListener('visibilitychange', handleFocus)
}
if (revalidateOnReconnect) {
window.addEventListener('online', handleOnline)
}
setupRefreshInterval()
})
onUnmounted(() => {
isMounted = false
cleanupRefreshInterval()
if (revalidateOnFocus) {
document.removeEventListener('visibilitychange', handleFocus)
}
if (revalidateOnReconnect) {
window.removeEventListener('online', handleOnline)
}
})
// Revalidate when key changes
watch(() => key, () => {
if (isMounted) {
load()
}
})
return {
data: computed(() => data.value),
error: computed(() => error.value),
isLoading: computed(() => isLoading.value),
isValidating: computed(() => isValidating.value),
mutate
}
}
// Helper for SSR compatibility
export const useSWRV = <T>(key: string, fetcher: () => Promise<T>) => {
return useDataLoader<T>({
key,
fetcher,
revalidateOnFocus: false
})
}

View File

@@ -1,90 +0,0 @@
import { reactive } from 'vue'
export type ToastSeverity = 'success' | 'error' | 'info' | 'warn'
export interface ToastMessage {
id: string
severity: ToastSeverity
summary: string
detail?: string
life?: number
}
interface ToastState {
messages: ToastMessage[]
}
const state = reactive<ToastState>({
messages: []
})
let toastIdCounter = 0
export const useToast = () => {
const add = (message: Omit<ToastMessage, 'id'>) => {
const id = `toast-${++toastIdCounter}`
const newMessage: ToastMessage = {
id,
life: 3000,
...message
}
state.messages.push(newMessage)
if (newMessage.life && newMessage.life > 0) {
setTimeout(() => {
remove(id)
}, newMessage.life)
}
return id
}
const remove = (id: string) => {
const index = state.messages.findIndex(m => m.id === id)
if (index > -1) {
state.messages.splice(index, 1)
}
}
const clear = () => {
state.messages.length = 0
}
const success = (detail: string, summary: string = 'Success') => {
return add({ severity: 'success', summary, detail })
}
const error = (detail: string, summary: string = 'Error') => {
return add({ severity: 'error', summary, detail })
}
const info = (detail: string, summary: string = 'Info') => {
return add({ severity: 'info', summary, detail })
}
const warn = (detail: string, summary: string = 'Warning') => {
return add({ severity: 'warn', summary, detail })
}
return {
messages: state.messages,
add,
remove,
clear,
success,
error,
info,
warn
}
}
// Global toast instance for use outside of components
let globalToastInstance: ReturnType<typeof useToast> | null = null
export const getGlobalToast = () => {
if (!globalToastInstance) {
globalToastInstance = useToast()
}
return globalToastInstance
}

View File

@@ -9,9 +9,8 @@ import { buildBootstrapScript } from './lib/manifest';
import { createTextTransformStreamClass } from './lib/replateStreamText';
import { createApp } from './main';
import { useAuthStore } from './stores/auth';
const app = new Hono()
// app.use(renderer)
app.use('*', contextStorage());
app.use(cors(), async (c, next) => {
c.set("fetch", app.request.bind(app));
@@ -32,7 +31,8 @@ app.use(cors(), async (c, next) => {
url.protocol = 'https:'
url.pathname = path.replace(/^\/r/, '') || '/'
url.port = ''
// console.log("url", url.toString())
// console.log("c.req.raw", c.req.raw)
const headers = new Headers(c.req.header());
headers.delete("host");
headers.delete("connection");
@@ -46,11 +46,9 @@ app.use(cors(), async (c, next) => {
credentials: 'include'
});
});
app.get("/.well-known/*", (c) => {
return c.json({ ok: true });
});
app.get("*", async (c) => {
const nonce = crypto.randomUUID();
const url = new URL(c.req.url);
@@ -58,28 +56,25 @@ app.get("*", async (c) => {
app.provide("honoContext", c);
const auth = useAuthStore();
auth.$reset();
await auth.init();
await router.push(url.pathname);
await router.isReady();
return streamText(c, async (stream) => {
c.header("Content-Type", "text/html; charset=utf-8");
c.header("Content-Encoding", "Identity");
const ctx: Record<string, any> = {};
const appStream = renderToWebStream(app, ctx);
// console.log("ctx: ", );
await stream.write("<!DOCTYPE html><html lang='en'><head>");
await stream.write("<base href='" + url.origin + "'/>");
await renderSSRHead(head).then((headString) => stream.write(headString.headTags.replace(/\n/g, "")));
// await stream.write(`<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"rel="stylesheet"></link>`);
await stream.write(`<link rel="preconnect" href="https://fonts.googleapis.com">`);
await stream.write(`<link href="https://fonts.googleapis.com/css2?family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700&display=swap" rel="stylesheet">`);
await stream.write('<link rel="icon" href="/favicon.ico" />');
await stream.write(buildBootstrapScript());
await stream.write(`</head><body class='${bodyClass}'>`);
await stream.pipe(createTextTransformStreamClass(appStream, (text) => text.replace('<div id="anchor-header" class="p-4"></div>', `<div id="anchor-header" class="p-4">${ctx.teleports["#anchor-header"] || ""}</div>`).replace('<div id="anchor-top"></div>', `<div id="anchor-top">${ctx.teleports["#anchor-top"] || ""}</div>`)));
delete ctx.teleports
delete ctx.__teleportBuffers
delete ctx.modules;
@@ -88,7 +83,6 @@ app.get("*", async (c) => {
await stream.write("</body></html>");
});
})
const ESCAPE_LOOKUP: { [match: string]: string } = {
"&": "\\u0026",
">": "\\u003e",
@@ -102,5 +96,4 @@ const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
function htmlEscape(str: string): string {
return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
}
export default app

File diff suppressed because one or more lines are too long

View File

@@ -5,24 +5,18 @@ import { createSSRApp } from 'vue';
import { RouterView } from 'vue-router';
import { withErrorBoundary } from './lib/hoc/withErrorBoundary';
import createAppRouter from './routes';
const bodyClass = ":uno: font-sans text-gray-800 antialiased flex flex-col min-h-screen"
export function createApp() {
const pinia = createPinia();
const app = createSSRApp(withErrorBoundary(RouterView));
const head = import.meta.env.SSR ? SSRHead() : CSRHead();
app.use(head);
// Directive để skip hydration cho các phần tử không cần thiết
app.directive('nh', {
created(el) {
el.__v_skip = true;
}
});
// Restore state từ SSR
if (!import.meta.env.SSR) {
Object.entries(JSON.parse(document.getElementById("__APP_DATA__")?.innerText || "{}")).forEach(([key, value]) => {
(window as any)[key] = value;
@@ -31,9 +25,7 @@ export function createApp() {
pinia.state.value = (window as any).$p;
}
}
app.use(pinia);
const router = createAppRouter();
app.use(router);

397
src/routes/add/Add.vue Normal file
View File

@@ -0,0 +1,397 @@
<script setup lang="ts">
import { client, type ModelVideo } from '@/api/client';
import PageHeader from '@/components/dashboard/PageHeader.vue';
import StatsCard from '@/components/dashboard/StatsCard.vue';
import { Skeleton } from '@/components/ui/form';
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const loading = ref(true);
const recentVideos = ref<ModelVideo[]>([]);
// Mock stats data (in real app, fetch from API)
const stats = ref({
totalVideos: 0,
totalViews: 0,
storageUsed: 0,
storageLimit: 10737418240, // 10GB in bytes
uploadsThisMonth: 0
});
const quickActions = [
{
title: 'Upload Video',
description: 'Upload a new video to your library',
icon: 'i-heroicons-cloud-arrow-up',
color: 'bg-gradient-to-br from-primary/20 to-primary/5',
iconColor: 'text-primary',
onClick: () => router.push('/upload')
},
{
title: 'Video Library',
description: 'Browse all your videos',
icon: 'i-heroicons-film',
color: 'bg-gradient-to-br from-blue-100 to-blue-50',
iconColor: 'text-blue-600',
onClick: () => router.push('/video')
},
{
title: 'Analytics',
description: 'Track performance & insights',
icon: 'i-heroicons-chart-bar',
color: 'bg-gradient-to-br from-purple-100 to-purple-50',
iconColor: 'text-purple-600',
onClick: () => {}
},
{
title: 'Manage Plan',
description: 'Upgrade or change your plan',
icon: 'i-heroicons-credit-card',
color: 'bg-gradient-to-br from-orange-100 to-orange-50',
iconColor: 'text-orange-600',
onClick: () => router.push('/plans')
},
];
const fetchDashboardData = async () => {
loading.value = true;
try {
// Fetch recent videos
const response = await client.videos.videosList({ page: 1, limit: 5 });
const body = response.data as any;
if (body.data && Array.isArray(body.data)) {
recentVideos.value = body.data;
stats.value.totalVideos = body.data.length;
} else if (Array.isArray(body)) {
recentVideos.value = body;
stats.value.totalVideos = body.length;
}
// Calculate mock stats
stats.value.totalViews = recentVideos.value.reduce((sum, v: any) => sum + (v.views || 0), 0);
stats.value.storageUsed = recentVideos.value.reduce((sum, v) => sum + (v.size || 0), 0);
stats.value.uploadsThisMonth = recentVideos.value.filter(v => {
const uploadDate = new Date(v.created_at || '');
const now = new Date();
return uploadDate.getMonth() === now.getMonth() && uploadDate.getFullYear() === now.getFullYear();
}).length;
} catch (err) {
console.error('Failed to fetch dashboard data:', err);
} finally {
loading.value = false;
}
};
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const formatDuration = (seconds?: number) => {
if (!seconds) return '0:00';
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
};
const formatDate = (dateString?: string) => {
if (!dateString) return '';
return new Date(dateString).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
};
const getStatusClass = (status?: string) => {
switch(status?.toLowerCase()) {
case 'ready': return 'bg-green-100 text-green-700';
case 'processing': return 'bg-yellow-100 text-yellow-700';
case 'failed': return 'bg-red-100 text-red-700';
default: return 'bg-gray-100 text-gray-700';
}
};
const storagePercentage = computed(() => {
return Math.round((stats.value.storageUsed / stats.value.storageLimit) * 100);
});
const storageBreakdown = computed(() => {
const videoSize = stats.value.storageUsed;
const thumbSize = stats.value.totalVideos * 300 * 1024; // ~300KB per thumbnail
const otherSize = stats.value.totalVideos * 100 * 1024; // ~100KB other files
const total = videoSize + thumbSize + otherSize;
return [
{ label: 'Videos', size: videoSize, percentage: (videoSize / total) * 100, color: 'bg-primary' },
{ label: 'Thumbnails & Assets', size: thumbSize, percentage: (thumbSize / total) * 100, color: 'bg-blue-500' },
{ label: 'Other Files', size: otherSize, percentage: (otherSize / total) * 100, color: 'bg-gray-400' },
];
});
onMounted(() => {
fetchDashboardData();
});
</script>
<template>
<div class="dashboard-overview">
<PageHeader
title="Dashboard"
description="Welcome back! Here's what's happening with your videos."
:breadcrumbs="[
{ label: 'Dashboard' }
]"
/>
<!-- Loading State -->
<div v-if="loading" class="animate-pulse">
<!-- Stats Grid Skeleton -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div v-for="i in 4" :key="i" class="bg-white rounded-xl border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4">
<div class="space-y-2">
<Skeleton width="5rem" height="1rem" class="mb-2 rounded" />
<Skeleton width="8rem" height="2rem" class="rounded" />
</div>
<Skeleton width="3rem" height="3rem" class="rounded-full" />
</div>
<Skeleton width="4rem" height="1rem" class="rounded" />
</div>
</div>
<!-- Quick Actions Skeleton -->
<div class="mb-8">
<Skeleton width="10rem" height="1.5rem" class="mb-4 rounded" />
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div v-for="i in 4" :key="i" class="p-6 rounded-xl border border-gray-200">
<Skeleton width="3rem" height="3rem" class="mb-4 rounded-full" />
<Skeleton width="8rem" height="1.25rem" class="mb-2 rounded" />
<Skeleton width="100%" height="1rem" class="rounded" />
</div>
</div>
</div>
<!-- Recent Videos Skeleton -->
<div class="mb-8">
<div class="flex items-center justify-between mb-4">
<Skeleton width="8rem" height="1.5rem" class="rounded" />
<Skeleton width="5rem" height="1rem" class="rounded" />
</div>
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden">
<div class="p-4 border-b border-gray-200" v-for="i in 5" :key="i">
<div class="flex gap-4">
<Skeleton width="4rem" height="2.5rem" class="rounded" />
<div class="flex-1 space-y-2">
<Skeleton width="30%" height="1rem" class="rounded" />
<Skeleton width="20%" height="0.8rem" class="rounded" />
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else>
<!-- Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<StatsCard
title="Total Videos"
:value="stats.totalVideos"
icon="i-heroicons-film"
color="primary"
:trend="{ value: 12, isPositive: true }"
/>
<StatsCard
title="Total Views"
:value="stats.totalViews.toLocaleString()"
icon="i-heroicons-eye"
color="info"
:trend="{ value: 8, isPositive: true }"
/>
<StatsCard
title="Storage Used"
:value="`${formatBytes(stats.storageUsed)} / ${formatBytes(stats.storageLimit)}`"
icon="i-heroicons-server"
color="warning"
/>
<StatsCard
title="Uploads This Month"
:value="stats.uploadsThisMonth"
icon="i-heroicons-arrow-up-tray"
color="success"
:trend="{ value: 25, isPositive: true }"
/>
</div>
<!-- Quick Actions -->
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4">Quick Actions</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<button
v-for="action in quickActions"
:key="action.title"
@click="action.onClick"
:class="[
'p-6 rounded-xl text-left transition-all duration-200',
'border border-gray-200 hover:border-primary hover:shadow-lg',
'group press-animated',
action.color
]"
>
<div :class="['w-12 h-12 rounded-lg flex items-center justify-center mb-4 bg-white/80', action.iconColor]">
<span :class="[action.icon, 'w-6 h-6']" />
</div>
<h3 class="font-semibold mb-1 group-hover:text-primary transition-colors">{{ action.title }}</h3>
<p class="text-sm text-gray-600">{{ action.description }}</p>
</button>
</div>
</div>
<!-- Recent Videos -->
<div class="mb-8">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold">Recent Videos</h2>
<router-link
to="/video"
class="text-sm text-primary hover:underline font-medium flex items-center gap-1"
>
View all
<span class="i-heroicons-arrow-right w-4 h-4" />
</router-link>
</div>
<div v-if="recentVideos.length === 0" class="bg-white rounded-xl border border-gray-200 p-8 text-center">
<div class="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mx-auto mb-4">
<span class="i-heroicons-film w-8 h-8 text-gray-400" />
</div>
<p class="text-gray-600 mb-4">No videos yet</p>
<router-link
to="/upload"
class="inline-flex items-center gap-2 px-4 py-2 bg-primary hover:bg-primary-600 text-white rounded-lg font-medium transition-colors"
>
<span class="i-heroicons-plus w-5 h-5" />
Upload your first video
</router-link>
</div>
<div v-else class="bg-white rounded-xl border border-gray-200 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Video</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Duration</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Upload Date</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-for="video in recentVideos" :key="video.id" class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-16 h-10 bg-gray-200 rounded overflow-hidden flex-shrink-0">
<img v-if="video.thumbnail" :src="video.thumbnail" :alt="video.title" class="w-full h-full object-cover" />
<div v-else class="w-full h-full flex items-center justify-center">
<span class="i-heroicons-film text-gray-400 text-xl" />
</div>
</div>
<div class="min-w-0">
<p class="font-medium text-gray-900 truncate">{{ video.title }}</p>
<p class="text-sm text-gray-500 truncate">{{ video.description || 'No description' }}</p>
</div>
</div>
</td>
<td class="px-6 py-4">
<span :class="['px-2 py-1 text-xs font-medium rounded-full', getStatusClass(video.status)]">
{{ video.status || 'Unknown' }}
</span>
</td>
<td class="px-6 py-4 text-sm text-gray-500">
{{ formatDuration(video.duration) }}
</td>
<td class="px-6 py-4 text-sm text-gray-500">
{{ formatDate(video.created_at) }}
</td>
<td class="px-6 py-4">
<div class="flex items-center gap-2">
<button class="p-1.5 hover:bg-gray-100 rounded transition-colors" title="Edit">
<span class="i-heroicons-pencil w-4 h-4 text-gray-600" />
</button>
<button class="p-1.5 hover:bg-gray-100 rounded transition-colors" title="Share">
<span class="i-heroicons-share w-4 h-4 text-gray-600" />
</button>
<button class="p-1.5 hover:bg-red-100 rounded transition-colors" title="Delete">
<span class="i-heroicons-trash w-4 h-4 text-red-600" />
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Storage Usage -->
<div class="bg-white rounded-xl border border-gray-200 p-6">
<h2 class="text-xl font-semibold mb-4">Storage Usage</h2>
<div class="mb-4">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-700">
{{ formatBytes(stats.storageUsed) }} of {{ formatBytes(stats.storageLimit) }} used
</span>
<span class="text-sm font-medium" :class="storagePercentage > 80 ? 'text-danger' : 'text-gray-700'">
{{ storagePercentage }}%
</span>
</div>
<div class="h-3 bg-gray-200 rounded-full overflow-hidden">
<div
class="h-full transition-all duration-500 rounded-full"
:class="storagePercentage > 80 ? 'bg-danger' : 'bg-primary'"
:style="{ width: `${storagePercentage}%` }"
/>
</div>
</div>
<div class="space-y-2">
<div
v-for="item in storageBreakdown"
:key="item.label"
class="flex items-center justify-between text-sm"
>
<div class="flex items-center gap-2">
<div :class="['w-3 h-3 rounded-sm', item.color]" />
<span class="text-gray-700">{{ item.label }}</span>
</div>
<span class="text-gray-500">{{ formatBytes(item.size) }}</span>
</div>
</div>
<div v-if="storagePercentage > 80" class="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<div class="flex gap-2">
<span class="i-heroicons-exclamation-triangle w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" />
<div>
<p class="text-sm font-medium text-yellow-800">Storage running low</p>
<p class="text-sm text-yellow-700 mt-1">
Consider upgrading your plan to get more storage.
<router-link to="/plans" class="underline font-medium">View plans</router-link>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -1,87 +1,64 @@
<template>
<div class="w-full">
<Toast />
<Form
:initial-values="initialValues"
:resolver="forgotSchema"
class="flex flex-col gap-4 w-full"
@submit="onFormSubmit"
>
<template #default="{ form }">
<div class="text-sm text-gray-600 mb-2">
Enter your email address and we'll send you a link to reset your password.
</div>
<div class="w-full">
<Toast />
<Form
:initialValues="initialValues"
:validators="validators"
@submit="onFormSubmit"
class="flex flex-col gap-4 w-full"
>
<div class="text-sm text-gray-600 mb-2">
Enter your email address and we'll send you a link to reset your password.
</div>
<div class="flex flex-col gap-1">
<label for="email" class="text-sm font-medium text-gray-700">Email address</label>
<Input
name="email"
type="email"
placeholder="you@example.com"
fluid
/>
<Message
v-if="form.getFieldMeta('email')?.errorMap?.onChange"
severity="error"
size="sm"
>
{{ form.getFieldMeta('email')?.errorMap?.onChange }}
</Message>
</div>
<Field name="email" label="Email address">
<template #default="{ value, error, isInvalid }">
<Input name="email" type="email" placeholder="you@example.com" :modelValue="value" />
<div v-if="isInvalid" class="text-xs text-red-600 mt-1">{{ error }}</div>
</template>
</Field>
<Button type="submit" size="sm" fluid>
Send Reset Link
</Button>
<Button type="submit" label="Send Reset Link" />
<div class="text-center mt-2">
<router-link
to="/login"
replace
class="inline-flex items-center text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
></path>
</svg>
Back to Sign in
</router-link>
</div>
</template>
</Form>
</div>
<div class="text-center mt-2">
<router-link to="/login" replace
class="inline-flex items-center text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
Back to Sign in
</router-link>
</div>
</Form>
</div>
</template>
<script setup lang="ts">
import { client } from '@/api/client'
import Form from '@/components/form/Form.vue'
import Message from '@/components/form/Message.vue'
import Button from '@/components/ui/Button.vue'
import Input from '@/components/ui/Input.vue'
import Toast from '@/components/ui/Toast.vue'
import { useToast } from '@/composables/useToast'
import { reactive } from 'vue'
import { z } from 'zod'
import { client } from '@/api/client';
import { Button, Field, Form, Input, Toast } from '@/components/ui/form';
import { inject, reactive } from 'vue';
const toast = useToast()
const forgotSchema = z.object({
email: z.string().min(1, { message: 'Email is required.' }).email({ message: 'Invalid email address.' })
})
const toast = inject<{ add: (t: any) => void }>('toast');
const initialValues = reactive({
email: ''
})
email: ''
});
const onFormSubmit = async (values: any) => {
try {
await client.auth.forgotPasswordCreate({ email: values.email })
toast.success('Reset link sent', 'Success')
} catch (error: any) {
toast.error(error.message || 'An error occurred', 'Error')
}
}
const validators = {
email: [
(value: string) => !value ? 'Email is required.' : undefined,
(value: string) => !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? 'Invalid email address.' : undefined,
],
};
const onFormSubmit = (values: Record<string, any>) => {
client.auth.forgotPasswordCreate({ email: values.email })
.then(() => {
toast?.add({ severity: 'success', summary: 'Success', detail: 'Reset link sent', life: 3000 });
})
.catch((error: any) => {
toast?.add({ severity: 'error', summary: 'Error', detail: error.message || 'An error occurred', life: 3000 });
});
};
</script>

View File

@@ -23,6 +23,7 @@
</div>
</template>
<script setup lang="ts">
import vueHead from "@/components/VueHead";
import { useRoute } from 'vue-router';
const route = useRoute();

View File

@@ -1,157 +1,118 @@
<template>
<div class="w-full">
<Toast />
<Form
:initial-values="initialValues"
:resolver="loginSchema"
class="flex flex-col gap-4 w-full"
@submit="onFormSubmit"
>
<template #default="{ form }">
<div class="flex flex-col gap-1">
<label for="email" class="text-sm font-medium text-gray-700">Email</label>
<Input
name="email"
type="text"
placeholder="Enter your email"
fluid
:disabled="auth.loading"
/>
<Message
v-if="form.getFieldMeta('email')?.errorMap?.onChange"
severity="error"
size="sm"
>
{{ form.getFieldMeta('email')?.errorMap?.onChange }}
</Message>
</div>
<div class="flex flex-col gap-1">
<label for="password" class="text-sm font-medium text-gray-700">Password</label>
<InputPassword
name="password"
placeholder="Enter your password"
:feedback="false"
fluid
:disabled="auth.loading"
/>
<Message
v-if="form.getFieldMeta('password')?.errorMap?.onChange"
severity="error"
size="sm"
>
{{ form.getFieldMeta('password')?.errorMap?.onChange }}
</Message>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<Checkbox
:model-value="form.getFieldValue('rememberMe')"
binary
:disabled="auth.loading"
@update:model-value="form.setFieldValue('rememberMe', $event)"
/>
<label for="remember-me" class="text-sm text-gray-900">Remember me</label>
</div>
<div class="text-sm">
<router-link
to="/forgot"
class="text-blue-600 hover:text-blue-500 hover:underline"
>
Forgot password?
</router-link>
</div>
</div>
<Button
type="submit"
size="sm"
:loading="auth.loading"
fluid
<div class="w-full">
<Toast />
<Form
:initialValues="initialValues"
:validators="validators"
@submit="onFormSubmit"
class="flex flex-col gap-4 w-full"
>
{{ auth.loading ? 'Signing in...' : 'Sign in' }}
</Button>
<Field name="email" label="Email">
<template #default="{ value, error, isInvalid }">
<Input
name="email"
type="text"
placeholder="Enter your email"
:modelValue="value"
:disabled="auth.loading"
/>
<div v-if="isInvalid" class="text-xs text-red-600 mt-1">{{ error }}</div>
</template>
</Field>
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-gray-500">Or continue with</span>
</div>
</div>
<Field name="password" label="Password">
<template #default="{ value, error, isInvalid }">
<Input
name="password"
type="password"
placeholder="Enter your password"
:modelValue="value"
:disabled="auth.loading"
/>
<div v-if="isInvalid" class="text-xs text-red-600 mt-1">{{ error }}</div>
</template>
</Field>
<Button
size="sm"
type="button"
variant="outline"
class="w-full flex items-center justify-center gap-2"
:disabled="auth.loading"
@click="loginWithGoogle"
>
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path
d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032s2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.503,2.988,15.139,2,12.545,2C7.021,2,2.543,6.477,2.543,12s4.478,10,10.002,10c8.396,0,10.249-7.85,9.426-11.748L12.545,10.239z"
/>
</svg>
Google
</Button>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<input
id="remember-me"
type="checkbox"
v-model="initialValues.rememberMe"
class="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary/20"
/>
<label for="remember-me" class="text-sm text-gray-900">Remember me</label>
</div>
<div class="text-sm">
<router-link to="/forgot"
class="text-blue-600 hover:text-blue-500 hover:underline">Forgot
password?</router-link>
</div>
</div>
<div class="mt-2 flex flex-col items-center justify-center gap-1 text-sm text-gray-600">
<p class="text-center text-sm text-gray-600">
Don't have an account?
<router-link
to="/sign-up"
class="font-medium text-blue-600 hover:text-blue-500 hover:underline"
>
Sign up
</router-link>
</p>
</div>
</template>
</Form>
</div>
<Button type="submit" :label="auth.loading ? 'Signing in...' : 'Sign in'" :loading="auth.loading" />
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-gray-500">Or continue with</span>
</div>
</div>
<Button type="button" variant="outlined" label="Google" :loading="auth.loading" @click="loginWithGoogle">
<template #default>
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path
d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032s2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.503,2.988,15.139,2,12.545,2C7.021,2,2.543,6.477,2.543,12s4.478,10,10.002,10c8.396,0,10.249-7.85,9.426-11.748L12.545,10.239z" />
</svg>
</template>
</Button>
<div class="mt-2 flex flex-col items-center justify-center gap-1 text-sm text-gray-600">
<p class="text-center text-sm text-gray-600">
Don't have an account?
<router-link to="/sign-up" class="font-medium text-blue-600 hover:text-blue-500 hover:underline">Sign up</router-link>
</p>
</div>
</Form>
</div>
</template>
<script setup lang="ts">
import Form from '@/components/form/Form.vue'
import Message from '@/components/form/Message.vue'
import Button from '@/components/ui/Button.vue'
import Checkbox from '@/components/ui/Checkbox.vue'
import Input from '@/components/ui/Input.vue'
import InputPassword from '@/components/ui/InputPassword.vue'
import Toast from '@/components/ui/Toast.vue'
import { useToast } from '@/composables/useToast'
import { useAuthStore } from '@/stores/auth'
import { reactive, watch } from 'vue'
import { z } from 'zod'
import { Button, Field, Form, Input, Toast } from '@/components/ui/form';
import { useAuthStore } from '@/stores/auth';
import { inject, reactive, watch } from 'vue';
const auth = useAuthStore()
const toast = useToast()
const loginSchema = z.object({
email: z.string().min(1, { message: 'Email or username is required.' }),
password: z.string().min(1, { message: 'Password is required.' })
})
const initialValues = reactive({
email: '',
password: '',
rememberMe: false
})
const auth = useAuthStore();
const toast = inject<{ add: (t: any) => void }>('toast');
watch(() => auth.error, (newError) => {
if (newError) {
toast.error(String(auth.error), 'Error')
}
})
if (newError && toast) {
toast.add({ severity: 'error', summary: String(auth.error), detail: newError, life: 5000 });
}
});
const onFormSubmit = async (values: any) => {
await auth.login(values.email, values.password)
}
const initialValues = reactive({
email: '',
password: '',
rememberMe: false
});
const validators = {
email: [
(value: string) => !value ? 'Email or username is required.' : undefined,
],
password: [
(value: string) => !value ? 'Password is required.' : undefined,
],
};
const onFormSubmit = async (values: Record<string, any>) => {
auth.login(values.email, values.password);
};
const loginWithGoogle = () => {
auth.loginWithGoogle()
}
auth.loginWithGoogle();
};
</script>

View File

@@ -1,106 +1,71 @@
<template>
<div class="w-full">
<Form
:initial-values="initialValues"
:resolver="signupSchema"
class="flex flex-col gap-4 w-full"
@submit="onFormSubmit"
>
<template #default="{ form }">
<div class="flex flex-col gap-1">
<label for="name" class="text-sm font-medium text-gray-700">Full Name</label>
<Input
name="name"
placeholder="John Doe"
fluid
/>
<Message
v-if="form.getFieldMeta('name')?.errorMap?.onChange"
severity="error"
size="sm"
>
{{ form.getFieldMeta('name')?.errorMap?.onChange }}
</Message>
</div>
<div class="w-full">
<Form
:initialValues="initialValues"
:validators="validators"
@submit="onFormSubmit"
class="flex flex-col gap-4 w-full"
>
<Field name="name" label="Full Name">
<template #default="{ value, error, isInvalid }">
<Input name="name" type="text" placeholder="John Doe" :modelValue="value" />
<div v-if="isInvalid" class="text-xs text-red-600 mt-1">{{ error }}</div>
</template>
</Field>
<div class="flex flex-col gap-1">
<label for="email" class="text-sm font-medium text-gray-700">Email address</label>
<Input
name="email"
type="email"
placeholder="you@example.com"
fluid
/>
<Message
v-if="form.getFieldMeta('email')?.errorMap?.onChange"
severity="error"
size="sm"
>
{{ form.getFieldMeta('email')?.errorMap?.onChange }}
</Message>
</div>
<Field name="email" label="Email address">
<template #default="{ value, error, isInvalid }">
<Input name="email" type="email" placeholder="you@example.com" :modelValue="value" />
<div v-if="isInvalid" class="text-xs text-red-600 mt-1">{{ error }}</div>
</template>
</Field>
<div class="flex flex-col gap-1">
<label for="password" class="text-sm font-medium text-gray-700">Password</label>
<InputPassword
name="password"
placeholder="Create a password"
:feedback="true"
fluid
/>
<small class="text-gray-500">Must be at least 8 characters.</small>
<Message
v-if="form.getFieldMeta('password')?.errorMap?.onChange"
severity="error"
size="sm"
>
{{ form.getFieldMeta('password')?.errorMap?.onChange }}
</Message>
</div>
<Field name="password" label="Password">
<template #default="{ value, error, isInvalid }">
<Input name="password" type="password" placeholder="Create a password" :modelValue="value" />
<small class="text-gray-500">Must be at least 8 characters.</small>
<div v-if="isInvalid" class="text-xs text-red-600 mt-1">{{ error }}</div>
</template>
</Field>
<Button type="submit" size="sm" fluid>
Create Account
</Button>
<Button type="submit" label="Create Account" />
<p class="mt-4 text-center text-sm text-gray-600">
Already have an account?
<router-link
to="/login"
class="font-medium text-blue-600 hover:text-blue-500 hover:underline"
>
Sign in
</router-link>
</p>
</template>
</Form>
</div>
<p class="mt-4 text-center text-sm text-gray-600">
Already have an account?
<router-link to="/login" class="font-medium text-blue-600 hover:text-blue-500 hover:underline">Sign in</router-link>
</p>
</Form>
</div>
</template>
<script setup lang="ts">
import Form from '@/components/form/Form.vue'
import Message from '@/components/form/Message.vue'
import Button from '@/components/ui/Button.vue'
import Input from '@/components/ui/Input.vue'
import InputPassword from '@/components/ui/InputPassword.vue'
import { useAuthStore } from '@/stores/auth'
import { reactive } from 'vue'
import { z } from 'zod'
import { Button, Field, Form, Input } from '@/components/ui/form';
import { useAuthStore } from '@/stores/auth';
import { reactive } from 'vue';
const auth = useAuthStore()
const signupSchema = z.object({
name: z.string().min(1, { message: 'Name is required.' }),
email: z.string().min(1, { message: 'Email is required.' }).email({ message: 'Invalid email address.' }),
password: z.string().min(8, { message: 'Password must be at least 8 characters.' })
})
const auth = useAuthStore();
const initialValues = reactive({
name: '',
email: '',
password: ''
})
name: '',
email: '',
password: ''
});
const onFormSubmit = (values: any) => {
auth.register(values.name, values.email, values.password)
}
const validators = {
name: [
(value: string) => !value ? 'Name is required.' : undefined,
],
email: [
(value: string) => !value ? 'Email is required.' : undefined,
(value: string) => !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? 'Invalid email address.' : undefined,
],
password: [
(value: string) => !value ? 'Password is required.' : undefined,
(value: string) => value.length < 8 ? 'Password must be at least 8 characters.' : undefined,
],
};
const onFormSubmit = (values: Record<string, any>) => {
auth.register(values.name, values.email, values.password);
};
</script>

View File

@@ -3,7 +3,7 @@ import Chart from '@/components/icons/Chart.vue';
import Credit from '@/components/icons/Credit.vue';
import Upload from '@/components/icons/Upload.vue';
import Video from '@/components/icons/Video.vue';
import Skeleton from '@/components/ui/Skeleton.vue';
import { Skeleton } from '@/components/ui/form';
import { useRouter } from 'vue-router';
import Referral from './Referral.vue';
@@ -45,19 +45,19 @@ const quickActions = [
<template>
<div v-if="loading" class="mb-8">
<Skeleton width="10rem" height="1.5rem" class="mb-4"></Skeleton>
<Skeleton width="10rem" height="1.5rem" class="mb-4" />
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div v-for="i in 4" :key="i" class="p-6 rounded-xl border border-gray-200">
<Skeleton circle width="3rem" height="3rem" class="mb-4"></Skeleton>
<Skeleton width="8rem" height="1.25rem" class="mb-2"></Skeleton>
<Skeleton width="100%" height="1rem"></Skeleton>
<Skeleton width="3rem" height="3rem" borderRadius="9999px" class="mb-4" />
<Skeleton width="8rem" height="1.25rem" class="mb-2" />
<Skeleton width="100%" height="1rem" />
</div>
</div>
<div class="flex flex-col justify-between p-6 rounded-xl border border-gray-200">
<Skeleton width="10rem" height="2rem"></Skeleton>
<Skeleton width="100%" height="1.25rem" class="my-4"></Skeleton>
<Skeleton width="100%" height="1rem"></Skeleton>
<Skeleton width="10rem" height="2rem" />
<Skeleton width="100%" height="1.25rem" class="my-4" />
<Skeleton width="100%" height="1rem" />
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ModelVideo } from '@/api/client';
import EmptyState from '@/components/dashboard/EmptyState.vue';
import Skeleton from '@/components/ui/Skeleton.vue';
import { Skeleton } from '@/components/ui/form';
import { formatDate, formatDuration } from '@/lib/utils';
import { useRouter } from 'vue-router';
@@ -34,7 +34,7 @@ const getStatusClass = (status?: string) => {
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden">
<div class="p-4 border-b border-gray-200" v-for="i in 5" :key="i">
<div class="flex gap-4">
<Skeleton width="4rem" height="2.5rem" border-radius="0.25rem"></Skeleton>
<Skeleton width="4rem" height="2.5rem" class="rounded"></Skeleton>
<div class="flex-1 space-y-2">
<Skeleton width="30%" height="1rem"></Skeleton>
<Skeleton width="20%" height="0.8rem"></Skeleton>

View File

@@ -7,7 +7,7 @@
<p class="text-sm text-gray-600 font-medium">Share your referral link and earn commissions from
referred users!</p>
<div class="flex gap-2">
<Input class="w-full" readonly type="text" :value="url" @click="copyToClipboard" />
<InputText class="w-full" readonly type="text" :value="url" @click="copyToClipboard" />
<button class="btn btn-primary" @click="copyToClipboard" :disabled="isCopied">
<svg v-if="!isCopied" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
@@ -27,7 +27,6 @@
</div>
</template>
<script lang="ts" setup>
import Input from '@/components/ui/Input.vue';
import { useAuthStore } from '@/stores/auth';
import { ref } from 'vue';
const auth = useAuthStore()

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import StatsCard from '@/components/dashboard/StatsCard.vue';
import Skeleton from '@/components/ui/Skeleton.vue';
import { Skeleton } from '@/components/ui/form';
import { formatBytes } from '@/lib/utils';
interface Props {
@@ -22,12 +22,11 @@ defineProps<Props>();
<div v-for="i in 4" :key="i" class="bg-surface rounded-xl border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4">
<div class="space-y-2">
<Skeleton width="5rem" height="1rem" class="mb-2"></Skeleton>
<Skeleton width="8rem" height="2rem"></Skeleton>
<Skeleton width="5rem" height="1rem" class="mb-2" />
<Skeleton width="8rem" height="2rem" />
</div>
<!-- <Skeleton circle width="3rem" height="3rem"></Skeleton> -->
</div>
<Skeleton width="4rem" height="1rem"></Skeleton>
<Skeleton width="4rem" height="1rem" />
</div>
</div>

View File

@@ -1,177 +1,186 @@
<script setup lang="ts">
import { client, type ModelPlan } from '@/api/client'
import PageHeader from '@/components/dashboard/PageHeader.vue'
import { useSWRV } from '@/composables/useDataLoader'
import { useAuthStore } from '@/stores/auth'
import { computed, ref } from 'vue'
import CurrentPlanCard from './components/CurrentPlanCard.vue'
import EditPlanDialog from './components/EditPlanDialog.vue'
import ManageSubscriptionDialog from './components/ManageSubscriptionDialog.vue'
import PlanList from './components/PlanList.vue'
import PlanPaymentHistory from './components/PlanPaymentHistory.vue'
import UsageStatsCard from './components/UsageStatsCard.vue'
import { client, type ModelPlan } from '@/api/client';
import PageHeader from '@/components/dashboard/PageHeader.vue';
import { useAuthStore } from '@/stores/auth';
import { computed, onMounted, ref } from 'vue';
import CurrentPlanCard from './components/CurrentPlanCard.vue';
import EditPlanDialog from './components/EditPlanDialog.vue';
import ManageSubscriptionDialog from './components/ManageSubscriptionDialog.vue';
import PlanList from './components/PlanList.vue';
import PlanPaymentHistory from './components/PlanPaymentHistory.vue';
import UsageStatsCard from './components/UsageStatsCard.vue';
const auth = useAuthStore()
const subscribing = ref<string | null>(null)
const showManageDialog = ref(false)
const cancelling = ref(false)
const auth = useAuthStore();
const subscribing = ref<string | null>(null);
const showManageDialog = ref(false);
const cancelling = ref(false);
const isLoading = ref(true);
const plansData = ref<any>(null);
// Mock Payment History Data
const paymentHistory = ref([
{ id: 'inv_001', date: 'Oct 24, 2025', amount: 9.99, plan: 'Basic Plan', status: 'success', invoiceId: 'INV-2025-001' },
{ id: 'inv_002', date: 'Nov 24, 2025', amount: 9.99, plan: 'Basic Plan', status: 'success', invoiceId: 'INV-2025-002' },
{ id: 'inv_003', date: 'Dec 24, 2025', amount: 19.99, plan: 'Pro Plan', status: 'failed', invoiceId: 'INV-2025-003' },
{ id: 'inv_004', date: 'Jan 24, 2026', amount: 19.99, plan: 'Pro Plan', status: 'pending', invoiceId: 'INV-2026-001' },
])
{ id: 'inv_001', date: 'Oct 24, 2025', amount: 9.99, plan: 'Basic Plan', status: 'success', invoiceId: 'INV-2025-001' },
{ id: 'inv_002', date: 'Nov 24, 2025', amount: 9.99, plan: 'Basic Plan', status: 'success', invoiceId: 'INV-2025-002' },
{ id: 'inv_003', date: 'Dec 24, 2025', amount: 19.99, plan: 'Pro Plan', status: 'failed', invoiceId: 'INV-2025-003' },
{ id: 'inv_004', date: 'Jan 24, 2026', amount: 19.99, plan: 'Pro Plan', status: 'pending', invoiceId: 'INV-2026-001' },
]);
const { data, isLoading, mutate: mutatePlans } = useSWRV('r/plans', () => client.plans.plansList())
const fetchPlans = async () => {
isLoading.value = true;
try {
plansData.value = await client.plans.plansList();
} catch (e) {
console.error('Failed to fetch plans', e);
} finally {
isLoading.value = false;
}
};
onMounted(() => {
fetchPlans();
});
// Computed Usage (Mock if not in store)
const storageUsed = computed(() => auth.user?.storage_used || 0) // bytes
const storageLimit = computed(() => 10737418240)
const uploadsUsed = ref(12)
const uploadsLimit = ref(50)
const storageUsed = computed(() => auth.user?.storage_used || 0); // bytes
const storageLimit = computed(() => 10737418240);
const uploadsUsed = ref(12);
const uploadsLimit = ref(50);
const currentPlanId = computed(() => {
if (auth.user?.plan_id) return auth.user.plan_id
if (Array.isArray(data.value?.data?.data?.plans) && data.value?.data?.data?.plans.length > 0) return data.value.data.data.plans[0].id
return undefined
})
if (auth.user?.plan_id) return auth.user.plan_id;
if (Array.isArray(plansData.value?.data?.data?.plans) && plansData.value?.data?.data?.plans.length > 0) return plansData.value.data.data.plans[0].id;
return undefined;
});
const currentPlan = computed(() => {
if (!Array.isArray(data.value?.data?.data?.plans)) return undefined
return data.value.data.data.plans.find((p: ModelPlan) => p.id === currentPlanId.value)
})
if (!Array.isArray(plansData.value?.data?.data?.plans)) return undefined;
return plansData.value.data.data.plans.find((p: ModelPlan) => p.id === currentPlanId.value);
});
const showEditDialog = ref(false)
const editingPlan = ref<ModelPlan>({} as ModelPlan)
const isSaving = ref(false)
const showEditDialog = ref(false);
const editingPlan = ref<ModelPlan>({});
const isSaving = ref(false);
const openEditPlan = (plan: ModelPlan) => {
editingPlan.value = { ...plan }
showEditDialog.value = true
}
editingPlan.value = { ...plan };
showEditDialog.value = true;
};
const savePlan = async (updatedPlan: ModelPlan) => {
isSaving.value = true
try {
if (!updatedPlan.id) return
isSaving.value = true;
try {
if (!updatedPlan.id) return;
await client.request({
path: `/plans/${updatedPlan.id}`,
method: 'PUT',
body: updatedPlan
})
await client.request({
path: `/plans/${updatedPlan.id}`,
method: 'PUT',
body: updatedPlan
});
await mutatePlans()
showEditDialog.value = false
alert('Plan updated successfully')
} catch (e: any) {
console.error('Failed to update plan', e)
const idx = data.value!.data.data.plans.findIndex((p: ModelPlan) => p.id === updatedPlan.id)
if (idx !== -1) {
data.value!.data.data.plans[idx] = { ...updatedPlan }
await fetchPlans();
showEditDialog.value = false;
alert('Plan updated successfully');
} catch (e: any) {
console.error('Failed to update plan', e);
showEditDialog.value = false;
} finally {
isSaving.value = false;
}
showEditDialog.value = false
} finally {
isSaving.value = false
}
}
};
const subscribe = async (plan: ModelPlan) => {
if (!plan.id) return
subscribing.value = plan.id
if (!plan.id) return;
subscribing.value = plan.id;
try {
await client.payments.paymentsCreate({
amount: plan.price || 0,
plan_id: plan.id
})
alert(`Successfully subscribed to ${plan.name}`)
await client.payments.paymentsCreate({
amount: plan.price || 0,
plan_id: plan.id
});
alert(`Successfully subscribed to ${plan.name}`);
paymentHistory.value.unshift({
id: `inv_${Date.now()}`,
date: new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }),
amount: plan.price || 0,
plan: plan.name || 'Unknown',
status: 'success',
invoiceId: `INV-${new Date().getFullYear()}-${Math.floor(Math.random() * 1000)}`
})
paymentHistory.value.unshift({
id: `inv_${Date.now()}`,
date: new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }),
amount: plan.price || 0,
plan: plan.name || 'Unknown',
status: 'success',
invoiceId: `INV-${new Date().getFullYear()}-${Math.floor(Math.random() * 1000)}`
});
} catch (err: any) {
console.error(err)
alert('Failed to subscribe: ' + (err.message || 'Unknown error'))
console.error(err);
alert('Failed to subscribe: ' + (err.message || 'Unknown error'));
} finally {
subscribing.value = null
subscribing.value = null;
}
}
};
const cancelSubscription = async () => {
cancelling.value = true
try {
await new Promise(resolve => setTimeout(resolve, 1500))
alert('Subscription has been canceled.')
showManageDialog.value = false
} catch (e) {
alert('Failed to cancel subscription.')
} finally {
cancelling.value = false
}
}
cancelling.value = true;
try {
await new Promise(resolve => setTimeout(resolve, 1500));
alert('Subscription has been canceled.');
showManageDialog.value = false;
} catch (e) {
alert('Failed to cancel subscription.');
} finally {
cancelling.value = false;
}
};
</script>
<template>
<div class="plans-page">
<PageHeader
title="Subscription"
description="Manage your workspace plan and usage"
:breadcrumbs="[
{ label: 'Dashboard', to: '/' },
{ label: 'Subscription' }
]"
title="Subscription"
description="Manage your workspace plan and usage"
:breadcrumbs="[
{ label: 'Dashboard', to: '/' },
{ label: 'Subscription' }
]"
/>
<div class="content max-w-7xl mx-auto space-y-12 pb-12">
<!-- Hero Section: Current Plan & Usage -->
<div v-if="!isLoading" class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<CurrentPlanCard
:current-plan="currentPlan"
@manage="showManageDialog = true"
<!-- Hero Section: Current Plan & Usage -->
<div v-if="!isLoading" class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<CurrentPlanCard
:current-plan="currentPlan"
@manage="showManageDialog = true"
/>
<UsageStatsCard
:storage-used="storageUsed"
:storage-limit="storageLimit"
:uploads-used="uploadsUsed"
:uploads-limit="uploadsLimit"
/>
</div>
<PlanList
:plans="plansData?.data?.data?.plans || []"
:is-loading="!!isLoading"
:current-plan-id="currentPlanId"
:subscribing-plan-id="subscribing"
:is-admin="auth.user?.role === 'admin'"
@subscribe="subscribe"
@edit="openEditPlan"
/>
<UsageStatsCard
:storage-used="storageUsed"
:storage-limit="storageLimit"
:uploads-used="uploadsUsed"
:uploads-limit="uploadsLimit"
<PlanPaymentHistory :history="paymentHistory" />
<ManageSubscriptionDialog
v-model:visible="showManageDialog"
:current-plan="currentPlan"
:cancelling="cancelling"
@cancel-subscription="cancelSubscription"
/>
</div>
<PlanList
:plans="data?.data?.data?.plans || []"
:is-loading="!!isLoading"
:current-plan-id="currentPlanId"
:subscribing-plan-id="subscribing"
:is-admin="auth.user?.role === 'admin'"
@subscribe="subscribe"
@edit="openEditPlan"
/>
<PlanPaymentHistory :history="paymentHistory" />
<ManageSubscriptionDialog
v-model:visible="showManageDialog"
:current-plan="currentPlan"
:cancelling="cancelling"
@cancel-subscription="cancelSubscription"
/>
</div>
<EditPlanDialog
v-model:visible="showEditDialog"
:plan="editingPlan"
:loading="isSaving"
@save="savePlan"
v-model:visible="showEditDialog"
:plan="editingPlan"
:loading="isSaving"
@save="savePlan"
/>
</div>
</template>

View File

@@ -1,44 +1,38 @@
<script setup lang="ts">
import type { ModelPlan } from '@/api/client';
import Button from '@/components/ui/Button.vue';
import Tag from '@/components/ui/Tag.vue';
import { computed } from 'vue';
import { type ModelPlan } from '@/api/client';
import { Button, Tag } from '@/components/ui/form';
const props = defineProps<{
currentPlan?: ModelPlan
}>()
defineProps<{
currentPlan?: ModelPlan;
}>();
const emit = defineEmits<{
(e: 'manage'): void
}>()
const planName = computed(() => props.currentPlan?.name || 'Free Plan')
const planPrice = computed(() => props.currentPlan?.price || 0)
const planCycle = computed(() => props.currentPlan?.cycle || 'month')
const isActive = computed(() => props.currentPlan?.is_active !== false)
defineEmits<{
(e: 'manage'): void;
}>();
</script>
<template>
<div class="bg-gradient-to-br from-blue-600 to-purple-700 rounded-2xl p-8 text-white">
<div class="flex items-start justify-between mb-4">
<div>
<p class="text-blue-100 text-sm font-medium mb-1">Current Plan</p>
<h3 class="text-3xl font-bold">{{ planName }}</h3>
</div>
<Tag
:value="isActive ? 'Active' : 'Inactive'"
:severity="isActive ? 'success' : 'danger'"
class="!bg-white/20 !text-white !border-white/30"
/>
</div>
<div class="lg:col-span-2 relative overflow-hidden rounded-2xl bg-gradient-to-br from-gray-900 to-gray-800 text-white p-8">
<!-- Background decorations -->
<div class="absolute top-0 right-0 -mt-16 -mr-16 w-64 h-64 bg-primary-500 rounded-full blur-3xl opacity-20"></div>
<div class="absolute bottom-0 left-0 -mb-16 -ml-16 w-64 h-64 bg-purple-500 rounded-full blur-3xl opacity-20"></div>
<div class="flex items-baseline gap-1 mb-6">
<span class="text-4xl font-bold">${{ planPrice }}</span>
<span class="text-blue-100">/{{ planCycle }}</span>
</div>
<div class="relative z-10 flex flex-col h-full justify-between">
<div class="flex justify-between items-start">
<div>
<h2 class="text-sm font-medium text-gray-400 uppercase tracking-wider mb-1">Current Plan</h2>
<h3 class="text-4xl font-bold text-white mb-2">{{ currentPlan?.name || 'Standard Plan' }}</h3>
<Tag value="Active" severity="success" />
</div>
<div class="text-right">
<div class="text-3xl font-bold text-white">${{ currentPlan?.price || 0 }}<span class="text-lg text-gray-400 font-normal">/mo</span></div>
<p class="text-gray-400 text-sm mt-1">Next billing on Feb 24, 2026</p>
</div>
</div>
<Button variant="outline" class="!text-white !border-white/50 hover:!bg-white/20" @click="emit('manage')">
Manage Subscription
</Button>
</div>
<div class="mt-8 pt-8 border-t border-gray-700/50 flex gap-4">
<Button label="Manage Subscription" variant="secondary" @click="$emit('manage')" />
</div>
</div>
</div>
</template>

View File

@@ -1,107 +1,132 @@
<script setup lang="ts">
import { type ModelPlan } from '@/api/client'
import Button from '@/components/ui/Button.vue'
import Checkbox from '@/components/ui/Checkbox.vue'
import Dialog from '@/components/ui/Dialog.vue'
import Input from '@/components/ui/Input.vue'
import { computed, ref, watch } from 'vue'
import { type ModelPlan } from '@/api/client';
import { Button, Dialog } from '@/components/ui/form';
import { computed, ref, watch } from 'vue';
const props = defineProps<{
visible: boolean
plan: ModelPlan
loading?: boolean
}>()
visible: boolean;
plan: ModelPlan;
loading?: boolean;
}>();
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'save', plan: ModelPlan): void
}>()
(e: 'update:visible', value: boolean): void;
(e: 'save', plan: ModelPlan): void;
}>();
// Create a local copy to edit
const localPlan = ref<ModelPlan>({} as ModelPlan)
const localPlan = ref<ModelPlan>({});
// Sync when dialog opens or plan changes
watch(() => props.plan, (newPlan) => {
localPlan.value = { ...newPlan }
}, { immediate: true })
localPlan.value = { ...newPlan };
}, { immediate: true });
const onSave = () => {
emit('save', localPlan.value)
}
emit('save', localPlan.value);
};
const handleClose = () => {
emit('update:visible', false)
}
const isActive = computed({
get: () => localPlan.value.is_active ?? false,
set: (val: boolean) => {
localPlan.value.is_active = val
}
})
const visibleModel = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
});
</script>
<template>
<Dialog
:visible="visible"
header="Edit Plan"
width="40rem"
:closable="true"
@update:visible="handleClose"
>
<div class="space-y-4">
<div class="flex flex-col gap-2">
<label for="plan-name" class="text-sm font-medium text-gray-700">Name</label>
<Input id="plan-name" v-model="localPlan.name" placeholder="Plan Name" />
</div>
<Dialog v-model:visible="visibleModel" header="Edit Plan" :style="{ width: '40rem' }">
<div class="space-y-4">
<div class="flex flex-col gap-2">
<label for="plan-name" class="text-sm font-medium text-gray-700">Name</label>
<input
id="plan-name"
v-model="localPlan.name"
type="text"
placeholder="Plan Name"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
/>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="flex flex-col gap-2">
<label for="plan-price" class="text-sm font-medium text-gray-700">Price ($)</label>
<Input id="plan-price" :model-value="localPlan.price ?? ''" type="number" placeholder="0.00" @update:model-value="localPlan.price = Number($event)" />
</div>
<div class="flex flex-col gap-2">
<label for="plan-cycle" class="text-sm font-medium text-gray-700">Billing Cycle</label>
<Input id="plan-cycle" v-model="localPlan.cycle" placeholder="e.g. month, year" />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="flex flex-col gap-2">
<label for="plan-price" class="text-sm font-medium text-gray-700">Price ($)</label>
<input
id="plan-price"
v-model="localPlan.price"
type="number"
placeholder="Price"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
/>
</div>
<div class="flex flex-col gap-2">
<label for="plan-cycle" class="text-sm font-medium text-gray-700">Billing Cycle</label>
<input
id="plan-cycle"
v-model="localPlan.cycle"
type="text"
placeholder="e.g. month, year"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
/>
</div>
</div>
<div class="flex flex-col gap-2">
<label for="plan-desc" class="text-sm font-medium text-gray-700">Description</label>
<textarea
id="plan-desc"
v-model="localPlan.description"
rows="2"
class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
/>
</div>
<div class="flex flex-col gap-2">
<label for="plan-desc" class="text-sm font-medium text-gray-700">Description</label>
<textarea
id="plan-desc"
v-model="localPlan.description"
rows="2"
placeholder="Description"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary resize-none"
/>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="flex flex-col gap-2">
<label for="plan-storage" class="text-sm font-medium text-gray-700">Storage Limit (bytes)</label>
<Input id="plan-storage" :model-value="localPlan.storage_limit ?? ''" type="number" @update:model-value="localPlan.storage_limit = Number($event)" />
</div>
<div class="flex flex-col gap-2">
<label for="plan-uploads" class="text-sm font-medium text-gray-700">Upload Limit (per day)</label>
<Input id="plan-uploads" :model-value="localPlan.upload_limit ?? ''" type="number" @update:model-value="localPlan.upload_limit = Number($event)" />
</div>
<div class="flex flex-col gap-2">
<label for="plan-duration" class="text-sm font-medium text-gray-700">Duration Limit (sec)</label>
<Input id="plan-duration" :model-value="localPlan.duration_limit ?? ''" type="number" @update:model-value="localPlan.duration_limit = Number($event)" />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="flex flex-col gap-2">
<label for="plan-storage" class="text-sm font-medium text-gray-700">Storage Limit (bytes)</label>
<input
id="plan-storage"
v-model="localPlan.storage_limit"
type="number"
placeholder="Storage limit"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
/>
</div>
<div class="flex flex-col gap-2">
<label for="plan-uploads" class="text-sm font-medium text-gray-700">Upload Limit (per day)</label>
<input
id="plan-uploads"
v-model="localPlan.upload_limit"
type="number"
placeholder="Upload limit"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
/>
</div>
<div class="flex flex-col gap-2">
<label for="plan-duration" class="text-sm font-medium text-gray-700">Duration Limit (sec)</label>
<input
id="plan-duration"
v-model="localPlan.duration_limit"
type="number"
placeholder="Duration limit"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
/>
</div>
</div>
<div class="flex items-center gap-2 pt-2">
<Checkbox v-model="isActive" :binary="true" />
<label class="text-sm font-medium text-gray-700">Active</label>
</div>
</div>
<div class="flex items-center gap-2 pt-2">
<input
type="checkbox"
id="plan-active"
v-model="localPlan.is_active"
class="w-4 h-4 rounded border-gray-300"
/>
<label for="plan-active" class="text-sm font-medium text-gray-700">Active</label>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<Button variant="outline" @click="handleClose">Cancel</Button>
<Button :loading="loading" @click="onSave">Save Changes</Button>
</div>
</template>
</Dialog>
<template #footer>
<Button variant="secondary" label="Cancel" @click="visibleModel = false" />
<Button label="Save Changes" @click="onSave" :loading="loading" />
</template>
</Dialog>
</template>

View File

@@ -1,55 +1,54 @@
<script setup lang="ts">
import type { ModelPlan } from '@/api/client';
import Button from '@/components/ui/Button.vue';
import Dialog from '@/components/ui/Dialog.vue';
import { type ModelPlan } from '@/api/client';
import { Button, Dialog } from '@/components/ui/form';
import { computed } from 'vue';
const props = defineProps<{
visible: boolean
currentPlan?: ModelPlan
cancelling?: boolean
}>()
visible: boolean;
currentPlan?: ModelPlan;
cancelling?: boolean;
}>();
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'cancel-subscription'): void
}>()
(e: 'update:visible', value: boolean): void;
(e: 'cancel-subscription'): void;
}>();
const handleClose = () => {
emit('update:visible', false)
}
const visibleModel = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
});
</script>
<template>
<Dialog
:visible="visible"
header="Manage Subscription"
width="28rem"
:closable="true"
@update:visible="handleClose"
>
<div class="space-y-4">
<div v-if="currentPlan" class="bg-gray-50 rounded-lg p-4">
<h4 class="font-medium text-gray-900">{{ currentPlan.name }}</h4>
<p class="text-sm text-gray-500">${{ currentPlan.price }}/{{ currentPlan.cycle }}</p>
</div>
<div class="border-t border-gray-200 pt-4">
<h4 class="font-medium text-gray-900 mb-2">Cancel Subscription</h4>
<p class="text-sm text-gray-600 mb-4">
If you cancel, you'll lose access to premium features at the end of your billing period.
<Dialog v-model:visible="visibleModel" header="Manage Subscription" :style="{ width: '30rem' }">
<div class="mb-4">
<p class="text-gray-600 mb-4">You are currently subscribed to <span class="font-bold text-gray-900">{{ currentPlan?.name }}</span>.</p>
<div class="bg-gray-50 p-4 rounded-lg space-y-2 border border-gray-200">
<div class="flex justify-between">
<span class="text-sm text-gray-500">Status</span>
<span class="text-sm font-medium text-green-600">Active</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-gray-500">Renewal Date</span>
<span class="text-sm font-medium text-gray-900">Feb 24, 2026</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-gray-500">Amount</span>
<span class="text-sm font-medium text-gray-900">${{ currentPlan?.price || 0 }}/mo</span>
</div>
</div>
</div>
<p class="text-sm text-gray-600 mb-6">
Canceling your subscription will downgrade you to the Free plan at the end of your current billing period.
</p>
<Button
variant="danger"
:loading="cancelling"
@click="emit('cancel-subscription')"
>
Cancel Subscription
</Button>
</div>
</div>
<template #footer>
<Button variant="outline" @click="handleClose">Close</Button>
</template>
</Dialog>
<div class="flex justify-end gap-2">
<Button variant="secondary" label="Close" @click="visibleModel = false" />
<Button
label="Cancel Subscription"
@click="emit('cancel-subscription')"
:disabled="cancelling"
/>
</div>
</Dialog>
</template>

View File

@@ -1,99 +1,101 @@
<script setup lang="ts">
import type { ModelPlan } from '@/api/client';
import Button from '@/components/ui/Button.vue';
import Skeleton from '@/components/ui/Skeleton.vue';
import { type ModelPlan } from '@/api/client';
import { Button, Skeleton } from '@/components/ui/form';
import { formatBytes } from '@/lib/utils';
const props = defineProps<{
plans: ModelPlan[]
isLoading: boolean
currentPlanId?: string
subscribingPlanId?: string | null
isAdmin?: boolean
}>()
defineProps<{
plans: ModelPlan[];
isLoading: boolean;
currentPlanId?: string;
subscribingPlanId?: string | null;
isAdmin?: boolean;
}>();
const emit = defineEmits<{
(e: 'subscribe', plan: ModelPlan): void
(e: 'edit', plan: ModelPlan): void
}>()
(e: 'subscribe', plan: ModelPlan): void;
(e: 'edit', plan: ModelPlan): void;
}>();
const formatDuration = (seconds?: number) => {
if (!seconds) return '0 mins';
return `${Math.floor(seconds / 60)} mins`;
};
const isPopular = (plan: ModelPlan) => {
return plan.name?.toLowerCase().includes('pro') || plan.name?.toLowerCase().includes('premium');
};
const isCurrentComp = (plan: ModelPlan, currentId?: string) => {
return plan.id === currentId;
}
</script>
<template>
<section>
<h2 class="text-2xl font-bold mb-6 text-gray-900">Available Plans</h2>
<!-- Loading State -->
<div v-if="isLoading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div v-for="i in 3" :key="i" class="bg-white border border-gray-200 rounded-xl p-6">
<Skeleton height="1.5rem" width="60%" class="mb-4" />
<Skeleton height="2rem" width="40%" class="mb-6" />
<Skeleton height="1rem" class="mb-2" />
<Skeleton height="1rem" class="mb-2" />
<Skeleton height="1rem" width="80%" class="mb-6" />
<Skeleton height="2.5rem" />
</div>
</div>
<!-- Plans Grid -->
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="plan in plans"
:key="plan.id"
class="bg-white border rounded-xl p-6 transition-all hover:shadow-lg"
:class="plan.id === currentPlanId ? 'border-blue-500 ring-2 ring-blue-500/20' : 'border-gray-200'"
>
<div class="flex items-start justify-between mb-4">
<div>
<h3 class="text-lg font-bold text-gray-900">{{ plan.name }}</h3>
<p class="text-sm text-gray-500">{{ plan.cycle }}</p>
</div>
<div v-if="plan.id === currentPlanId" class="text-xs font-medium text-blue-600 bg-blue-50 px-2 py-1 rounded-full">
Current
</div>
<section>
<div class="flex items-center justify-between mb-8">
<h2 class="text-2xl font-bold text-gray-900">Upgrade your workspace</h2>
</div>
<div class="mb-6">
<span class="text-3xl font-bold text-gray-900">${{ plan.price }}</span>
<span class="text-gray-500">/{{ plan.cycle }}</span>
<!-- Loading State -->
<div v-if="isLoading" class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div v-for="i in 3" :key="i" class="h-full">
<Skeleton height="300px" borderRadius="16px" />
</div>
</div>
<p class="text-sm text-gray-600 mb-6">{{ plan.description }}</p>
<div v-else class="grid grid-cols-1 md:grid-cols-3 gap-8 items-start">
<div v-for="plan in plans" :key="plan.id" class="relative group h-full">
<div v-if="isPopular(plan) && !isCurrentComp(plan, currentPlanId)" class="absolute -top-3 left-1/2 -translate-x-1/2 bg-primary text-white text-xs font-bold px-3 py-1 rounded-full z-10 shadow-md uppercase tracking-wide">
Recommended
</div>
<div class="space-y-2 mb-6 text-sm">
<div class="flex items-center gap-2">
<span class="i-heroicons-check-circle text-green-500 w-4 h-4" />
<span>{{ formatBytes(plan.storage_limit || 0) }} storage</span>
</div>
<div class="flex items-center gap-2">
<span class="i-heroicons-check-circle text-green-500 w-4 h-4" />
<span>{{ plan.upload_limit }} uploads/day</span>
</div>
<div class="flex items-center gap-2">
<span class="i-heroicons-check-circle text-green-500 w-4 h-4" />
<span>{{ plan.duration_limit }}s video duration</span>
</div>
</div>
<!-- Admin Edit Button -->
<Button
v-if="isAdmin"
class="absolute top-2 right-2 z-20"
variant="secondary"
@click.stop="emit('edit', plan)"
/>
<div class="flex gap-2">
<Button
v-if="isAdmin"
variant="outline"
class="flex-1"
@click="emit('edit', plan)"
>
Edit
</Button>
<Button
class="flex-1"
:variant="plan.id === currentPlanId ? 'outline' : 'primary'"
:loading="subscribingPlanId === plan.id"
:disabled="plan.id === currentPlanId"
@click="emit('subscribe', plan)"
>
{{ plan.id === currentPlanId ? 'Current Plan' : 'Subscribe' }}
</Button>
<div :class="[
'relative bg-white rounded-2xl p-6 h-full border transition-all duration-200 flex flex-col',
isCurrentComp(plan, currentPlanId) ? 'border-primary ring-1 ring-primary/50 bg-primary-50/10' : 'border-gray-200 hover:border-gray-300 hover:shadow-lg',
isPopular(plan) && !isCurrentComp(plan, currentPlanId) ? 'shadow-md border-primary/20' : ''
]">
<div class="mb-4">
<h3 class="text-xl font-bold text-gray-900">{{ plan.name }}</h3>
<p class="text-gray-500 text-sm min-h-[2.5rem] mt-2">{{ plan.description }}</p>
</div>
<div class="mb-6">
<span class="text-4xl font-bold text-gray-900">${{ plan.price }}</span>
<span class="text-gray-500 text-sm">/{{ plan.cycle }}</span>
</div>
<ul class="space-y-3 mb-8 flex-grow">
<li class="flex items-center gap-3 text-sm text-gray-700">
<span class="i-heroicons-check-circle text-green-500 text-lg flex-shrink-0"></span>
{{ formatBytes(plan.storage_limit || 0) }} Storage
</li>
<li class="flex items-center gap-3 text-sm text-gray-700">
<span class="i-heroicons-check-circle text-green-500 text-lg flex-shrink-0"></span>
{{ formatDuration(plan.duration_limit) }} Max Duration
</li>
<li class="flex items-center gap-3 text-sm text-gray-700">
<span class="i-heroicons-check-circle text-green-500 text-lg flex-shrink-0"></span>
{{ plan.upload_limit }} Uploads / day
</li>
</ul>
<Button
:label="isCurrentComp(plan, currentPlanId) ? 'Current Plan' : (subscribingPlanId === plan.id ? 'Processing...' : 'Upgrade')"
class="w-full"
:variant="isCurrentComp(plan, currentPlanId) ? 'outlined' : 'primary'"
:disabled="!!subscribingPlanId || isCurrentComp(plan, currentPlanId)"
@click="emit('subscribe', plan)"
/>
</div>
</div>
</div>
</div>
</div>
</section>
</section>
</template>

View File

@@ -1,78 +1,83 @@
<script setup lang="ts">
import { createColumnHelper } from '@/components/table/Column'
import DataTable from '@/components/table/DataTable.vue'
import Tag from '@/components/ui/Tag.vue'
import Toast from '@/components/ui/Toast.vue'
import { useToast } from '@/composables/useToast'
import { h } from 'vue'
import { Tag } from '@/components/ui/form';
import { inject } from 'vue';
interface PaymentHistoryItem {
id: string
date: string
amount: number
plan: string
status: string
invoiceId: string
id: string;
date: string;
amount: number;
plan: string;
status: string;
invoiceId: string;
}
const props = defineProps<{
history: PaymentHistoryItem[]
}>()
defineProps<{
history: PaymentHistoryItem[];
}>();
const toast = useToast()
const getStatusSeverity = (status: string): 'success' | 'error' | 'warn' | 'info' | 'secondary' => {
switch (status) {
case 'success':
return 'success';
case 'failed':
return 'error';
case 'pending':
return 'warn';
default:
return 'info';
}
};
const getStatusSeverity = (status: string) => {
switch (status) {
case 'success':
return 'success' as const
case 'failed':
return 'danger' as const
case 'pending':
return 'warning' as const
default:
return 'info' as const
}
}
const columnHelper = createColumnHelper<PaymentHistoryItem>()
const columns = [
columnHelper.accessor('date', {
header: 'Date',
cell: ({ getValue }) => h('span', { class: 'font-medium' }, getValue()),
enableSorting: true
}),
columnHelper.accessor('amount', {
header: 'Amount',
cell: ({ getValue }) => h('span', {}, `$${getValue()}`)
}),
columnHelper.accessor('plan', {
header: 'Plan'
}),
columnHelper.accessor('status', {
header: 'Status',
cell: ({ getValue }) => h(Tag, {
value: getValue(),
severity: getStatusSeverity(getValue())
})
})
]
const toast = inject<{ add: (t: any) => void }>('toast');
const downloadInvoice = (item: PaymentHistoryItem) => {
toast.info(`Downloading invoice #${item.invoiceId}...`, 'Downloading')
toast?.add({
severity: 'info',
summary: 'Downloading',
detail: `Downloading invoice #${item.invoiceId}...`,
life: 2000
});
setTimeout(() => {
toast.success(`Invoice #${item.invoiceId} downloaded successfully`, 'Downloaded')
}, 1500)
}
setTimeout(() => {
toast?.add({
severity: 'success',
summary: 'Downloaded',
detail: `Invoice #${item.invoiceId} downloaded successfully`,
life: 3000
});
}, 1500);
};
</script>
<template>
<section>
<Toast />
<h2 class="text-2xl font-bold mb-6 text-gray-900">Billing History</h2>
<div class="bg-white border border-gray-200 rounded-xl overflow-hidden">
<DataTable :data="history" :columns="columns" />
</div>
</section>
<section>
<h2 class="text-2xl font-bold mb-6 text-gray-900">Billing History</h2>
<div class="bg-white border border-gray-200 rounded-xl overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-200 bg-gray-50">
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Amount</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Plan</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr v-for="item in history" :key="item.id">
<td class="px-4 py-3 text-sm font-medium text-gray-900">{{ item.date }}</td>
<td class="px-4 py-3 text-sm text-gray-900">${{ item.amount }}</td>
<td class="px-4 py-3 text-sm text-gray-500">{{ item.plan }}</td>
<td class="px-4 py-3">
<Tag :value="item.status" :severity="getStatusSeverity(item.status)" />
</td>
</tr>
<tr v-if="history.length === 0">
<td colspan="4" class="px-4 py-8 text-center text-gray-500">No payment history found.</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
</template>

View File

@@ -1,43 +1,39 @@
<script setup lang="ts">
import ProgressBar from '@/components/ui/ProgressBar.vue';
import { ProgressBar } from '@/components/ui/form';
import { formatBytes } from '@/lib/utils';
import { computed } from 'vue';
const props = defineProps<{
storageUsed: number
storageLimit: number
uploadsUsed: number
uploadsLimit: number
}>()
storageUsed: number;
storageLimit: number;
uploadsUsed: number;
uploadsLimit: number;
}>();
const storagePercentage = computed(() => Math.min(Math.round((props.storageUsed / props.storageLimit) * 100), 100))
const uploadsPercentage = computed(() => Math.min(Math.round((props.uploadsUsed / props.uploadsLimit) * 100), 100))
const storagePercentage = computed(() => Math.min(Math.round((props.storageUsed / props.storageLimit) * 100), 100));
const uploadsPercentage = computed(() => Math.min(Math.round((props.uploadsUsed / props.uploadsLimit) * 100), 100));
</script>
<template>
<div class="bg-white border border-gray-200 rounded-2xl p-8 flex flex-col justify-center">
<h3 class="text-lg font-bold text-gray-900 mb-6">Usage Statistics</h3>
<div class="bg-white border border-gray-200 rounded-2xl p-8 flex flex-col justify-center">
<h3 class="text-lg font-bold text-gray-900 mb-6">Usage Statistics</h3>
<div class="mb-6">
<div class="flex justify-between text-sm mb-2">
<span class="text-gray-600 font-medium">Storage</span>
<span class="text-gray-900 font-bold">{{ storagePercentage }}%</span>
</div>
<ProgressBar
:value="storagePercentage"
:show-value="false"
:color="storagePercentage > 90 ? 'danger' : 'primary'"
/>
<p class="text-xs text-gray-500 mt-2">{{ formatBytes(storageUsed) }} of {{ formatBytes(storageLimit) }} used</p>
</div>
<div class="mb-6">
<div class="flex justify-between text-sm mb-2">
<span class="text-gray-600 font-medium">Storage</span>
<span class="text-gray-900 font-bold">{{ storagePercentage }}%</span>
</div>
<ProgressBar :value="storagePercentage" />
<p class="text-xs text-gray-500 mt-2">{{ formatBytes(storageUsed) }} of {{ formatBytes(storageLimit) }} used</p>
</div>
<div>
<div class="flex justify-between text-sm mb-2">
<span class="text-gray-600 font-medium">Monthly Uploads</span>
<span class="text-gray-900 font-bold">{{ uploadsPercentage }}%</span>
</div>
<ProgressBar :value="uploadsPercentage" :show-value="false" />
<p class="text-xs text-gray-500 mt-2">{{ uploadsUsed }} of {{ uploadsLimit }} uploads</p>
<div>
<div class="flex justify-between text-sm mb-2">
<span class="text-gray-600 font-medium">Monthly Uploads</span>
<span class="text-gray-900 font-bold">{{ uploadsPercentage }}%</span>
</div>
<ProgressBar :value="uploadsPercentage" />
<p class="text-xs text-gray-500 mt-2">{{ uploadsUsed }} of {{ uploadsLimit }} uploads</p>
</div>
</div>
</div>
</template>

View File

@@ -1,93 +1,107 @@
<script setup lang="ts">
import PageHeader from '@/components/dashboard/PageHeader.vue'
import Toast from '@/components/ui/Toast.vue'
import { useToast } from '@/composables/useToast'
import { useAuthStore } from '@/stores/auth'
import { computed, ref } from 'vue'
import AccountStatusCard from './components/AccountStatusCard.vue'
import ChangePasswordDialog from './components/ChangePasswordDialog.vue'
import LinkedAccountsCard from './components/LinkedAccountsCard.vue'
import ProfileHero from './components/ProfileHero.vue'
import ProfileInfoCard from './components/ProfileInfoCard.vue'
import PageHeader from '@/components/dashboard/PageHeader.vue';
import { useAuthStore } from '@/stores/auth';
import { computed, inject, ref } from 'vue';
import AccountStatusCard from './components/AccountStatusCard.vue';
import ChangePasswordDialog from './components/ChangePasswordDialog.vue';
import LinkedAccountsCard from './components/LinkedAccountsCard.vue';
import ProfileHero from './components/ProfileHero.vue';
import ProfileInfoCard from './components/ProfileInfoCard.vue';
const auth = useAuthStore()
const toast = useToast()
const auth = useAuthStore();
const toast = inject<{ add: (t: any) => void }>('toast');
// Dialog visibility
const showPasswordDialog = ref(false)
const showPasswordDialog = ref(false);
// Refs for dialog components
const passwordDialogRef = ref<InstanceType<typeof ChangePasswordDialog>>()
const passwordDialogRef = ref<any>();
// Computed storage values
const storageUsed = computed(() => auth.user?.storage_used || 0)
const storageLimit = computed(() => 10737418240) // 10GB default
const storageUsed = computed(() => auth.user?.storage_used || 0);
const storageLimit = computed(() => 10737418240); // 10GB default
// Handlers
const handleEditSave = async (data: { username: string; email: string }) => {
try {
await auth.updateProfile(data)
toast.success('Your profile has been updated successfully.', 'Profile Updated')
} catch (e) {
toast.error(auth.error || 'Failed to update profile.', 'Update Failed')
}
}
try {
await auth.updateProfile(data);
toast?.add({
severity: 'success',
summary: 'Profile Updated',
detail: 'Your profile has been updated successfully.',
life: 3000
});
} catch (e) {
toast?.add({
severity: 'error',
summary: 'Update Failed',
detail: auth.error || 'Failed to update profile.',
life: 5000
});
}
};
const handlePasswordSave = async (data: { currentPassword: string; newPassword: string }) => {
try {
await auth.changePassword(data.currentPassword, data.newPassword)
showPasswordDialog.value = false
toast.success('Your password has been changed successfully.', 'Password Changed')
} catch (e: any) {
passwordDialogRef.value?.setError(e.message || 'Failed to change password')
}
}
try {
await auth.changePassword(data.currentPassword, data.newPassword);
showPasswordDialog.value = false;
toast?.add({
severity: 'success',
summary: 'Password Changed',
detail: 'Your password has been changed successfully.',
life: 3000
});
} catch (e: any) {
if (passwordDialogRef.value?.setError) {
passwordDialogRef.value.setError(e.message || 'Failed to change password');
}
}
};
</script>
<template>
<div class="profile-page">
<Toast />
<PageHeader
title="Profile Settings"
description="Manage your account information and preferences."
:breadcrumbs="[
{ label: 'Dashboard', to: '/' },
{ label: 'Profile' }
]"
/>
<div class="profile-page">
<PageHeader
title="Profile Settings"
description="Manage your account information and preferences."
:breadcrumbs="[
{ label: 'Dashboard', to: '/' },
{ label: 'Profile' }
]"
/>
<div class="max-w-5xl mx-auto space-y-8 pb-12">
<!-- Hero Identity Card -->
<ProfileHero
:user="auth.user"
@logout="auth.logout()"
/>
<div class="max-w-5xl mx-auto space-y-8 pb-12">
<!-- Hero Identity Card -->
<ProfileHero
:user="auth.user"
@logout="auth.logout()"
/>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- Personal Info -->
<div class="md:col-span-2">
<ProfileInfoCard
:user="auth.user"
@change-password="showPasswordDialog = true"
/>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- Personal Info -->
<div class="md:col-span-2">
<ProfileInfoCard
:user="auth.user"
@change-password="showPasswordDialog = true"
/>
</div>
<!-- Stats Side -->
<div class="md:col-span-1 space-y-6">
<AccountStatusCard
:storage-used="storageUsed"
:storage-limit="storageLimit"
/>
<LinkedAccountsCard />
</div>
</div>
</div>
<!-- Stats Side -->
<div class="md:col-span-1 space-y-6">
<AccountStatusCard
:storage-used="storageUsed"
:storage-limit="storageLimit"
/>
<LinkedAccountsCard />
</div>
</div>
<!-- Dialogs -->
<ChangePasswordDialog
ref="passwordDialogRef"
v-model:visible="showPasswordDialog"
@save="handlePasswordSave"
/>
</div>
<!-- Dialogs -->
<ChangePasswordDialog
ref="passwordDialogRef"
v-model:visible="showPasswordDialog"
@save="handlePasswordSave"
/>
</div>
</template>

View File

@@ -1,37 +1,47 @@
<script setup lang="ts">
import ProgressBar from '@/components/ui/ProgressBar.vue';
import { ProgressBar } from '@/components/ui/form';
import { computed } from 'vue';
interface Props {
storageUsed?: number
storageTotal?: number
}
const props = defineProps<{
storageUsed: number;
storageLimit: number;
}>();
const props = withDefaults(defineProps<Props>(), {
storageUsed: 0,
storageTotal: 100
})
const storagePercentage = computed(() =>
Math.min(Math.round((props.storageUsed / props.storageLimit) * 100), 100)
);
const usagePercentage = computed(() => {
if (props.storageTotal === 0) return 0
return Math.round((props.storageUsed / props.storageTotal) * 100)
})
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
</script>
<template>
<div class="bg-white rounded-xl p-6 border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Account Status</h3>
<div class="space-y-4">
<div>
<div class="flex justify-between text-sm mb-2">
<span class="text-gray-600">Storage Usage</span>
<span class="font-medium text-gray-900">{{ usagePercentage }}%</span>
<div class="bg-white border border-gray-200 rounded-2xl p-6">
<h3 class="text-lg font-bold text-gray-900 mb-4">Account Status</h3>
<div class="space-y-4">
<div>
<div class="flex justify-between text-sm mb-2">
<span class="text-gray-600">Storage Used</span>
<span class="font-bold text-gray-900">{{ storagePercentage }}%</span>
</div>
<ProgressBar :value="storagePercentage" />
<p class="text-xs text-gray-500 mt-2">{{ formatBytes(storageUsed) }} of {{ formatBytes(storageLimit) }} used</p>
</div>
<div class="bg-green-50 rounded-lg p-4 border border-green-100 flex items-start gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-green-600 mt-0.5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
<div>
<h4 class="font-bold text-green-800 text-sm">Account Active</h4>
<p class="text-green-600 text-xs mt-0.5">Your subscription is in good standing.</p>
</div>
</div>
</div>
<ProgressBar :value="usagePercentage" :show-value="false" />
<p class="text-xs text-gray-500 mt-2">
{{ storageUsed }} GB of {{ storageTotal }} GB used
</p>
</div>
</div>
</div>
</template>

View File

@@ -1,132 +1,116 @@
<script setup lang="ts">
import Button from '@/components/ui/Button.vue';
import Dialog from '@/components/ui/Dialog.vue';
import Input from '@/components/ui/Input.vue';
import { Button, Dialog, Input } from '@/components/ui/form';
import { computed, ref, watch } from 'vue';
const props = defineProps<{
visible: boolean
}>()
visible: boolean;
}>();
const emit = defineEmits<{
'update:visible': [value: boolean]
save: [data: { currentPassword: string; newPassword: string }]
}>()
'update:visible': [value: boolean];
save: [data: { currentPassword: string; newPassword: string }];
}>();
const currentPassword = ref('')
const newPassword = ref('')
const confirmPassword = ref('')
const loading = ref(false)
const error = ref('')
const currentPassword = ref('');
const newPassword = ref('');
const confirmPassword = ref('');
const loading = ref(false);
const error = ref('');
watch(() => props.visible, (val) => {
if (val) {
currentPassword.value = ''
newPassword.value = ''
confirmPassword.value = ''
error.value = ''
}
})
if (val) {
currentPassword.value = '';
newPassword.value = '';
confirmPassword.value = '';
error.value = '';
}
});
const isValid = computed(() => {
return currentPassword.value.length >= 1
&& newPassword.value.length >= 6
&& newPassword.value === confirmPassword.value
})
return currentPassword.value.length >= 1
&& newPassword.value.length >= 6
&& newPassword.value === confirmPassword.value;
});
const passwordMismatch = computed(() => {
return confirmPassword.value.length > 0 && newPassword.value !== confirmPassword.value
})
return confirmPassword.value.length > 0 && newPassword.value !== confirmPassword.value;
});
const passwordTooShort = computed(() => {
return newPassword.value.length > 0 && newPassword.value.length < 6
})
const newPasswordInvalidClass = computed(() => passwordTooShort.value ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : '')
const confirmPasswordInvalidClass = computed(() => passwordMismatch.value ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : '')
return newPassword.value.length > 0 && newPassword.value.length < 6;
});
const handleSave = () => {
if (!isValid.value) return
loading.value = true
error.value = ''
emit('save', {
currentPassword: currentPassword.value,
newPassword: newPassword.value
})
}
if (!isValid.value) return;
loading.value = true;
error.value = '';
emit('save', {
currentPassword: currentPassword.value,
newPassword: newPassword.value
});
};
const handleClose = () => {
emit('update:visible', false)
}
emit('update:visible', false);
};
// Expose methods for parent to control loading state
defineExpose({
setLoading: (val: boolean) => { loading.value = val },
setError: (msg: string) => { error.value = msg; loading.value = false }
})
setLoading: (val: boolean) => { loading.value = val; },
setError: (msg: string) => { error.value = msg; loading.value = false; }
});
</script>
<template>
<Dialog
:visible="visible"
header="Change Password"
width="28rem"
:closable="true"
:draggable="false"
@update:visible="handleClose"
>
<div class="space-y-6 pt-2">
<div
v-if="error"
class="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm"
>
{{ error }}
</div>
<Dialog
:visible="visible"
@update:visible="emit('update:visible', $event)"
header="Change Password"
:style="{ width: '28rem' }"
:closable="true"
>
<div class="space-y-6 pt-2">
<div v-if="error" class="p-3 bg-red-50 border border-red-200 rounded-lg text-red-800 text-sm">
{{ error }}
</div>
<div class="flex flex-col gap-2">
<label for="current-password" class="text-sm font-medium text-gray-700">Current Password</label>
<Input
id="current-password"
v-model="currentPassword"
type="password"
placeholder="Enter current password"
/>
</div>
<div class="flex flex-col gap-2">
<label for="current-password" class="text-sm font-medium text-gray-700">Current Password</label>
<Input
id="current-password"
v-model="currentPassword"
type="password"
placeholder="Enter current password"
/>
</div>
<div class="flex flex-col gap-2">
<label for="new-password" class="text-sm font-medium text-gray-700">New Password</label>
<Input
id="new-password"
v-model="newPassword"
type="password"
placeholder="Enter new password (min 6 characters)"
:class="newPasswordInvalidClass"
/>
<small v-if="passwordTooShort" class="text-red-500">Password must be at least 6 characters</small>
</div>
<div class="flex flex-col gap-2">
<label for="new-password" class="text-sm font-medium text-gray-700">New Password</label>
<Input
id="new-password"
v-model="newPassword"
type="password"
placeholder="Enter new password (min 6 characters)"
/>
<small v-if="passwordTooShort" class="text-red-500">Password must be at least 6 characters</small>
</div>
<div class="flex flex-col gap-2">
<label for="confirm-password" class="text-sm font-medium text-gray-700">Confirm New Password</label>
<Input
id="confirm-password"
v-model="confirmPassword"
type="password"
placeholder="Confirm new password"
:class="confirmPasswordInvalidClass"
/>
<small v-if="passwordMismatch" class="text-red-500">Passwords do not match</small>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-3 pt-4">
<Button variant="outline" :disabled="loading" @click="handleClose">
Cancel
</Button>
<Button :loading="loading" :disabled="!isValid" @click="handleSave">
Change Password
</Button>
</div>
</template>
</Dialog>
<div class="flex flex-col gap-2">
<label for="confirm-password" class="text-sm font-medium text-gray-700">Confirm New Password</label>
<Input
id="confirm-password"
v-model="confirmPassword"
type="password"
placeholder="Confirm new password"
/>
<small v-if="passwordMismatch" class="text-red-500">Passwords do not match</small>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-3 pt-4">
<Button variant="secondary" label="Cancel" @click="handleClose" :disabled="loading" />
<Button label="Change Password" @click="handleSave" :loading="loading" :disabled="!isValid" />
</div>
</template>
</Dialog>
</template>

View File

@@ -1,24 +1,25 @@
<script setup lang="ts">
import Tag from '@/components/ui/Tag.vue';
import { Tag } from '@/components/ui/form';
</script>
<template>
<div class="bg-white rounded-xl p-6 border border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Linked Accounts</h3>
<div class="space-y-4">
<!-- Placeholder for linked accounts -->
<div class="flex items-center justify-between py-2">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center">
<span class="i-heroicons-envelope text-gray-600" />
</div>
<div>
<p class="font-medium text-gray-900">Email</p>
<p class="text-sm text-gray-500">Connected</p>
</div>
<div class="bg-white border border-gray-200 rounded-2xl p-6">
<h3 class="text-lg font-bold text-gray-900 mb-4">Linked Accounts</h3>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 rounded-lg border border-gray-100 hover:border-gray-200 transition-colors">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-red-100 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-red-600" viewBox="0 0 24 24">
<path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="currentColor" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="currentColor" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
</div>
<span class="font-medium text-gray-700">Google</span>
</div>
<Tag value="Connected" severity="success" />
</div>
</div>
<Tag value="Active" severity="success" />
</div>
</div>
</div>
</template>

View File

@@ -1,44 +1,70 @@
<script setup lang="ts">
import type { ModelUser } from '@/api/client'
import Avatar from '@/components/ui/Avatar.vue'
import Button from '@/components/ui/Button.vue'
import Tag from '@/components/ui/Tag.vue'
import { computed } from 'vue'
import type { ModelUser } from '@/api/client';
import { Avatar, Button, Tag } from '@/components/ui/form';
import { computed } from 'vue';
interface Props {
user: ModelUser | null
}
const props = defineProps<{
user: ModelUser | null;
}>();
const props = defineProps<Props>()
const emit = defineEmits<{
logout: [];
changePassword: [];
}>();
const displayName = computed(() => {
return props.user?.username || 'User'
})
const displayEmail = computed(() => {
return props.user?.email || 'No email'
})
const joinDate = computed(() => {
return new Date(props.user?.created_at || Date.now()).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
});
</script>
<template>
<div class="bg-gradient-to-r from-blue-600 to-purple-600 rounded-xl p-6 text-white">
<div class="flex items-center gap-4">
<Avatar
:label="displayName"
size="xl"
shape="circle"
class="border-4 border-white/30"
/>
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<h2 class="text-2xl font-bold">{{ displayName }}</h2>
<Tag value="Active" severity="success" class="!bg-white/20 !text-white !border-white/30" />
<div class="relative overflow-hidden rounded-2xl bg-gradient-to-r from-gray-900 via-gray-800 to-gray-900 text-white p-8 md:p-10">
<!-- Background decorations -->
<div class="absolute top-0 right-0 -mt-20 -mr-20 w-80 h-80 bg-primary-500 rounded-full mix-blend-overlay filter blur-3xl"></div>
<div class="absolute bottom-0 left-0 -mb-20 -ml-20 w-80 h-80 bg-purple-500 rounded-full mix-blend-overlay filter blur-3xl"></div>
<div class="relative z-10 flex flex-col md:flex-row items-center gap-8">
<div class="relative">
<div class="absolute inset-0 bg-primary-500 rounded-full blur-lg opacity-40"></div>
<Avatar
size="large"
shape="circle"
label=""
/>
</div>
<div class="text-center md:text-left space-y-2 flex-grow">
<div class="flex flex-col md:flex-row items-center gap-3 justify-center md:justify-start">
<h2 class="text-3xl font-bold text-white">{{ user?.username || 'User' }}</h2>
<Tag :value="user?.role || 'User'" severity="info" />
</div>
<p class="text-gray-400 text-lg">{{ user?.email }}</p>
<p class="text-gray-500 text-sm flex items-center justify-center md:justify-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="18" height="18" x="3" y="4" rx="2" ry="2"/>
<line x1="16" x2="16" y1="2" y2="6"/>
<line x1="8" x2="8" y1="2" y2="6"/>
<line x1="3" x2="21" y1="10" y2="10"/>
</svg>
Member since {{ joinDate }}
</p>
</div>
<div class="flex gap-3">
<Button label="Logout" @click="emit('logout')">
<template #default>
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" x2="9" y1="12" y2="12"/>
</svg>
</template>
</Button>
</div>
</div>
<p class="text-white/80">{{ displayEmail }}</p>
</div>
<Button variant="ghost" class="!text-white hover:!bg-white/20">
Edit Profile
</Button>
</div>
</div>
</template>

View File

@@ -1,81 +1,69 @@
<script setup lang="ts">
import type { ModelUser } from '@/api/client'
import Button from '@/components/ui/Button.vue'
import Input from '@/components/ui/Input.vue'
import { ref, watch } from 'vue'
import type { ModelUser } from '@/api/client';
import { Button } from '@/components/ui/form';
interface Props {
user: ModelUser | null
}
defineProps<{
user: ModelUser | null;
}>();
const props = defineProps<Props>()
const emit = defineEmits<{
save: [data: { username: string; email: string }]
}>()
const isEditing = ref(false)
const username = ref('')
const email = ref('')
watch(() => props.user, (newUser) => {
if (newUser) {
username.value = newUser.username || ''
email.value = newUser.email || ''
}
}, { immediate: true })
const handleSave = () => {
emit('save', { username: username.value, email: email.value })
isEditing.value = false
}
const handleCancel = () => {
username.value = props.user?.username || ''
email.value = props.user?.email || ''
isEditing.value = false
}
edit: [];
changePassword: [];
}>();
</script>
<template>
<div class="bg-white rounded-xl p-6 border border-gray-200">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Profile Information</h3>
<Button
v-if="!isEditing"
size="sm"
variant="outline"
@click="isEditing = true"
>
Edit
</Button>
<div class="bg-white border border-gray-200 rounded-2xl p-8">
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-bold text-gray-900">Personal Information</h3>
<div class="flex gap-2">
<Button variant="text" @click="emit('changePassword')">
<template #default>
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
Change Password
</template>
</Button>
</div>
</div>
<div class="grid grid-cols-1 gap-6">
<div class="flex flex-col gap-2">
<label class="text-sm font-medium text-gray-700">Username</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
</div>
<input
type="text"
:value="user?.username"
readonly
class="w-full pl-10 px-3 py-2 text-sm border border-gray-300 rounded-lg bg-gray-50 outline-none"
/>
</div>
</div>
<div class="flex flex-col gap-2">
<label class="text-sm font-medium text-gray-700">Email Address</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="20" height="16" x="2" y="4" rx="2"/>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
</svg>
</div>
<input
type="text"
:value="user?.email"
readonly
class="w-full pl-10 px-3 py-2 text-sm border border-gray-300 rounded-lg bg-gray-50 outline-none"
/>
</div>
</div>
</div>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Username</label>
<Input
v-if="isEditing"
v-model="username"
placeholder="Enter username"
/>
<p v-else class="text-gray-900">{{ user?.username || 'Not set' }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
<Input
v-if="isEditing"
v-model="email"
type="email"
placeholder="Enter email"
/>
<p v-else class="text-gray-900">{{ user?.email || 'Not set' }}</p>
</div>
<div v-if="isEditing" class="flex gap-2 pt-2">
<Button size="sm" variant="outline" @click="handleCancel">Cancel</Button>
<Button size="sm" @click="handleSave">Save</Button>
</div>
</div>
</div>
</template>

View File

@@ -1,113 +1,77 @@
<script setup lang="ts">
import type { ModelVideo } from '@/api/client'
import Checkbox from '@/components/ui/Checkbox.vue'
import { formatDate, formatDuration, getStatusClass } from '@/lib/utils'
import type { ModelVideo } from '@/api/client';
import { formatDate, formatDuration, getStatusClass } from '@/lib/utils';
interface Props {
videos: ModelVideo[]
selectedVideos: ModelVideo[]
}
const props = defineProps<Props>()
defineProps<{
videos: ModelVideo[];
selectedVideos: ModelVideo[];
}>();
const emit = defineEmits<{
(e: 'update:selectedVideos', value: ModelVideo[]): void
(e: 'delete', videoId: string): void
}>()
const isSelected = (video: ModelVideo) => {
return props.selectedVideos.some(v => v.id === video.id)
}
const toggleSelection = (video: ModelVideo) => {
const newSelection = isSelected(video)
? props.selectedVideos.filter(v => v.id !== video.id)
: [...props.selectedVideos, video]
emit('update:selectedVideos', newSelection)
}
(e: 'update:selectedVideos', value: ModelVideo[]): void;
(e: 'delete', videoId: string): void;
}>();
</script>
<template>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
<div
v-for="video in videos"
:key="video.id"
class="bg-white rounded-xl border overflow-hidden shadow-sm hover:shadow-md transition-shadow group relative"
:class="isSelected(video) ? 'border-primary ring-2 ring-primary' : 'border-gray-200'"
>
<!-- Header/Thumbnail -->
<div
class="aspect-video bg-gray-200 relative overflow-hidden group-hover:opacity-95 transition-opacity"
>
<!-- Grid Selection Checkbox -->
<div
class="absolute top-2 left-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity"
:class="{ 'opacity-100': isSelected(video) }"
>
<Checkbox
:model-value="isSelected(video)"
:binary="true"
@click="toggleSelection(video)"
/>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
<div v-for="video in videos" :key="video.id"
class="bg-white rounded-xl shadow-sm overflow-hidden hover:shadow-md transition-shadow group relative border border-gray-200"
:class="{ 'border-primary ring-2 ring-primary': selectedVideos.some(v => v.id === video.id) }">
<div class="aspect-video bg-gray-200 relative overflow-hidden group-hover:opacity-95 transition-opacity">
<!-- Grid Selection Checkbox -->
<div class="absolute top-2 left-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity"
:class="{ 'opacity-100': selectedVideos.some(v => v.id === video.id) }">
<input
type="checkbox"
:checked="selectedVideos.some(v => v.id === video.id)"
@change="emit('update:selectedVideos', selectedVideos.some(v => v.id === video.id) ? selectedVideos.filter(v => v.id !== video.id) : [...selectedVideos, video])"
class="rounded border-gray-300"
/>
</div>
<img v-if="video.thumbnail" :src="video.thumbnail" :alt="video.title"
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" />
<div v-else class="w-full h-full flex items-center justify-center text-gray-400">
<span class="i-heroicons-film text-3xl" />
</div>
<div
class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center pointer-events-none">
</div>
<span
class="absolute bottom-1.5 right-1.5 bg-black/70 text-white text-[10px] font-medium px-1.5 py-0.5 rounded">
{{ formatDuration(video.duration) }}
</span>
</div>
<div class="p-4 flex flex-col h-full">
<div class="flex items-start justify-between gap-2 mb-1">
<h3 class="font-medium text-sm text-gray-900 line-clamp-2 leading-snug flex-1"
:title="video.title">
{{ video.title }}
</h3>
<button class="text-gray-400 hover:text-gray-700">
<span class="i-heroicons-ellipsis-vertical w-4 h-4" />
</button>
</div>
<p class="text-xs text-gray-500 mb-3 line-clamp-1 h-4">{{ video.description || 'No description' }}
</p>
<div class="mt-auto flex items-center justify-between">
<span
:class="['px-1.5 py-0.5 text-[10px] font-medium rounded-full uppercase tracking-wider', getStatusClass(video.status)]">
{{ video.status }}
</span>
<div class="text-[10px] text-gray-400">
{{ formatDate(video.created_at) }}
</div>
</div>
</div>
</div>
<img
v-if="video.thumbnail"
:src="video.thumbnail"
:alt="video.title"
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div
v-else
class="w-full h-full flex items-center justify-center text-gray-400"
>
<span class="i-heroicons-film text-3xl" />
</div>
<div
class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center pointer-events-none"
/>
<span
class="absolute bottom-1.5 right-1.5 bg-black/70 text-white text-[10px] font-medium px-1.5 py-0.5 rounded"
>
{{ formatDuration(video.duration) }}
</span>
</div>
<!-- Content -->
<div class="flex flex-col h-full p-4">
<div class="flex items-start justify-between gap-2 mb-1">
<h3
class="font-medium text-sm text-gray-900 line-clamp-2 leading-snug flex-1"
:title="video.title"
>
{{ video.title }}
</h3>
<button class="text-gray-400 hover:text-gray-700">
<span class="i-heroicons-ellipsis-vertical w-4 h-4" />
</button>
</div>
<p class="text-xs text-gray-500 mb-3 line-clamp-1 h-4">
{{ video.description || 'No description' }}
</p>
<div class="mt-auto flex items-center justify-between">
<span
:class="[
'px-1.5 py-0.5 text-[10px] font-medium rounded-full uppercase tracking-wider',
getStatusClass(video.status)
]"
>
{{ video.status }}
</span>
<div class="text-[10px] text-gray-400">
{{ formatDate(video.created_at) }}
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -1,142 +1,97 @@
<script setup lang="ts">
import type { ModelVideo } from '@/api/client'
import { createColumnHelper } from '@/components/table/Column'
import DataTable from '@/components/table/DataTable.vue'
import Checkbox from '@/components/ui/Checkbox.vue'
import { formatBytes, formatDate, formatDuration, getStatusClass } from '@/lib/utils'
import { h } from 'vue'
import type { ModelVideo } from '@/api/client';
import { formatBytes, formatDate, formatDuration, getStatusClass } from '@/lib/utils';
interface Props {
videos: ModelVideo[]
selectedVideos: ModelVideo[]
}
const props = defineProps<Props>()
defineProps<{
videos: ModelVideo[];
selectedVideos: ModelVideo[];
}>();
const emit = defineEmits<{
(e: 'update:selectedVideos', value: ModelVideo[]): void
(e: 'delete', videoId: string): void
}>()
const columnHelper = createColumnHelper<ModelVideo>()
const isSelected = (video: ModelVideo) => {
return props.selectedVideos.some(v => v.id === video.id)
}
const toggleAll = () => {
const allSelected = props.videos.length > 0 && props.videos.every(v => isSelected(v))
const newSelection = allSelected ? [] : [...props.videos]
emit('update:selectedVideos', newSelection)
}
const allSelected = () => props.videos.length > 0 && props.videos.every(v => isSelected(v))
const someSelected = () => props.videos.some(v => isSelected(v)) && !allSelected()
const columns = [
columnHelper.display({
id: 'select',
header: () => h('div', {
class: 'flex justify-center'
}, h(Checkbox, {
modelValue: allSelected(),
binary: true,
onClick: toggleAll
})),
cell: ({ row }) => h('div', {
class: 'flex justify-center'
}, h(Checkbox, {
modelValue: isSelected(row.original),
binary: true,
onClick: () => {
const newSelection = isSelected(row.original)
? props.selectedVideos.filter(v => v.id !== row.original.id)
: [...props.selectedVideos, row.original]
emit('update:selectedVideos', newSelection)
}
})),
size: 50
}),
columnHelper.accessor('title', {
header: 'Video',
cell: ({ row }) => h('div', { class: 'flex items-center gap-3' }, [
h('div', { class: 'w-20 h-12 bg-gray-200 rounded overflow-hidden flex-shrink-0' }, [
row.original.thumbnail
? h('img', {
src: row.original.thumbnail,
alt: row.original.title,
class: 'w-full h-full object-cover'
})
: h('div', { class: 'w-full h-full flex items-center justify-center' }, [
h('span', { class: 'i-heroicons-film text-gray-400 text-xl' })
])
]),
h('div', { class: 'min-w-0 flex-1' }, [
h('p', { class: 'font-medium text-gray-900 truncate' }, row.original.title),
h('p', { class: 'text-sm text-gray-500 truncate' }, row.original.description || 'No description')
])
]),
enableSorting: true
}),
columnHelper.accessor('status', {
header: 'Status',
cell: ({ getValue }) => {
const status = getValue() || 'Unknown'
return h('span', {
class: `px-2 py-1 text-xs font-medium rounded-full whitespace-nowrap ${getStatusClass(status)}`
}, status)
},
enableSorting: true
}),
columnHelper.accessor('duration', {
header: 'Duration',
cell: ({ getValue }) => h('span', { class: 'text-sm text-gray-500' }, formatDuration(getValue())),
enableSorting: true
}),
columnHelper.accessor('size', {
header: 'Size',
cell: ({ getValue }) => h('span', { class: 'text-sm text-gray-500' }, formatBytes(getValue())),
enableSorting: true
}),
columnHelper.accessor('created_at', {
header: 'Upload Date',
cell: ({ getValue }) => h('span', { class: 'text-sm text-gray-500' }, formatDate(getValue())),
enableSorting: true
}),
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: ({ row }) => h('div', { class: 'flex items-center gap-1' }, [
h('button', {
class: 'p-1.5 text-gray-400 hover:text-primary hover:bg-primary/5 rounded transition-colors',
title: 'Download'
}, h('span', { class: 'i-heroicons-arrow-down-tray w-4 h-4' })),
h('button', {
class: 'p-1.5 text-gray-400 hover:text-primary hover:bg-primary/5 rounded transition-colors',
title: 'Copy Link'
}, h('span', { class: 'i-heroicons-link w-4 h-4' })),
h('div', { class: 'w-px h-3 bg-gray-200 mx-1' }),
h('button', {
class: 'p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors',
title: 'Edit'
}, h('span', { class: 'i-heroicons-pencil w-4 h-4' })),
h('button', {
class: 'p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors',
title: 'Delete',
onClick: () => row.original.id && emit('delete', row.original.id)
}, h('span', { class: 'i-heroicons-trash w-4 h-4' }))
]),
size: 150
})
]
(e: 'update:selectedVideos', value: ModelVideo[]): void;
(e: 'delete', videoId: string): void;
}>();
</script>
<template>
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden">
<DataTable
:data="videos"
:columns="columns"
:enable-sorting="true"
/>
</div>
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full min-w-[50rem]">
<thead>
<tr class="border-b border-gray-200 bg-gray-50">
<th class="w-12 px-4 py-3 text-left">
<input type="checkbox" class="rounded border-gray-300" />
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Video</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Duration</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Size</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Upload Date</th>
<th class="w-32 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr v-for="video in videos" :key="video.id" class="hover:bg-gray-50">
<td class="px-4 py-3">
<input
type="checkbox"
:checked="selectedVideos.some(v => v.id === video.id)"
@change="emit('update:selectedVideos', selectedVideos.some(v => v.id === video.id) ? selectedVideos.filter(v => v.id !== video.id) : [...selectedVideos, video])"
class="rounded border-gray-300"
/>
</td>
<td class="px-4 py-3">
<div class="flex items-center gap-3">
<div class="w-20 h-12 bg-gray-200 rounded overflow-hidden flex-shrink-0">
<img v-if="video.thumbnail" :src="video.thumbnail" :alt="video.title"
class="w-full h-full object-cover" />
<div v-else class="w-full h-full flex items-center justify-center">
<span class="i-heroicons-film text-gray-400 text-xl" />
</div>
</div>
<div class="min-w-0 flex-1">
<p class="font-medium text-gray-900 truncate">{{ video.title }}</p>
<p class="text-sm text-gray-500 truncate">{{ video.description || 'No description' }}</p>
</div>
</div>
</td>
<td class="px-4 py-3">
<span
:class="['px-2 py-1 text-xs font-medium rounded-full whitespace-nowrap', getStatusClass(video.status)]">
{{ video.status || 'Unknown' }}
</span>
</td>
<td class="px-4 py-3 text-sm text-gray-500">{{ formatDuration(video.duration) }}</td>
<td class="px-4 py-3 text-sm text-gray-500">{{ formatBytes(video.size) }}</td>
<td class="px-4 py-3 text-sm text-gray-500">{{ formatDate(video.created_at) }}</td>
<td class="px-4 py-3">
<div class="flex items-center gap-1">
<button
class="p-1.5 text-gray-400 hover:text-primary hover:bg-primary/5 rounded transition-colors"
title="Download">
<span class="i-heroicons-arrow-down-tray w-4 h-4" />
</button>
<button
class="p-1.5 text-gray-400 hover:text-primary hover:bg-primary/5 rounded transition-colors"
title="Copy Link">
<span class="i-heroicons-link w-4 h-4" />
</button>
<div class="w-px h-3 bg-gray-200 mx-1"></div>
<button
class="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
title="Edit">
<span class="i-heroicons-pencil w-4 h-4" />
</button>
<button @click="emit('delete', video.id!)"
class="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
title="Delete">
<span class="i-heroicons-trash w-4 h-4" />
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>

View File

@@ -178,10 +178,6 @@ export default defineConfig({
DEFAULT: "#fafafa",
light: "#f8f9fa",
},
muted: {
DEFAULT: "#f5f4f2",
light: "#f8f9fa",
},
border: {
DEFAULT: "#e6e7e2",
light: "#f8f9fa",

View File

@@ -3,39 +3,28 @@ import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import path from "node:path";
import unocss from "unocss/vite";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { defineConfig } from "vite";
import ssrPlugin from "./ssrPlugin";
export default defineConfig((env) => {
// console.log("env:", env, import.meta.env);
return {
plugins: [
unocss(),
vue(),
vueJsx(),
AutoImport({
imports: ["vue", "vue-router", "pinia"],
dts: true,
}),
Components({
dirs: ["src/components"],
extensions: ["vue", "tsx"],
dts: true,
dtsTsx: true,
directives: false,
}),
ssrPlugin(),
cloudflare(),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
// "httpClientAdapter": path.resolve(__dirname, "./src/api/httpClientAdapter.server.ts")
},
},
optimizeDeps: {
exclude: ["vue"],
},
ssr: {
noExternal: ["vue"],
},