feat: cards

This commit is contained in:
trev 2025-03-17 06:47:49 -04:00
parent 4adedd96ee
commit 71c69b7b80
18 changed files with 966 additions and 776 deletions

596
client/package-lock.json generated
View File

@ -734,9 +734,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.0.tgz",
"integrity": "sha512-RoV8Xs9eNwiDvhv7M+xcL4PWyRyIXRY/FLp3buU4h1EYfdF7unWUy3dOjPqb3C7rMUewIcqwW850PgS8h1o1yg==",
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz",
"integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -855,6 +855,23 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
@ -868,6 +885,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/@eslint/js": {
"version": "9.22.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz",
@ -1666,28 +1690,28 @@
]
},
"node_modules/@scalar/api-client": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@scalar/api-client/-/api-client-2.3.1.tgz",
"integrity": "sha512-fRfWWFCTnXpCVVOQLeqf+NJmeWqkUIOn4u4wwRGYcmDNYZcWgfJtROHjISBEnwjCs18eMZsS0xB9j2fIiw4bAw==",
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@scalar/api-client/-/api-client-2.3.5.tgz",
"integrity": "sha512-bVBP8H3laa4VG3dExv4Ak/Kf+q9z8aK/Glpv6CPE0wxgZJWxwkJoSrvwxUE+46JktSObUrhu4xQiqSCmBz8esQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@headlessui/tailwindcss": "^0.2.0",
"@headlessui/vue": "^1.7.20",
"@scalar/components": "0.13.35",
"@scalar/components": "0.13.37",
"@scalar/draggable": "0.1.11",
"@scalar/icons": "0.1.3",
"@scalar/import": "0.3.0",
"@scalar/oas-utils": "0.2.118",
"@scalar/import": "0.3.2",
"@scalar/oas-utils": "0.2.120",
"@scalar/object-utils": "1.1.13",
"@scalar/openapi-parser": "0.10.10",
"@scalar/openapi-types": "0.1.9",
"@scalar/postman-to-openapi": "0.1.41",
"@scalar/postman-to-openapi": "0.1.43",
"@scalar/snippetz": "0.2.16",
"@scalar/themes": "0.9.77",
"@scalar/types": "0.1.0",
"@scalar/use-codemirror": "0.11.80",
"@scalar/use-hooks": "0.1.31",
"@scalar/themes": "0.9.79",
"@scalar/types": "0.1.1",
"@scalar/use-codemirror": "0.11.82",
"@scalar/use-hooks": "0.1.33",
"@scalar/use-toasts": "0.7.9",
"@scalar/use-tooltip": "1.0.6",
"@vueuse/core": "^10.10.0",
@ -1709,44 +1733,25 @@
"node": ">=18"
}
},
"node_modules/@scalar/api-client/node_modules/nanoid": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.3.tgz",
"integrity": "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/@scalar/api-reference": {
"version": "1.28.1",
"resolved": "https://registry.npmjs.org/@scalar/api-reference/-/api-reference-1.28.1.tgz",
"integrity": "sha512-axhOMIDcDkD7QlFoicfDfILOkEKSPS/P6oiTqnErkTvKifFFhMxzUY/gtjkkbpMmnEi9bnlbM8kPFeRgXiSISA==",
"version": "1.28.5",
"resolved": "https://registry.npmjs.org/@scalar/api-reference/-/api-reference-1.28.5.tgz",
"integrity": "sha512-GNBQae0OCk39KnOmJNtciCc0gKY0BtWYZt1wkqogetm8d2D4JypzxpR4+TqKJeBoASsKF45XogXuKlDbywOGZA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@floating-ui/vue": "^1.0.2",
"@headlessui/vue": "^1.7.20",
"@scalar/api-client": "2.3.1",
"@scalar/code-highlight": "0.0.24",
"@scalar/components": "0.13.35",
"@scalar/oas-utils": "0.2.118",
"@scalar/api-client": "2.3.5",
"@scalar/code-highlight": "0.0.25",
"@scalar/components": "0.13.37",
"@scalar/oas-utils": "0.2.120",
"@scalar/openapi-parser": "0.10.10",
"@scalar/openapi-types": "0.1.9",
"@scalar/snippetz": "0.2.16",
"@scalar/themes": "0.9.77",
"@scalar/types": "0.1.0",
"@scalar/use-hooks": "0.1.31",
"@scalar/themes": "0.9.79",
"@scalar/types": "0.1.1",
"@scalar/use-hooks": "0.1.33",
"@scalar/use-toasts": "0.7.9",
"@unhead/vue": "^1.11.11",
"@vueuse/core": "^10.10.0",
@ -1760,29 +1765,10 @@
"node": ">=18"
}
},
"node_modules/@scalar/api-reference/node_modules/nanoid": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.3.tgz",
"integrity": "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/@scalar/code-highlight": {
"version": "0.0.24",
"resolved": "https://registry.npmjs.org/@scalar/code-highlight/-/code-highlight-0.0.24.tgz",
"integrity": "sha512-xQFt5yVbNr3+7dmMsQrV/yc0Zi9rMV9E9rSvzroD0TQvwi39rAiMmdQzwLXT/S9B40foIJiP/3y6FmxDEf3DoA==",
"version": "0.0.25",
"resolved": "https://registry.npmjs.org/@scalar/code-highlight/-/code-highlight-0.0.25.tgz",
"integrity": "sha512-rmiXaAoL3Zl+OycIO1CMj8apaeAU/p41EmCpHTxInZiFVW0++iClce2fun1lK6qjTMZneR6UwE4qBKiUUVLCpg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1809,18 +1795,18 @@
}
},
"node_modules/@scalar/components": {
"version": "0.13.35",
"resolved": "https://registry.npmjs.org/@scalar/components/-/components-0.13.35.tgz",
"integrity": "sha512-R6qTijAyk/PAmWi4knomn13VQr85Jyevr+L5HkRMsPe/VC+YItcvD2JJhMK1vAZ/v9dnfByBbCq5ZcPIXa3n5A==",
"version": "0.13.37",
"resolved": "https://registry.npmjs.org/@scalar/components/-/components-0.13.37.tgz",
"integrity": "sha512-bhJxg0I63nUH0qoZgb8nyHKCSzL8L9widP2WIYymIvXpCFLwCvF64Z0CAbihwgXxq0YblPvNM+g5N3dRtmXqdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.2",
"@floating-ui/vue": "^1.0.2",
"@headlessui/vue": "^1.7.20",
"@scalar/code-highlight": "0.0.24",
"@scalar/themes": "0.9.77",
"@scalar/use-hooks": "0.1.31",
"@scalar/code-highlight": "0.0.25",
"@scalar/themes": "0.9.79",
"@scalar/use-hooks": "0.1.33",
"@scalar/use-toasts": "0.7.9",
"@vueuse/core": "^10.10.0",
"cva": "1.0.0-beta.2",
@ -1834,25 +1820,6 @@
"node": ">=18"
}
},
"node_modules/@scalar/components/node_modules/nanoid": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.3.tgz",
"integrity": "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/@scalar/components/node_modules/tailwind-merge": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
@ -1891,13 +1858,13 @@
}
},
"node_modules/@scalar/import": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@scalar/import/-/import-0.3.0.tgz",
"integrity": "sha512-5MYEaomfIPwURC6j5X2ZY9ei9JEvRfU9xE99uWK4ecisLACWC6UwxNvv+eJmx0R3bJvkN6driYmhjSSCARx55w==",
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@scalar/import/-/import-0.3.2.tgz",
"integrity": "sha512-de7IDZgEYOhhgaq8lFFbfiJ4Hx/ITIXvuLA8SfSl5UQKg+LxiOnnn/5PSKF+pnjWmUf7Kz/ds0eHOXpZv5iMkw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@scalar/oas-utils": "0.2.118",
"@scalar/oas-utils": "0.2.120",
"@scalar/openapi-parser": "0.10.10",
"yaml": "^2.4.5"
},
@ -1906,17 +1873,17 @@
}
},
"node_modules/@scalar/oas-utils": {
"version": "0.2.118",
"resolved": "https://registry.npmjs.org/@scalar/oas-utils/-/oas-utils-0.2.118.tgz",
"integrity": "sha512-8e8cgsJQT3p4/2iSj3os/eYckjeeLpW2CgMopxiNiT//utnLRgc9uZPcQJY8f4c791Ro3/9jYtYNWfQ+ENoNXA==",
"version": "0.2.120",
"resolved": "https://registry.npmjs.org/@scalar/oas-utils/-/oas-utils-0.2.120.tgz",
"integrity": "sha512-npu0uLClqqXVZfxMdKBWxkWCmONK0jKaUcfmVhGza9Jij5aJyvdfDw6vH/Hh+DghgECwAvQLQeIBZTxjr9ufzg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@hyperjump/json-schema": "^1.9.6",
"@scalar/object-utils": "1.1.13",
"@scalar/openapi-types": "0.1.9",
"@scalar/themes": "0.9.77",
"@scalar/types": "0.1.0",
"@scalar/themes": "0.9.79",
"@scalar/types": "0.1.1",
"flatted": "^3.3.1",
"microdiff": "^1.4.0",
"nanoid": "^5.0.9",
@ -1927,25 +1894,6 @@
"node": ">=18"
}
},
"node_modules/@scalar/oas-utils/node_modules/nanoid": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.3.tgz",
"integrity": "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/@scalar/object-utils": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/@scalar/object-utils/-/object-utils-1.1.13.tgz",
@ -1979,45 +1927,6 @@
"node": ">=18"
}
},
"node_modules/@scalar/openapi-parser/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@scalar/openapi-parser/node_modules/ajv-draft-04": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
"integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^8.5.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/@scalar/openapi-parser/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/@scalar/openapi-types": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.1.9.tgz",
@ -2029,13 +1938,13 @@
}
},
"node_modules/@scalar/postman-to-openapi": {
"version": "0.1.41",
"resolved": "https://registry.npmjs.org/@scalar/postman-to-openapi/-/postman-to-openapi-0.1.41.tgz",
"integrity": "sha512-RkDTWARU628BronjZFnW4dAy4OUNXzY1yjD52pYfFRLPHNwk3tgd2l1QDujp7L/IREqmG06LGigePmxz5v7aVw==",
"version": "0.1.43",
"resolved": "https://registry.npmjs.org/@scalar/postman-to-openapi/-/postman-to-openapi-0.1.43.tgz",
"integrity": "sha512-gLOkYYPCTKYFBOwyBOKZDc0seZjntmwPTchJUr3oxGQmLB1Y5VBJ+8fXJCTp5TwKiiztjALJs79y9s7jXBdWMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@scalar/oas-utils": "0.2.118",
"@scalar/oas-utils": "0.2.120",
"@scalar/openapi-types": "0.1.9"
},
"engines": {
@ -2056,22 +1965,22 @@
}
},
"node_modules/@scalar/themes": {
"version": "0.9.77",
"resolved": "https://registry.npmjs.org/@scalar/themes/-/themes-0.9.77.tgz",
"integrity": "sha512-vpvJu9pF+wl/XRD+xulgasmvoItj9ICSrgq2VyYvPoUUJ1CT0Ey4JrI1g1Zl+OSiDAB/h8K9VcINDUmP+y0+uA==",
"version": "0.9.79",
"resolved": "https://registry.npmjs.org/@scalar/themes/-/themes-0.9.79.tgz",
"integrity": "sha512-zWiHCZAIjPGa8X9o/NORBPRMTMblLEz2+2RcfW9yIKNO/8H4Gz0rltiGGlJ6vX0o+qHwx7AdgfY+7njmWQR4ng==",
"dev": true,
"license": "MIT",
"dependencies": {
"@scalar/types": "0.1.0"
"@scalar/types": "0.1.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@scalar/types": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.1.0.tgz",
"integrity": "sha512-6yYwc7+PSY3bAWkQVCYxp7qPOgqaWDHmp2ceZhXaXGYbwGK7jLqwG+YAMZfl9qqo0NzoBoClUVkb8aTER1LX2A==",
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.1.1.tgz",
"integrity": "sha512-LlUX6AmOOGoRqOMoO835V2FezM1KiO5UlvQC3poT/s7oqD6ranqwRNFxyrPz/IxClPYR+SV1yBUSNKely4ZQhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2084,9 +1993,9 @@
}
},
"node_modules/@scalar/use-codemirror": {
"version": "0.11.80",
"resolved": "https://registry.npmjs.org/@scalar/use-codemirror/-/use-codemirror-0.11.80.tgz",
"integrity": "sha512-MMWmSMndIjOlWSLfX048JVyDl1GbSv1Qt+R3dwq92v87JljTab0b5bzNSxhnRdlFU1HRSdPoGwILNUHYpHGndQ==",
"version": "0.11.82",
"resolved": "https://registry.npmjs.org/@scalar/use-codemirror/-/use-codemirror-0.11.82.tgz",
"integrity": "sha512-zFECln7aWKRf6iJO9oovByD59EsrOMenNLfLhneH6L+K1CrBoHFVr4czSDlom1wlr3HPg3xwpZrukoAteHYILQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2105,7 +2014,7 @@
"@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.2",
"@replit/codemirror-css-color-picker": "^6.3.0",
"@scalar/components": "0.13.35",
"@scalar/components": "0.13.37",
"codemirror": "^6.0.0",
"style-mod": "^4.1.2",
"vue": "^3.5.12"
@ -2115,13 +2024,13 @@
}
},
"node_modules/@scalar/use-hooks": {
"version": "0.1.31",
"resolved": "https://registry.npmjs.org/@scalar/use-hooks/-/use-hooks-0.1.31.tgz",
"integrity": "sha512-R9ofnTHKSo0C/RGIUrpZnNOe3x3B4W1PGcBgc5TnWBp9HvqLo+/X0Zpu7B/k3dpxGEAoj2dyphC99pyR7AV7pQ==",
"version": "0.1.33",
"resolved": "https://registry.npmjs.org/@scalar/use-hooks/-/use-hooks-0.1.33.tgz",
"integrity": "sha512-ENm0bWwRdAWWF/S6TbE+fFx0vP2mgEpG5APqQBomm0a41/6L2HJ/TN+9ajAvrJXGi0ULWuxihNS4Jue6tpEssA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@scalar/themes": "0.9.77",
"@scalar/themes": "0.9.79",
"@scalar/use-toasts": "0.7.9",
"@vueuse/core": "^10.10.0",
"vue": "^3.5.12",
@ -2146,25 +2055,6 @@
"node": ">=18"
}
},
"node_modules/@scalar/use-toasts/node_modules/nanoid": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.3.tgz",
"integrity": "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/@scalar/use-tooltip": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@scalar/use-tooltip/-/use-tooltip-1.0.6.tgz",
@ -2200,9 +2090,9 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.19.0.tgz",
"integrity": "sha512-UTx28Ad4sYsLU//gqkEo5aFOPFBRT2uXCmXTsURqhurDCvzkVwXruJgBcHDaMiK6RKKpYRteDUaXYqZyGPgCXQ==",
"version": "2.19.2",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.19.2.tgz",
"integrity": "sha512-OkW7MMGkjXtdfqdHWlyPozh/Ct1X3pthXAKTSqHm+mwmvmTBASmPE6FhwlvUgsqlCceRYL+5QUGiIJfOy0xIjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2281,44 +2171,44 @@
}
},
"node_modules/@tailwindcss/node": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.13.tgz",
"integrity": "sha512-P9TmtE9Vew0vv5FwyD4bsg/dHHsIsAuUXkenuGUc5gm8fYgaxpdoxIKngCyEMEQxyCKR8PQY5V5VrrKNOx7exg==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.14.tgz",
"integrity": "sha512-Ux9NbFkKWYE4rfUFz6M5JFLs/GEYP6ysxT8uSyPn6aTbh2K3xDE1zz++eVK4Vwx799fzMF8CID9sdHn4j/Ab8w==",
"dev": true,
"license": "MIT",
"dependencies": {
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"tailwindcss": "4.0.13"
"tailwindcss": "4.0.14"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.13.tgz",
"integrity": "sha512-pTH3Ex5zAWC9LbS+WsYAFmkXQW3NRjmvxkKJY3NP1x0KHBWjz0Q2uGtdGMJzsa0EwoZ7wq9RTbMH1UNPceCpWw==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.14.tgz",
"integrity": "sha512-M8VCNyO/NBi5vJ2cRcI9u8w7Si+i76a7o1vveoGtbbjpEYJZYiyc7f2VGps/DqawO56l3tImIbq2OT/533jcrA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.0.13",
"@tailwindcss/oxide-darwin-arm64": "4.0.13",
"@tailwindcss/oxide-darwin-x64": "4.0.13",
"@tailwindcss/oxide-freebsd-x64": "4.0.13",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.13",
"@tailwindcss/oxide-linux-arm64-gnu": "4.0.13",
"@tailwindcss/oxide-linux-arm64-musl": "4.0.13",
"@tailwindcss/oxide-linux-x64-gnu": "4.0.13",
"@tailwindcss/oxide-linux-x64-musl": "4.0.13",
"@tailwindcss/oxide-win32-arm64-msvc": "4.0.13",
"@tailwindcss/oxide-win32-x64-msvc": "4.0.13"
"@tailwindcss/oxide-android-arm64": "4.0.14",
"@tailwindcss/oxide-darwin-arm64": "4.0.14",
"@tailwindcss/oxide-darwin-x64": "4.0.14",
"@tailwindcss/oxide-freebsd-x64": "4.0.14",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.14",
"@tailwindcss/oxide-linux-arm64-gnu": "4.0.14",
"@tailwindcss/oxide-linux-arm64-musl": "4.0.14",
"@tailwindcss/oxide-linux-x64-gnu": "4.0.14",
"@tailwindcss/oxide-linux-x64-musl": "4.0.14",
"@tailwindcss/oxide-win32-arm64-msvc": "4.0.14",
"@tailwindcss/oxide-win32-x64-msvc": "4.0.14"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.13.tgz",
"integrity": "sha512-+9zmwaPQ8A9ycDcdb+hRkMn6NzsmZ4YJBsW5Xqq5EdOu9xlIgmuMuJauVzDPB5BSbIWfhPdZ+le8NeRZpl1coA==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.14.tgz",
"integrity": "sha512-VBFKC2rFyfJ5J8lRwjy6ub3rgpY186kAcYgiUr8ArR8BAZzMruyeKJ6mlsD22Zp5ZLcPW/FXMasJiJBx0WsdQg==",
"cpu": [
"arm64"
],
@ -2333,9 +2223,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.13.tgz",
"integrity": "sha512-Bj1QGlEJSjs/205CIRfb5/jeveOqzJ4pFMdRxu0gyiYWxBRyxsExXqaD+7162wnLP/EDKh6S1MC9E/1GwEhLtA==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.14.tgz",
"integrity": "sha512-U3XOwLrefGr2YQZ9DXasDSNWGPZBCh8F62+AExBEDMLDfvLLgI/HDzY8Oq8p/JtqkAY38sWPOaNnRwEGKU5Zmg==",
"cpu": [
"arm64"
],
@ -2350,9 +2240,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.13.tgz",
"integrity": "sha512-lRTkxjTpMGXhLLM5GjZ0MtjPczMuhAo9j7PeSsaU6Imkm7W7RbrXfT8aP934kS7cBBV+HKN5U19Z0WWaORfb8Q==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.14.tgz",
"integrity": "sha512-V5AjFuc3ndWGnOi1d379UsODb0TzAS2DYIP/lwEbfvafUaD2aNZIcbwJtYu2DQqO2+s/XBvDVA+w4yUyaewRwg==",
"cpu": [
"x64"
],
@ -2367,9 +2257,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.13.tgz",
"integrity": "sha512-p/YLyKhs+xFibVeAPlpMGDVMKgjChgzs12VnDFaaqRSJoOz+uJgRSKiir2tn50e7Nm4YYw35q/DRBwpDBNo1MQ==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.14.tgz",
"integrity": "sha512-tXvtxbaZfcPfqBwW3f53lTcyH6EDT+1eT7yabwcfcxTs+8yTPqxsDUhrqe9MrnEzpNkd+R/QAjJapfd4tjWdLg==",
"cpu": [
"x64"
],
@ -2384,9 +2274,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.13.tgz",
"integrity": "sha512-Ua/5ydE/QOTX8jHuc7M9ICWnaLi6K2MV/r+Ws2OppsOjy8tdlPbqYainJJ6Kl7ofm524K+4Fk9CQITPzeIESPw==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.14.tgz",
"integrity": "sha512-cSeLNWWqIWeSTmBntQvyY2/2gcLX8rkPFfDDTQVF8qbRcRMVPLxBvFVJyfSAYRNch6ZyVH2GI6dtgALOBDpdNA==",
"cpu": [
"arm"
],
@ -2401,9 +2291,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.13.tgz",
"integrity": "sha512-/W1+Q6tBAVgZWh/bhfOHo4n7Ryh6E7zYj4bJd9SRbkPyLtRioyK3bi6RLuDj57sa7Amk/DeomSV9iycS0xqIPA==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.14.tgz",
"integrity": "sha512-bwDWLBalXFMDItcSXzFk6y7QKvj6oFlaY9vM+agTlwFL1n1OhDHYLZkSjaYsh6KCeG0VB0r7H8PUJVOM1LRZyg==",
"cpu": [
"arm64"
],
@ -2418,9 +2308,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.13.tgz",
"integrity": "sha512-GQj6TWevNxwsYw20FdT2r2d1f7uiRsF07iFvNYxPIvIyPEV74eZ0zgFEsAH1daK1OxPy+LXdZ4grV17P5tVzhQ==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.14.tgz",
"integrity": "sha512-gVkJdnR/L6iIcGYXx64HGJRmlme2FGr/aZH0W6u4A3RgPMAb+6ELRLi+UBiH83RXBm9vwCfkIC/q8T51h8vUJQ==",
"cpu": [
"arm64"
],
@ -2435,9 +2325,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.13.tgz",
"integrity": "sha512-sQRH09faifF9w9WS6TKDWr1oLi4hoPx0EIWXZHQK/jcjarDpXGQ2DbF0KnALJCwWBxOIP/1nrmU01fZwwMzY3g==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.14.tgz",
"integrity": "sha512-EE+EQ+c6tTpzsg+LGO1uuusjXxYx0Q00JE5ubcIGfsogSKth8n8i2BcS2wYTQe4jXGs+BQs35l78BIPzgwLddw==",
"cpu": [
"x64"
],
@ -2452,9 +2342,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.13.tgz",
"integrity": "sha512-Or1N8DIF3tP+LsloJp+UXLTIMMHMUcWXFhJLCsM4T7MzFzxkeReewRWXfk5mk137cdqVeUEH/R50xAhY1mOkTQ==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.14.tgz",
"integrity": "sha512-KCCOzo+L6XPT0oUp2Jwh233ETRQ/F6cwUnMnR0FvMUCbkDAzHbcyOgpfuAtRa5HD0WbTbH4pVD+S0pn1EhNfbw==",
"cpu": [
"x64"
],
@ -2469,9 +2359,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.13.tgz",
"integrity": "sha512-u2mQyqCFrr9vVTP6sfDRfGE6bhOX3/7rInehzxNhHX1HYRIx09H3sDdXzTxnZWKOjIg3qjFTCrYFUZckva5PIg==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.14.tgz",
"integrity": "sha512-AHObFiFL9lNYcm3tZSPqa/cHGpM5wOrNmM2uOMoKppp+0Hom5uuyRh0QkOp7jftsHZdrZUpmoz0Mp6vhh2XtUg==",
"cpu": [
"arm64"
],
@ -2486,9 +2376,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.13.tgz",
"integrity": "sha512-sOEc4iCanp1Yqyeu9suQcEzfaUcHnqjBUgDg0ZXpjUMUwdSi37S1lu1RGoV1BYInvvGu3y3HHTmvsSfDhx2L8w==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.14.tgz",
"integrity": "sha512-rNXXMDJfCJLw/ZaFTOLOHoGULxyXfh2iXTGiChFiYTSgKBKQHIGEpV0yn5N25WGzJJ+VBnRjHzlmDqRV+d//oQ==",
"cpu": [
"x64"
],
@ -2503,16 +2393,16 @@
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.13.tgz",
"integrity": "sha512-0XTd/NoVUAktIDaA4MdXhve0QWYh7WlZg20EHCuBFR80F8FhbVkRX+AY5cjbUP/IO2itHzt0iHc0iSE5kBUMhQ==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.14.tgz",
"integrity": "sha512-y69ztPTRFy+13EPS/7dEFVl7q2Goh1pQueVO8IfGeyqSpcx/joNJXFk0lLhMgUbF0VFJotwRSb9ZY7Xoq3r26Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.0.13",
"@tailwindcss/oxide": "4.0.13",
"@tailwindcss/node": "4.0.14",
"@tailwindcss/oxide": "4.0.14",
"lightningcss": "1.29.2",
"tailwindcss": "4.0.13"
"tailwindcss": "4.0.14"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6"
@ -3258,22 +3148,37 @@
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-draft-04": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
"integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^8.5.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ajv-formats": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
@ -3292,30 +3197,6 @@
}
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -3391,9 +3272,9 @@
"license": "MIT"
},
"node_modules/bits-ui": {
"version": "1.3.11",
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.3.11.tgz",
"integrity": "sha512-E8OMM9ae3iTwzKBdy6v0A9sp6xwvWEBz41TS5KkLYeFwZZnTJvc6P+0IqyzFLpQHxuRCZZNFLb5Wid00QvRbJA==",
"version": "1.3.12",
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.3.12.tgz",
"integrity": "sha512-RhPvg2e7mTQSXR9WMdsyR5eTC2DSa4ch5MT/TjIaN3suMJ3RGlbzYPNvf4n56Lgck3W4Xm+L4jSgrzTdynuM8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3943,9 +3824,9 @@
}
},
"node_modules/eslint-plugin-svelte": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.1.0.tgz",
"integrity": "sha512-hSQyLDkuuHPJby1ixZfUVrfLON42mT0Odf18MbwAgFUPuyIwJlhy3acUY1/bxt+Njucq/dQxR543zYDqkBNLmw==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.2.0.tgz",
"integrity": "sha512-cxRkiF5XrydmLertOWoJ2LPqu0DItX3bbbJRFqzZ9MsRy3+3tPtQUsfsSuSxFTyDWWWAT1errzNhDLHehir9bw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4006,6 +3887,30 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/eslint/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/esm-env": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
@ -4917,9 +4822,9 @@
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
@ -6213,19 +6118,6 @@
"node": ">=8.6"
}
},
"node_modules/micromatch/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -6292,9 +6184,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.9",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz",
"integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==",
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.4.tgz",
"integrity": "sha512-GTFcMIDgR7tqji/LpSY8rtg464VnJl/j6ypoehYnuGb+Y8qZUdtKB8WVCXon0UEZgFDbuUxpIl//6FHLHgXSNA==",
"dev": true,
"funding": [
{
@ -6304,10 +6196,10 @@
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
"node": "^18 || >=20"
}
},
"node_modules/natural-compare": {
@ -6444,15 +6336,13 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
@ -6595,6 +6485,25 @@
"node": ">=4"
}
},
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.10",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.10.tgz",
"integrity": "sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -6805,25 +6714,6 @@
"vue": ">= 3.2.0"
}
},
"node_modules/radix-vue/node_modules/nanoid": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.3.tgz",
"integrity": "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@ -7297,9 +7187,9 @@
}
},
"node_modules/svelte": {
"version": "5.23.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.23.0.tgz",
"integrity": "sha512-v0lL3NuKontiCxholEiAXCB+BYbndlKbwlDMK0DS86WgGELMJSpyqCSbJeMEMBDwOglnS7Ar2Rq0wwa/z2L8Vg==",
"version": "5.23.1",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.23.1.tgz",
"integrity": "sha512-DUu3e5tQDO+PtKffjqJ548YfeKtw2Rqc9/+nlP26DZ0AopWTJNylkNnTOP/wcgIt1JSnovyISxEZ/lDR1OhbOw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -7347,9 +7237,9 @@
}
},
"node_modules/svelte-eslint-parser": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.0.1.tgz",
"integrity": "sha512-JjdEMXOJqy+dxeaElxbN+meTOtVpHfLnq9VGpiTAOLgM0uHO+ogmUsA3IFgx0x3Wl15pqTZWycCikcD7cAQN/g==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.1.0.tgz",
"integrity": "sha512-JP0v/wzDXWxza6c8K9ZjKKHYfgt0KidlbWx1e9n9UV4q+o28GTkk71fR0IDZDmLUDYs3vSq0+Tm9fofDqzGe1w==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -7425,9 +7315,9 @@
}
},
"node_modules/tailwindcss": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.13.tgz",
"integrity": "sha512-gbvFrB0fOsTv/OugXWi2PtflJ4S6/ctu6Mmn3bCftmLY/6xRsQVEJPgIIpABwpZ52DpONkCA3bEj5b54MHxF2Q==",
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.14.tgz",
"integrity": "sha512-92YT2dpt671tFiHH/e1ok9D987N9fHD5VWoly1CdPD/Cd1HMglvZwP3nx2yTj2lbXDAHt8QssZkxTLCCTNL+xw==",
"dev": true,
"license": "MIT"
},
@ -7527,9 +7417,9 @@
"license": "0BSD"
},
"node_modules/tw-animate-css": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.0.tgz",
"integrity": "sha512-bpQKhkp5CrI3GJ9IIHfhKjp2Fk1I43iGrJUvPadCdddr2oYBUXa+9o+zTRbJhylgbqosDtL97ss8PLTTcdAh8w==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.2.tgz",
"integrity": "sha512-TdSaQcV+V8MypECoXr6Nnv3EQFz05kxfTUaOHtpWTQZp8r5Bx2OmTnkSZVlOTaRiS20a6wYw0RaAnNX2D8ywrQ==",
"dev": true,
"license": "MIT"
},
@ -7799,9 +7689,9 @@
}
},
"node_modules/vite": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz",
"integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz",
"integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -0,0 +1,5 @@
import type { User } from "./services/user/v1/user_pb"
export let userState: { user: User | undefined } = $state({
user: undefined
});

View File

@ -0,0 +1,15 @@
<script lang="ts">
import { userState } from '$lib/sharedState.svelte';
import { Avatar } from 'bits-ui';
</script>
<Avatar.Root class="flex h-full w-full items-center justify-center">
<Avatar.Image
src={userState.user?.profilePicture}
alt={`${userState.user?.username}'s avatar`}
class="rounded-full"
/>
<Avatar.Fallback class="font-medium uppercase"
>{userState.user?.username.substring(0, 2)}</Avatar.Fallback
>
</Avatar.Root>

View File

@ -20,7 +20,7 @@
<Button.Root
{type}
class={cn(
'bg-sky text-crust hover:brightness-120 w-fit cursor-pointer rounded p-2 px-4 text-sm font-medium transition-all',
'bg-sky text-crust flex justify-center items-center hover:brightness-120 focus:outline-sky w-fit cursor-pointer rounded p-2 px-4 text-sm font-medium transition-all focus:outline-2 focus:outline-offset-1',
className
)}
{onclick}

View File

@ -1,23 +1,32 @@
<script lang="ts">
import { ArrowLeft, ArrowRight, Minus, Calendar } from '@lucide/svelte';
import { ArrowLeft, ArrowRight, Minus, Calendar, X } from '@lucide/svelte';
import { DateRangePicker, type DateRange } from 'bits-ui';
import { fade } from 'svelte/transition';
import { getLocalTimeZone } from '@internationalized/date';
import { cn } from '$lib/utils';
let {
className,
start = $bindable(),
end = $bindable(),
onchange
}: {
className?: string;
start?: Date;
end?: Date;
onchange?: (start: Date, end: Date) => void;
onchange?: (start?: Date, end?: Date) => void;
} = $props();
let daterange: DateRange | undefined = $state();
let daterange: DateRange = $state({
start: undefined,
end: undefined
});
let rerender = $state(false);
</script>
<DateRangePicker.Root
<!-- Need to rerender because setting to undefined doesn't work -->
{#key rerender}
<DateRangePicker.Root
bind:value={daterange}
onValueChange={(v) => {
if (v.start && v.end) {
@ -28,11 +37,12 @@
}
}
}}
>
<div
class="bg-mantle border-surface-0 hover:border-surface-2 flex items-center justify-center gap-2 rounded border p-1 text-sm drop-shadow-md transition-all"
class={cn(className)}
>
<DateRangePicker.Label />
<div
class="bg-mantle border-surface-0 hover:border-surface-2 flex items-center rounded border pl-2 text-sm drop-shadow-md transition-all"
>
<div class="grow flex items-center justify-center">
{#each ['start', 'end'] as const as type}
<DateRangePicker.Input {type}>
{#snippet children({ segments })}
@ -55,16 +65,34 @@
{/snippet}
</DateRangePicker.Input>
{#if type === 'start'}
<div aria-hidden="true">
<div aria-hidden="true" class="px-1">
<Minus size="10" />
</div>
{/if}
{/each}
</div>
<DateRangePicker.Trigger
class="text-overlay-2 hover:bg-surface-0 ml-1 cursor-pointer rounded p-1 transition-all"
class="text-overlay-2 hover:bg-surface-0 grow flex justify-center items-center focus:outline-sky ml-1 cursor-pointer p-2 transition-all focus:outline focus:outline-offset-1"
>
<Calendar size="20" />
</DateRangePicker.Trigger>
<button
class="text-overlay-2 hover:bg-surface-0 focus:outline-sky cursor-pointer rounded-r p-2 transition-all focus:outline focus:outline-offset-1"
onclick={() => {
if (daterange) {
daterange.end = undefined;
daterange.start = undefined;
}
start = undefined;
end = undefined;
if (onchange) {
onchange(start, end);
}
rerender = !rerender;
}}
>
<X size="20" />
</button>
</div>
<DateRangePicker.Content forceMount>
{#snippet child({ props, open })}
@ -138,4 +166,5 @@
{/if}
{/snippet}
</DateRangePicker.Content>
</DateRangePicker.Root>
</DateRangePicker.Root>
{/key}

View File

@ -0,0 +1,51 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { X } from '@lucide/svelte';
let {
name,
value = $bindable(''),
type = 'text',
placeholder,
className,
onchange
}: {
name?: string;
value?: string | number;
type?: string;
placeholder?: string;
className?: string;
onchange?: (e: Event) => void;
} = $props();
</script>
<div
class={cn(
'border-surface-0 hover:border-surface-2 flex items-center justify-between gap-1 rounded border p-0 drop-shadow-md transition-all',
className
)}
>
<input
id={name}
{name}
{type}
{placeholder}
class="focus:outline-sky grow rounded-l p-2 text-sm transition-all focus:outline focus:outline-offset-1"
bind:value
{onchange}
/>
<button
class="text-overlay-2 hover:bg-surface-0 focus:outline-sky cursor-pointer rounded-r p-2 transition-all focus:outline focus:outline-offset-1"
type="button"
onclick={(e) => {
if (value) {
value = '';
if (onchange) {
onchange(e);
}
}
}}
>
<X size="20" />
</button>
</div>

View File

@ -1,18 +1,27 @@
<script lang="ts">
import { X } from '@lucide/svelte';
import { Dialog } from 'bits-ui';
import { fade } from 'svelte/transition';
import type { Snippet } from 'svelte';
let {
trigger,
title,
content,
open = $bindable(false)
}: { trigger: Snippet; content: Snippet; open?: boolean } = $props();
}: {
trigger: Snippet<[Record<string, unknown>]>;
title: Snippet;
content: Snippet;
open?: boolean;
} = $props();
</script>
<Dialog.Root bind:open>
<Dialog.Trigger>
{@render trigger()}
{#snippet child({ props })}
{@render trigger(props)}
{/snippet}
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay forceMount>
@ -30,8 +39,8 @@
{/snippet}
</Dialog.Overlay>
<Dialog.Content forceMount>
{#snippet child({ props, open })}
{#if open}
{#snippet child({ props, open: propopen })}
{#if propopen}
<div
{...props}
transition:fade={{
@ -41,6 +50,20 @@
<div
class="bg-mantle border-surface-0 fixed inset-0 left-[50%] top-[50%] z-50 size-fit w-96 -translate-x-1/2 -translate-y-1/2 transform overflow-y-auto rounded-xl border pb-1 drop-shadow-md"
>
<div class="border-surface-0 flex justify-between border-b p-2">
<h1 class="grow truncate p-1 text-center text-xl font-bold">
{@render title()}
</h1>
<button
tabindex="-1"
class="text-overlay-2 hover:bg-surface-0 focus:outline-sky cursor-pointer rounded p-1 transition-all focus:outline focus:outline-offset-1"
onclick={() => {
open = false;
}}
>
<X />
</button>
</div>
{@render content()}
</div>
</div>

View File

@ -0,0 +1,63 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
import { Pagination } from 'bits-ui';
let {
count = $bindable(),
limit = $bindable(),
offset = $bindable(0),
className,
onchange
}: {
count: number;
limit: number;
offset?: number;
className?: string;
onchange?: (e: number) => void;
} = $props();
</script>
{#key count && limit}
<Pagination.Root
{count}
perPage={limit}
onPageChange={(e) => {
offset = (e - 1) * limit;
window.scrollTo(0, 0);
onchange?.(e);
}}
>
{#snippet children({ pages, range })}
<div class={cn('mb-2 flex items-center justify-center gap-2', className)}>
<Pagination.PrevButton
class="hover:bg-surface-0 disabled:text-overlay-0 inline-flex cursor-pointer items-center justify-center rounded p-2 transition-all disabled:cursor-not-allowed hover:disabled:bg-transparent"
>
<ChevronLeft />
</Pagination.PrevButton>
<div class="flex items-center gap-2">
{#each pages as page (page.key)}
{#if page.type === 'ellipsis'}
<div class="select-none font-medium">...</div>
{:else}
<Pagination.Page
{page}
class="hover:bg-surface-0 data-selected:bg-surface-0 data-selected:text-background inline-flex size-10 cursor-pointer select-none items-center justify-center rounded bg-transparent font-medium transition-all disabled:cursor-not-allowed disabled:opacity-50 hover:disabled:bg-transparent"
>
{page.value}
</Pagination.Page>
{/if}
{/each}
</div>
<Pagination.NextButton
class="hover:bg-surface-0 disabled:text-overlay-0 inline-flex cursor-pointer items-center justify-center rounded p-2 transition-all disabled:cursor-not-allowed hover:disabled:bg-transparent"
>
<ChevronRight />
</Pagination.NextButton>
</div>
<p class="text-overlay-2 text-center text-sm">
Showing {range.start} - {range.end}
</p>
{/snippet}
</Pagination.Root>
{/key}

View File

@ -0,0 +1,82 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { Check, ChevronsDown, ChevronsUp, ChevronsUpDown, X } from '@lucide/svelte';
import { Select } from 'bits-ui';
let {
value = $bindable('10'),
placeholder = 'Select an item',
items = [],
defaultValue = '',
className,
onchange
}: {
value?: string;
placeholder?: string;
items: { value: string; label: string; disabled?: boolean }[];
defaultValue?: string;
className?: string;
onchange?: (e: string) => void;
} = $props();
const selectedLabel = $derived(value ? items.find((i) => i.value === value)?.label : placeholder);
</script>
<div
class={cn(
'border-surface-0 bg-mantle hover:border-surface-2 flex items-center justify-between rounded border p-0 drop-shadow-md transition-all',
className
)}
>
<Select.Root type="single" {items} bind:value onValueChange={onchange}>
<Select.Trigger
class="focus:outline-sky data-placeholder:text-overlay-0 gap-2 inline-flex grow cursor-pointer select-none items-center justify-between rounded-l py-2 pl-2 text-sm transition-colors focus:outline focus:outline-offset-1"
aria-label={placeholder}
>
{selectedLabel}
<ChevronsUpDown class="text-overlay-0" size="20" />
</Select.Trigger>
<Select.Portal>
<Select.Content
class="focus-override border-surface-0 bg-mantle shadow-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 outline-hidden z-50 select-none rounded border p-1"
sideOffset={10}
>
<Select.ScrollUpButton class="flex w-full items-center justify-center">
<ChevronsUp size="20" />
</Select.ScrollUpButton>
<Select.Viewport class="p-1">
{#each items as item, i (i + item.value)}
<Select.Item
class="data-disabled:cursor-not-allowed data-highlighted:bg-surface-0 outline-hidden data-disabled:opacity-50 flex h-10 w-full cursor-pointer select-none items-center gap-4 rounded px-5 py-3 text-sm capitalize"
value={item.value}
label={item.label}
disabled={item.disabled}
>
{#snippet children({ selected })}
{item.label}
{#if selected}
<div class="ml-auto">
<Check size="20" />
</div>
{/if}
{/snippet}
</Select.Item>
{/each}
</Select.Viewport>
<Select.ScrollDownButton class="flex w-full items-center justify-center">
<ChevronsDown size="20" />
</Select.ScrollDownButton>
</Select.Content>
</Select.Portal>
</Select.Root>
<button
class="text-overlay-2 hover:bg-surface-0 focus:outline-sky cursor-pointer rounded-r p-2 transition-all focus:outline focus:outline-offset-1"
type="button"
onclick={() => {
value = defaultValue;
onchange?.(value);
}}
>
<X size="20" />
</button>
</div>

View File

@ -9,17 +9,20 @@
House,
type Icon as IconType
} from '@lucide/svelte';
import { NavigationMenu, Popover, Separator, Dialog, Avatar } from 'bits-ui';
import { NavigationMenu, Popover, Separator, Dialog } from 'bits-ui';
import { fade, fly, slide } from 'svelte/transition';
import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation';
import { AuthClient, UserClient } from '$lib/transport';
import { page } from '$app/state';
import { cn } from '$lib/utils';
import { userState } from '$lib/sharedState.svelte';
import Avatar from '$lib/ui/Avatar.svelte';
let { children } = $props();
let user = UserClient.getUser({}).then((res) => {
return res.user;
UserClient.getUser({}).then((res) => {
userState.user = res.user;
});
let sidebarOpen = $state(false);
@ -52,6 +55,7 @@
await AuthClient.logout({});
await goto('/auth');
toast.success('logged out successfully');
userState.user = undefined;
if (sidebarOpen) {
sidebarOpen = false;
@ -175,16 +179,9 @@
<Popover.Root bind:open={popupOpen}>
<Popover.Trigger
class="outline-surface-2 hover:brightness-120 bg-text text-crust h-9 w-9 cursor-pointer rounded-full outline outline-offset-2 text-sm transition-all"
class="outline-surface-2 hover:brightness-120 bg-text text-crust h-9 w-9 cursor-pointer rounded-full text-sm outline outline-offset-2 transition-all"
>
{#await user then user}
<Avatar.Root class="flex h-full w-full items-center justify-center">
<Avatar.Image src={user?.profilePicture} alt={`${user?.username}'s avatar`} class="rounded-full" />
<Avatar.Fallback class="font-medium uppercase"
>{user?.username.substring(0, 2)}</Avatar.Fallback
>
</Avatar.Root>
{/await}
<Avatar />
</Popover.Trigger>
<Popover.Content forceMount>
{#snippet child({ wrapperProps, props, open })}
@ -223,6 +220,6 @@
</Popover.Root>
</header>
<div class="pt-[50px] overflow-auto">
<div class="overflow-auto pt-[50px]">
{@render children()}
</div>

View File

@ -1,19 +1,23 @@
<script lang="ts">
import { ItemClient } from '$lib/transport';
import { Plus, Trash, Pencil, Calendar, Minus, ArrowLeft, ArrowRight } from '@lucide/svelte';
import { Plus, Trash, Pencil } from '@lucide/svelte';
import { timestampFromDate, timestampDate } from '@bufbuild/protobuf/wkt';
import { toast } from 'svelte-sonner';
import { ConnectError } from '@connectrpc/connect';
import Modal from '$lib/ui/Modal.svelte';
import Button from '$lib/ui/Button.svelte';
import DateRangePicker from '$lib/ui/DateRangePicker.svelte';
import Input from '$lib/ui/Input.svelte';
import Select from '$lib/ui/Select.svelte';
import { SvelteMap } from 'svelte/reactivity';
import type { Item } from '$lib/services/item/v1/item_pb';
import Pagination from '$lib/ui/Pagination.svelte';
// Config
let limit: number = $state(10);
let offset: number = $state(0);
let start = $state(new Date(new Date().setDate(new Date().getDate() - 1)));
let end = $state(new Date());
let start: Date | undefined = $state();
let end: Date | undefined = $state();
let filter = $state('');
// Items
@ -29,8 +33,8 @@
return await ItemClient.getItems({
limit: limit,
offset: offset,
start: timestampFromDate(start),
end: timestampFromDate(end),
start: start ? timestampFromDate(start) : undefined,
end: end ? timestampFromDate(end) : undefined,
filter: filter
}).then((resp) => {
count = Number(resp.count);
@ -45,52 +49,44 @@
}
</script>
<div class="mx-4 my-2 flex items-center justify-center gap-4">
<input
type="text"
placeholder="Filter..."
class="border-surface-0 hover:border-surface-2 w-70 bg-mantle rounded border p-2 text-sm drop-shadow-md transition-all"
<div class="mx-4 my-2 flex flex-wrap items-center justify-center gap-2">
<Input
bind:value={filter}
className="bg-mantle"
placeholder="Filter"
onchange={updateItems}
/>
<DateRangePicker bind:start bind:end />
<Select
items={[
{
label: '10 Items',
value: '10'
},
{
label: '25 Items',
value: '25'
},
{
label: '100 Items',
value: '100'
},
{
label: '250 Items',
value: '250'
}
]}
placeholder="Items per page"
onchange={() => {
offset = 0;
updateItems();
}}
defaultValue="10"
bind:value={() => limit.toString(), (v) => (limit = parseInt(v))}
/>
<DateRangePicker bind:start bind:end onchange={updateItems} />
</div>
<div
class="border-surface-0 bg-mantle mx-4 my-2 overflow-x-auto rounded border-x border-t drop-shadow-md"
>
<table class="w-full table-auto border-collapse text-left rtl:text-right">
<thead>
<tr class="border-surface-0 border-b">
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Added</th>
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Name</th>
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Description</th>
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Price</th>
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Quantity</th>
<th class="w-0"></th>
</tr>
</thead>
<tbody>
{#await items}
<tr class="border-surface-0 border-b">
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
<td class="w-8"></td>
</tr>
{:then items}
{#each items as item}
<tr class="border-surface-0 border-b">
<td class="px-6 py-3">
{item.added ? timestampDate(item.added).toLocaleString() : ''}
</td>
<td class="px-6 py-3">{item.name}</td>
<td class="px-6 py-3">{item.description}</td>
<td class="px-6 py-3">{item.price}</td>
<td class="px-6 py-3">{item.quantity}</td>
<td class="pr-2">
<div class="flex gap-2">
{#snippet editModal(item: Item)}
<Modal
bind:open={
() =>
@ -100,16 +96,17 @@
(value) => editsOpen.set(item.id!, value)
}
>
{#snippet trigger()}
<Button className="bg-text">
{#snippet trigger(props)}
<Button {...props} className="bg-text">
<Pencil />
</Button>
{/snippet}
{#snippet title()}
Edit '{item.name}'
{/snippet}
{#snippet content()}
<h1 class="border-surface-0 border-b py-3 text-center text-xl font-bold">
Edit {item.name}
</h1>
<form
onsubmit={async (e) => {
e.preventDefault();
@ -145,50 +142,28 @@
<div class="flex flex-col gap-4 p-3">
<div class="flex flex-col gap-1">
<label for="name" class="text-sm">Name</label>
<input
id="name"
name="name"
type="text"
class="border-surface-0 rounded border p-2 text-sm"
value={item.name}
/>
<Input name="name" type="text" value={item.name} />
</div>
<div class="flex flex-col gap-1">
<label for="description" class="text-sm">Description</label>
<input
id="description"
name="description"
type="text"
class="border-surface-0 rounded border p-2 text-sm"
value={item.description}
/>
<Input name="description" type="text" value={item.description} />
</div>
<div class="flex flex-col gap-1">
<label for="price" class="text-sm">Price</label>
<input
id="price"
name="price"
type="number"
class="border-surface-0 rounded border p-2 text-sm"
value={item.price}
/>
<Input name="price" type="number" value={item.price} />
</div>
<div class="flex flex-col gap-1">
<label for="quantity" class="text-sm">Quantity</label>
<input
id="quantity"
name="quantity"
type="number"
class="border-surface-0 rounded border p-2 text-sm"
value={item.quantity}
/>
<Input name="quantity" type="number" value={item.quantity} />
</div>
<Button type="submit">Submit</Button>
</div>
</form>
{/snippet}
</Modal>
{/snippet}
{#snippet deleteModal(item: Item)}
<Modal
bind:open={
() =>
@ -198,16 +173,17 @@
(value) => deletesOpen.set(item.id!, value)
}
>
{#snippet trigger()}
<Button className="bg-red">
{#snippet trigger(props)}
<Button {...props} className="bg-red">
<Trash />
</Button>
{/snippet}
{#snippet title()}
Delete '{item.name}'
{/snippet}
{#snippet content()}
<h1 class="border-surface-0 border-b py-3 text-center text-xl font-bold">
Delete {item.name}
</h1>
<form
onsubmit={async (e) => {
e.preventDefault();
@ -227,9 +203,7 @@
}}
>
<div class="flex flex-col gap-4 p-3">
<span class="text-center"
>Are you sure you want to delete "{item.name}"?</span
>
<span class="text-center">Are you sure you want to delete "{item.name}"?</span>
<div class="flex justify-center gap-4">
<Button type="submit">Submit</Button>
</div>
@ -237,6 +211,46 @@
</form>
{/snippet}
</Modal>
{/snippet}
<div
class="border-surface-0 bg-mantle mx-4 my-2 hidden overflow-x-auto rounded border-x border-t drop-shadow-md sm:block"
>
<table class="w-full table-auto border-collapse text-left rtl:text-right">
<thead>
<tr class="border-surface-0 border-b">
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Added</th>
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Name</th>
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Description</th>
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Price</th>
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Quantity</th>
<th class="w-0"></th>
</tr>
</thead>
<tbody>
{#await items}
<tr class="border-surface-0 border-b">
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
<td class="w-8"></td>
</tr>
{:then items}
{#each items as item}
<tr class="border-surface-0 border-b">
<td class="px-6 py-3">
{item.added ? timestampDate(item.added).toLocaleString() : ''}
</td>
<td class="px-6 py-3">{item.name}</td>
<td class="px-6 py-3">{item.description}</td>
<td class="px-6 py-3">${item.price}</td>
<td class="px-6 py-3">{item.quantity}</td>
<td class="pr-2">
<div class="flex gap-2">
{@render editModal(item)}
{@render deleteModal(item)}
</div>
</td>
</tr>
@ -246,16 +260,81 @@
</table>
</div>
<div class="mx-4 mt-1 flex justify-end">
<div class="flex flex-wrap justify-center gap-2 px-4 sm:hidden">
{#await items}
<div
class="border-surface-0 bg-mantle flex w-full flex-col gap-2 rounded border p-5 drop-shadow-md"
>
<div class="flex flex-col">
<span class="text-subtext-0 text-sm">Added</span>
<div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div>
</div>
<div class="flex flex-col">
<span class="text-subtext-0 text-sm">Name</span>
<div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div>
</div>
<div class="flex flex-col">
<span class="text-subtext-0 text-sm">Description</span>
<div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div>
</div>
<div class="flex flex-col">
<span class="text-subtext-0 text-sm">Price</span>
<div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div>
</div>
<div class="flex flex-col">
<span class="text-subtext-0 text-sm">Quantity</span>
<div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div>
</div>
</div>
{:then items}
{#each items as item}
<div
class="border-surface-0 bg-mantle flex w-full flex-wrap gap-6 rounded border p-5 drop-shadow-md"
>
<div class="flex flex-col">
<span class="text-subtext-0 text-sm">Added</span>
<span class="truncate"
>{item.added ? timestampDate(item.added).toLocaleString() : ''}</span
>
</div>
<div class="flex flex-col">
<span class="text-subtext-0 text-sm">Name</span>
<span class="truncate">{item.name}</span>
</div>
<div class="flex flex-col">
<span class="text-subtext-0 text-sm">Description</span>
<span class="truncate">{item.description}</span>
</div>
<div class="flex flex-col">
<span class="text-subtext-0 text-sm">Price</span>
<span class="truncate">${item.price}</span>
</div>
<div class="flex flex-col">
<span class="text-subtext-0 text-sm">Quantity</span>
<span class="truncate">{item.quantity}</span>
</div>
<div class="flex justify-end ml-auto gap-2">
{@render editModal(item)}
{@render deleteModal(item)}
</div>
</div>
{/each}
{/await}
</div>
<div class="mx-4 mb-4 mt-2 flex justify-end sm:mt-1">
<Modal bind:open={addedOpen}>
{#snippet trigger()}
<Button className="bg-sky">
{#snippet trigger(props)}
<Button {...props} className="bg-sky">
<Plus />
</Button>
{/snippet}
{#snippet title()}
Add Item
{/snippet}
{#snippet content()}
<h1 class="border-surface-0 border-b py-3 text-center text-xl font-bold">Add Item</h1>
<form
onsubmit={async (e) => {
e.preventDefault();
@ -291,39 +370,19 @@
<div class="flex flex-col gap-4 p-3">
<div class="flex flex-col gap-1">
<label for="name" class="text-sm">Name</label>
<input
id="name"
name="name"
type="text"
class="border-surface-0 rounded border p-2 text-sm"
/>
<Input name="name" type="text" />
</div>
<div class="flex flex-col gap-1">
<label for="description" class="text-sm">Description</label>
<input
id="description"
name="description"
type="text"
class="border-surface-0 rounded border p-2 text-sm"
/>
<Input name="description" type="text" />
</div>
<div class="flex flex-col gap-1">
<label for="price" class="text-sm">Price</label>
<input
id="price"
name="price"
type="number"
class="border-surface-0 rounded border p-2 text-sm"
/>
<Input name="price" type="text" />
</div>
<div class="flex flex-col gap-1">
<label for="quantity" class="text-sm">Quantity</label>
<input
id="quantity"
name="quantity"
type="number"
class="border-surface-0 rounded border p-2 text-sm"
/>
<Input name="quantity" type="text" />
</div>
<Button type="submit">Submit</Button>
</div>
@ -331,3 +390,7 @@
{/snippet}
</Modal>
</div>
<div class="py-4">
<Pagination bind:count bind:limit bind:offset onchange={updateItems} />
</div>

View File

@ -2,46 +2,41 @@
import { UserClient } from '$lib/transport';
import Button from '$lib/ui/Button.svelte';
import Modal from '$lib/ui/Modal.svelte';
import Input from '$lib/ui/Input.svelte';
import { ConnectError } from '@connectrpc/connect';
import { Avatar, Separator } from 'bits-ui';
import { Separator } from 'bits-ui';
import { toast } from 'svelte-sonner';
import { userState } from '$lib/sharedState.svelte';
import Avatar from '$lib/ui/Avatar.svelte';
let user = UserClient.getUser({}).then((res) => {
return res.user;
});
let openChangeProfilePicture = $state(false);
let key = $state('');
</script>
<div class="flex h-[calc(100vh-50px)]">
<div class="m-auto flex w-96 flex-col gap-4 p-4">
{#await user then user}
<div class="flex items-center justify-center gap-4">
<div
class="outline-surface-2 bg-text text-crust h-9 w-9 select-none rounded-full outline outline-offset-2 text-sm"
class="outline-surface-2 bg-text text-crust h-9 w-9 select-none rounded-full text-sm outline outline-offset-2"
>
<Avatar.Root class="flex h-full w-full items-center justify-center">
<Avatar.Image src={user?.profilePicture} alt={`${user?.username}'s avatar`} class="rounded-full" />
<Avatar.Fallback class="font-medium uppercase"
>{user?.username.substring(0, 2)}</Avatar.Fallback
>
</Avatar.Root>
<Avatar />
</div>
<h1 class="overflow-x-hidden text-2xl font-medium">{user?.username}</h1>
<h1 class="overflow-x-hidden text-2xl font-medium">{userState.user?.username}</h1>
</div>
{/await}
<Separator.Root class="bg-surface-0 h-px" />
<div class="flex justify-around gap-2">
<Modal>
{#snippet trigger()}
<Button className="bg-text">Generate API Key</Button>
{#snippet trigger(props)}
<Button {...props} className="bg-text">Generate API Key</Button>
{/snippet}
{#snippet title()}
Generate API Key
{/snippet}
{#snippet content()}
<h1 class="border-surface-0 border-b py-3 text-center text-xl font-bold">
Generate API Key
</h1>
{#if key == ''}
<form
onsubmit={async (e) => {
@ -68,21 +63,11 @@
<div class="flex flex-col gap-4 p-3">
<div class="flex flex-col gap-1">
<label for="password" class="text-sm">Password</label>
<input
id="password"
name="password"
type="password"
class="border-surface-0 rounded border p-2 text-sm"
/>
<Input name="password" type="password" />
</div>
<div class="flex flex-col gap-1">
<label for="confirm-password" class="text-sm">Confirm Password</label>
<input
id="confirm-password"
name="confirm-password"
type="password"
class="border-surface-0 rounded border p-2 text-sm"
/>
<Input name="confirm-password" type="password" />
</div>
<Button type="submit">Submit</Button>
</div>
@ -95,15 +80,16 @@
{/snippet}
</Modal>
<Modal>
{#snippet trigger()}
<Button className="bg-text">Change Profile Picture</Button>
<Modal bind:open={openChangeProfilePicture}>
{#snippet trigger(props)}
<Button {...props} className="bg-text">Change Profile Picture</Button>
{/snippet}
{#snippet title()}
Change Profile Picture
{/snippet}
{#snippet content()}
<h1 class="border-surface-0 border-b py-3 text-center text-xl font-bold">
Change Profile Picture
</h1>
<form
onsubmit={async (e) => {
e.preventDefault();
@ -122,13 +108,14 @@
try {
const response = await UserClient.updateProfilePicture({
fileName: file.name,
data: data,
data: data
});
if (response.user) {
toast.success('Profile picture updated');
form.reset();
openChangeProfilePicture = false;
userState.user = response.user;
}
} catch (err) {
const error = ConnectError.from(err);
@ -139,12 +126,7 @@
<div class="flex flex-col gap-4 p-3">
<div class="flex flex-col gap-1">
<label for="file" class="text-sm">Profile Picture</label>
<input
id="file"
name="file"
type="file"
class="border-surface-0 rounded border p-2 text-sm"
/>
<Input name="file" type="file" />
</div>
<Button type="submit">Submit</Button>
</div>
@ -178,30 +160,15 @@
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label for="old-password" class="text-sm">Old Password</label>
<input
id="old-password"
name="old-password"
type="password"
class="border-surface-0 rounded border p-2 text-sm"
/>
<Input name="old-password" type="password" />
</div>
<div class="flex flex-col gap-1">
<label for="new-password" class="text-sm">New Password</label>
<input
id="new-password"
name="new-password"
type="password"
class="border-surface-0 rounded border p-2 text-sm"
/>
<Input name="new-password" type="password" />
</div>
<div class="flex flex-col gap-1">
<label for="confirm-password" class="text-sm">Confirm New Password</label>
<input
id="confirm-password"
name="confirm-password"
type="password"
class="border-surface-0 rounded border p-2 text-sm"
/>
<Input name="confirm-password" type="password" />
</div>
<Button type="submit">Submit</Button>
</div>

View File

@ -6,6 +6,7 @@
import { ConnectError } from '@connectrpc/connect';
import { toast } from 'svelte-sonner';
import Button from '$lib/ui/Button.svelte';
import Input from '$lib/ui/Input.svelte';
let tab = $state('login');
</script>
@ -59,21 +60,11 @@
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label for="login-username" class="text-sm">Username</label>
<input
id="login-username"
name="login-username"
type="text"
class="border-surface-0 rounded border p-2 text-sm"
/>
<Input name="login-username" />
</div>
<div class="flex flex-col gap-1">
<label for="login-password" class="text-sm">Password</label>
<input
id="login-password"
name="login-password"
type="password"
class="border-surface-0 rounded border p-2 text-sm"
/>
<Input name="login-password" type="password" />
</div>
<Button type="submit">Submit</Button>
</div>
@ -108,30 +99,15 @@
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label for="signup-username" class="text-sm">Username</label>
<input
id="signup-username"
name="signup-username"
type="text"
class="border-surface-0 rounded border p-2 text-sm"
/>
<Input name="signup-username" />
</div>
<div class="flex flex-col gap-1">
<label for="signup-password" class="text-sm">Password</label>
<input
id="signup-password"
name="signup-password"
type="password"
class="border-surface-0 rounded border p-2 text-sm"
/>
<Input name="signup-password" />
</div>
<div class="flex flex-col gap-1">
<label for="signup-confirm-password" class="text-sm">Confirm Password</label>
<input
id="signup-confirm-password"
name="signup-confirm-password"
type="password"
class="border-surface-0 rounded border p-2 text-sm"
/>
<Input name="signup-confirm-password" />
</div>
<Button type="submit">Submit</Button>
</div>

View File

@ -0,0 +1,22 @@
package database
import (
"log"
"os"
"time"
"gorm.io/gorm/logger"
)
func NewLogger() logger.Interface {
return logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Silent, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: true, // Disable color
},
)
}

View File

@ -7,7 +7,9 @@ import (
func NewPostgresConnection(user, pass, host, port, name string) (*gorm.DB, error) {
dsn := "host=" + host + " user=" + user + " password=" + pass + " dbname=" + name + " port=" + port + " sslmode=disable TimeZone=UTC"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: NewLogger(),
})
if err != nil {
return nil, err
}

View File

@ -24,7 +24,9 @@ func NewSQLiteConnection(name string) (*gorm.DB, error) {
// Open database
dbPath := filepath.Join(settingsPath, name)
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
Logger: NewLogger(),
})
if err != nil {
return nil, err
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"log"
"net/http"
"time"
@ -47,6 +48,7 @@ func (h *ItemHandler) GetItems(ctx context.Context, req *connect.Request[itemv1.
// Filters
sql := h.db.Where("user_id = ?", userid)
if req.Msg.Start != nil {
log.Println(req.Msg.Start)
sql = sql.Where("added >= ?", req.Msg.Start.AsTime())
}
if req.Msg.End != nil {

View File

@ -151,6 +151,7 @@ func (h *UserHandler) UpdateProfilePicture(ctx context.Context, req *connect.Req
// Update user profile picture
fid := uint(file.ID)
user.ProfilePictureID = &fid
user.ProfilePicture = &file
if err := h.db.Save(&user).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}