From 51107bae59c618ae75e4f8750fbbba8c90d4596d Mon Sep 17 00:00:00 2001
From: neon <18559658+neonsolstice@users.noreply.github.com>
Date: Mon, 22 Dec 2025 17:03:11 -0600
Subject: [PATCH] feat(client): add theming system, shadcn-vue UI, and sidebar
---
client/index.html | 21 +-
client/package-lock.json | 231 ++++++++++++-
client/package.json | 4 +
client/src/App.vue | 6 +
client/src/assets/main.css | 310 ++++++++++++++----
client/src/components/AccentPicker.vue | 23 ++
client/src/components/AppHeader.vue | 153 +++++++++
client/src/components/AppSidebar.vue | 77 +++++
client/src/components/RadiusPicker.vue | 28 ++
client/src/components/ThemeToggle.vue | 17 +
client/src/components/ui/badge/Badge.vue | 26 ++
client/src/components/ui/badge/index.ts | 26 ++
client/src/components/ui/button/Button.vue | 31 ++
client/src/components/ui/button/index.ts | 38 +++
.../ui/dropdown-menu/DropdownMenu.vue | 19 ++
.../DropdownMenuCheckboxItem.vue | 39 +++
.../ui/dropdown-menu/DropdownMenuContent.vue | 39 +++
.../ui/dropdown-menu/DropdownMenuGroup.vue | 15 +
.../ui/dropdown-menu/DropdownMenuItem.vue | 31 ++
.../ui/dropdown-menu/DropdownMenuLabel.vue | 23 ++
.../dropdown-menu/DropdownMenuRadioGroup.vue | 21 ++
.../dropdown-menu/DropdownMenuRadioItem.vue | 40 +++
.../dropdown-menu/DropdownMenuSeparator.vue | 23 ++
.../ui/dropdown-menu/DropdownMenuShortcut.vue | 17 +
.../ui/dropdown-menu/DropdownMenuSub.vue | 18 +
.../dropdown-menu/DropdownMenuSubContent.vue | 27 ++
.../dropdown-menu/DropdownMenuSubTrigger.vue | 31 ++
.../ui/dropdown-menu/DropdownMenuTrigger.vue | 17 +
.../src/components/ui/dropdown-menu/index.ts | 16 +
client/src/components/ui/input/Input.vue | 33 ++
client/src/components/ui/input/index.ts | 1 +
client/src/components/ui/popover/Popover.vue | 19 ++
.../components/ui/popover/PopoverAnchor.vue | 15 +
.../components/ui/popover/PopoverContent.vue | 45 +++
.../components/ui/popover/PopoverTrigger.vue | 15 +
client/src/components/ui/popover/index.ts | 4 +
.../src/components/ui/separator/Separator.vue | 29 ++
client/src/components/ui/separator/index.ts | 1 +
client/src/components/ui/sheet/Sheet.vue | 19 ++
client/src/components/ui/sheet/SheetClose.vue | 15 +
.../src/components/ui/sheet/SheetContent.vue | 62 ++++
.../components/ui/sheet/SheetDescription.vue | 21 ++
.../src/components/ui/sheet/SheetFooter.vue | 16 +
.../src/components/ui/sheet/SheetHeader.vue | 15 +
.../src/components/ui/sheet/SheetOverlay.vue | 21 ++
client/src/components/ui/sheet/SheetTitle.vue | 21 ++
.../src/components/ui/sheet/SheetTrigger.vue | 15 +
client/src/components/ui/sheet/index.ts | 8 +
client/src/components/ui/sidebar/Sidebar.vue | 96 ++++++
.../components/ui/sidebar/SidebarContent.vue | 18 +
.../components/ui/sidebar/SidebarFooter.vue | 18 +
.../components/ui/sidebar/SidebarGroup.vue | 18 +
.../ui/sidebar/SidebarGroupAction.vue | 27 ++
.../ui/sidebar/SidebarGroupContent.vue | 18 +
.../ui/sidebar/SidebarGroupLabel.vue | 25 ++
.../components/ui/sidebar/SidebarHeader.vue | 18 +
.../components/ui/sidebar/SidebarInput.vue | 22 ++
.../components/ui/sidebar/SidebarInset.vue | 21 ++
.../src/components/ui/sidebar/SidebarMenu.vue | 18 +
.../ui/sidebar/SidebarMenuAction.vue | 35 ++
.../ui/sidebar/SidebarMenuBadge.vue | 26 ++
.../ui/sidebar/SidebarMenuButton.vue | 48 +++
.../ui/sidebar/SidebarMenuButtonChild.vue | 36 ++
.../components/ui/sidebar/SidebarMenuItem.vue | 18 +
.../ui/sidebar/SidebarMenuSkeleton.vue | 35 ++
.../components/ui/sidebar/SidebarMenuSub.vue | 22 ++
.../ui/sidebar/SidebarMenuSubButton.vue | 36 ++
.../ui/sidebar/SidebarMenuSubItem.vue | 18 +
.../components/ui/sidebar/SidebarProvider.vue | 82 +++++
.../src/components/ui/sidebar/SidebarRail.vue | 33 ++
.../ui/sidebar/SidebarSeparator.vue | 19 ++
.../components/ui/sidebar/SidebarTrigger.vue | 27 ++
client/src/components/ui/sidebar/index.ts | 60 ++++
client/src/components/ui/sidebar/utils.ts | 19 ++
.../src/components/ui/skeleton/Skeleton.vue | 17 +
client/src/components/ui/skeleton/index.ts | 1 +
client/src/components/ui/tooltip/Tooltip.vue | 19 ++
.../components/ui/tooltip/TooltipContent.vue | 34 ++
.../components/ui/tooltip/TooltipProvider.vue | 14 +
.../components/ui/tooltip/TooltipTrigger.vue | 15 +
client/src/components/ui/tooltip/index.ts | 4 +
client/src/stores/theme.ts | 91 +++++
client/src/views/HomeView.vue | 78 +++--
server/src/db/migrations/meta/_journal.json | 2 +-
84 files changed, 2723 insertions(+), 107 deletions(-)
create mode 100644 client/src/components/AccentPicker.vue
create mode 100644 client/src/components/AppHeader.vue
create mode 100644 client/src/components/AppSidebar.vue
create mode 100644 client/src/components/RadiusPicker.vue
create mode 100644 client/src/components/ThemeToggle.vue
create mode 100644 client/src/components/ui/badge/Badge.vue
create mode 100644 client/src/components/ui/badge/index.ts
create mode 100644 client/src/components/ui/button/Button.vue
create mode 100644 client/src/components/ui/button/index.ts
create mode 100644 client/src/components/ui/dropdown-menu/DropdownMenu.vue
create mode 100644 client/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
create mode 100644 client/src/components/ui/dropdown-menu/DropdownMenuContent.vue
create mode 100644 client/src/components/ui/dropdown-menu/DropdownMenuGroup.vue
create mode 100644 client/src/components/ui/dropdown-menu/DropdownMenuItem.vue
create mode 100644 client/src/components/ui/dropdown-menu/DropdownMenuLabel.vue
create mode 100644 client/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
create mode 100644 client/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
create mode 100644 client/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue
create mode 100644 client/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue
create mode 100644 client/src/components/ui/dropdown-menu/DropdownMenuSub.vue
create mode 100644 client/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue
create mode 100644 client/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
create mode 100644 client/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue
create mode 100644 client/src/components/ui/dropdown-menu/index.ts
create mode 100644 client/src/components/ui/input/Input.vue
create mode 100644 client/src/components/ui/input/index.ts
create mode 100644 client/src/components/ui/popover/Popover.vue
create mode 100644 client/src/components/ui/popover/PopoverAnchor.vue
create mode 100644 client/src/components/ui/popover/PopoverContent.vue
create mode 100644 client/src/components/ui/popover/PopoverTrigger.vue
create mode 100644 client/src/components/ui/popover/index.ts
create mode 100644 client/src/components/ui/separator/Separator.vue
create mode 100644 client/src/components/ui/separator/index.ts
create mode 100644 client/src/components/ui/sheet/Sheet.vue
create mode 100644 client/src/components/ui/sheet/SheetClose.vue
create mode 100644 client/src/components/ui/sheet/SheetContent.vue
create mode 100644 client/src/components/ui/sheet/SheetDescription.vue
create mode 100644 client/src/components/ui/sheet/SheetFooter.vue
create mode 100644 client/src/components/ui/sheet/SheetHeader.vue
create mode 100644 client/src/components/ui/sheet/SheetOverlay.vue
create mode 100644 client/src/components/ui/sheet/SheetTitle.vue
create mode 100644 client/src/components/ui/sheet/SheetTrigger.vue
create mode 100644 client/src/components/ui/sheet/index.ts
create mode 100644 client/src/components/ui/sidebar/Sidebar.vue
create mode 100644 client/src/components/ui/sidebar/SidebarContent.vue
create mode 100644 client/src/components/ui/sidebar/SidebarFooter.vue
create mode 100644 client/src/components/ui/sidebar/SidebarGroup.vue
create mode 100644 client/src/components/ui/sidebar/SidebarGroupAction.vue
create mode 100644 client/src/components/ui/sidebar/SidebarGroupContent.vue
create mode 100644 client/src/components/ui/sidebar/SidebarGroupLabel.vue
create mode 100644 client/src/components/ui/sidebar/SidebarHeader.vue
create mode 100644 client/src/components/ui/sidebar/SidebarInput.vue
create mode 100644 client/src/components/ui/sidebar/SidebarInset.vue
create mode 100644 client/src/components/ui/sidebar/SidebarMenu.vue
create mode 100644 client/src/components/ui/sidebar/SidebarMenuAction.vue
create mode 100644 client/src/components/ui/sidebar/SidebarMenuBadge.vue
create mode 100644 client/src/components/ui/sidebar/SidebarMenuButton.vue
create mode 100644 client/src/components/ui/sidebar/SidebarMenuButtonChild.vue
create mode 100644 client/src/components/ui/sidebar/SidebarMenuItem.vue
create mode 100644 client/src/components/ui/sidebar/SidebarMenuSkeleton.vue
create mode 100644 client/src/components/ui/sidebar/SidebarMenuSub.vue
create mode 100644 client/src/components/ui/sidebar/SidebarMenuSubButton.vue
create mode 100644 client/src/components/ui/sidebar/SidebarMenuSubItem.vue
create mode 100644 client/src/components/ui/sidebar/SidebarProvider.vue
create mode 100644 client/src/components/ui/sidebar/SidebarRail.vue
create mode 100644 client/src/components/ui/sidebar/SidebarSeparator.vue
create mode 100644 client/src/components/ui/sidebar/SidebarTrigger.vue
create mode 100644 client/src/components/ui/sidebar/index.ts
create mode 100644 client/src/components/ui/sidebar/utils.ts
create mode 100644 client/src/components/ui/skeleton/Skeleton.vue
create mode 100644 client/src/components/ui/skeleton/index.ts
create mode 100644 client/src/components/ui/tooltip/Tooltip.vue
create mode 100644 client/src/components/ui/tooltip/TooltipContent.vue
create mode 100644 client/src/components/ui/tooltip/TooltipProvider.vue
create mode 100644 client/src/components/ui/tooltip/TooltipTrigger.vue
create mode 100644 client/src/components/ui/tooltip/index.ts
create mode 100644 client/src/stores/theme.ts
diff --git a/client/index.html b/client/index.html
index 9e5fc8f06..f418ffb15 100644
--- a/client/index.html
+++ b/client/index.html
@@ -1,10 +1,21 @@
-
+
-
-
-
- Vite App
+
+
+
+ Projectx
+
+
diff --git a/client/package-lock.json b/client/package-lock.json
index 3260cfbe6..7be71238e 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -8,11 +8,15 @@
"name": "client",
"version": "0.0.0",
"dependencies": {
+ "@fontsource-variable/fraunces": "^5.2.9",
+ "@fontsource-variable/inter": "^5.2.8",
"@tailwindcss/vite": "^4.2.2",
+ "@vueuse/core": "^14.2.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.577.0",
"pinia": "^3.0.4",
+ "reka-ui": "^2.9.2",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2",
"vue": "^3.5.29",
@@ -1268,6 +1272,86 @@
}
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT"
+ },
+ "node_modules/@floating-ui/vue": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.11.tgz",
+ "integrity": "sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.6",
+ "@floating-ui/utils": "^0.2.11",
+ "vue-demi": ">=0.13.0"
+ }
+ },
+ "node_modules/@floating-ui/vue/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@fontsource-variable/fraunces": {
+ "version": "5.2.9",
+ "resolved": "https://registry.npmjs.org/@fontsource-variable/fraunces/-/fraunces-5.2.9.tgz",
+ "integrity": "sha512-Y6IjunlN9Ni723np+GIgAaKzCDBrPRrqNi01TZxHs5wtHYROWFM9W6yiT+/gGwSjWIRD18oX17kD/BRWekc/Lw==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
+ "node_modules/@fontsource-variable/inter": {
+ "version": "5.2.8",
+ "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.8.tgz",
+ "integrity": "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1320,6 +1404,24 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@internationalized/date": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz",
+ "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ }
+ },
+ "node_modules/@internationalized/number": {
+ "version": "3.6.5",
+ "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.5.tgz",
+ "integrity": "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ }
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -2108,6 +2210,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@swc/helpers": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
+ "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
"node_modules/@tailwindcss/node": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
@@ -2365,6 +2476,32 @@
"vite": "^5.2.0 || ^6 || ^7 || ^8"
}
},
+ "node_modules/@tanstack/virtual-core": {
+ "version": "3.13.23",
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz",
+ "integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/vue-virtual": {
+ "version": "3.13.23",
+ "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.23.tgz",
+ "integrity": "sha512-b5jPluAR6U3eOq6GWAYSpj3ugnAIZgGR0e6aGAgyRse0Yu6MVQQ0ZWm9SArSXWtageogn6bkVD8D//c4IjW3xQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/virtual-core": "3.13.23"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "vue": "^2.7.0 || ^3.0.0"
+ }
+ },
"node_modules/@tsconfig/node24": {
"version": "24.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node24/-/node24-24.0.4.tgz",
@@ -2447,6 +2584,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.21",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+ "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz",
@@ -3237,6 +3380,44 @@
}
}
},
+ "node_modules/@vueuse/core": {
+ "version": "14.2.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz",
+ "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.21",
+ "@vueuse/metadata": "14.2.1",
+ "@vueuse/shared": "14.2.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "14.2.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz",
+ "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "14.2.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz",
+ "integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
"node_modules/abbrev": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
@@ -3339,6 +3520,18 @@
"node": ">=14"
}
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -3795,6 +3988,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/defu": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
+ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
+ "license": "MIT"
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -5498,7 +5697,6 @@
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/open": {
@@ -5911,6 +6109,31 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/reka-ui": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.9.2.tgz",
+ "integrity": "sha512-/t4e6y1hcG+uDuRfpg6tbMz3uUEvRzNco6NeYTufoJeUghy5Iosxos5YL/p+ieAsid84sdMX9OrgDqpEuCJhBw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.6.13",
+ "@floating-ui/vue": "^1.1.6",
+ "@internationalized/date": "^3.5.0",
+ "@internationalized/number": "^3.5.0",
+ "@tanstack/vue-virtual": "^3.12.0",
+ "@vueuse/core": "^14.1.0",
+ "@vueuse/shared": "^14.1.0",
+ "aria-hidden": "^1.2.4",
+ "defu": "^6.1.4",
+ "ohash": "^2.0.11"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/zernonia"
+ },
+ "peerDependencies": {
+ "vue": ">= 3.4.0"
+ }
+ },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -6460,6 +6683,12 @@
"typescript": ">=4.8.4"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/tw-animate-css": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
diff --git a/client/package.json b/client/package.json
index 343f144cb..996a92bc4 100644
--- a/client/package.json
+++ b/client/package.json
@@ -16,11 +16,15 @@
"format": "prettier --write --experimental-cli src/"
},
"dependencies": {
+ "@fontsource-variable/fraunces": "^5.2.9",
+ "@fontsource-variable/inter": "^5.2.8",
"@tailwindcss/vite": "^4.1.18",
+ "@vueuse/core": "^14.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.562.0",
"pinia": "^3.0.4",
+ "reka-ui": "^2.6.1",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"vue": "^3.5.26",
diff --git a/client/src/App.vue b/client/src/App.vue
index 7c2aa3f3e..7c6a2e92a 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -1,3 +1,9 @@
+
+
diff --git a/client/src/assets/main.css b/client/src/assets/main.css
index e481200a0..e7af7c745 100644
--- a/client/src/assets/main.css
+++ b/client/src/assets/main.css
@@ -1,127 +1,287 @@
-@import "tailwindcss";
-@import "tw-animate-css";
+@import '@fontsource-variable/inter';
+@import '@fontsource-variable/fraunces';
+@import 'tailwindcss';
+@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
/* ── Theme tokens ───────────────────────────────────────────
- All design decisions live here. Change a value here and
- it propagates everywhere. Never hardcode colours in components.
+ Single source of truth. Never hardcode values in components.
+
+ Architecture:
+ - --tint-h / --tint-c control the hue of ALL neutral surfaces.
+ Changing just --tint-h shifts the entire palette toward that hue.
+ - Accent classes override --tint-h (surfaces) + --primary (interactive).
+ - Radius classes override --radius.
+ - Dark mode overrides lightness + slightly raises --tint-c.
────────────────────────────────────────────────────────────── */
:root {
- --background: oklch(1 0 0);
- --foreground: oklch(0.145 0 0);
+ /* Tint controls — warm amber base */
+ --tint-h: 80;
+ --tint-c: 0.006;
- --card: oklch(1 0 0);
- --card-foreground: oklch(0.145 0 0);
+ /* Colour roles */
+ --background: oklch(0.99 var(--tint-c) var(--tint-h));
+ --foreground: oklch(0.145 0.015 var(--tint-h));
- --primary: oklch(0.205 0 0);
- --primary-foreground: oklch(0.985 0 0);
+ --card: oklch(0.975 var(--tint-c) var(--tint-h));
+ --card-foreground: oklch(0.145 0.015 var(--tint-h));
- --secondary: oklch(0.97 0 0);
- --secondary-foreground: oklch(0.205 0 0);
+ --popover: oklch(0.975 var(--tint-c) var(--tint-h));
+ --popover-foreground: oklch(0.145 0.015 var(--tint-h));
- --muted: oklch(0.97 0 0);
- --muted-foreground: oklch(0.556 0 0);
+ --primary: oklch(0.21 0.015 var(--tint-h));
+ --primary-foreground: oklch(0.985 0.004 var(--tint-h));
- --accent: oklch(0.97 0 0);
- --accent-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.955 var(--tint-c) var(--tint-h));
+ --secondary-foreground: oklch(0.21 0.015 var(--tint-h));
+
+ --muted: oklch(0.955 var(--tint-c) var(--tint-h));
+ --muted-foreground: oklch(0.52 0.008 var(--tint-h));
+
+ --accent: oklch(0.955 var(--tint-c) var(--tint-h));
+ --accent-foreground: oklch(0.21 0.015 var(--tint-h));
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
- --border: oklch(0.922 0 0);
- --input: oklch(0.922 0 0);
- --ring: oklch(0.708 0 0);
+ --border: oklch(0.878 0.01 var(--tint-h));
+ --input: oklch(0.878 0.01 var(--tint-h));
+ --ring: oklch(0.65 0.01 var(--tint-h));
- --radius: 0.625rem;
-
- --popover: oklch(1 0 0);
-
- --popover-foreground: oklch(0.145 0 0);
+ --sidebar: oklch(0.965 var(--tint-c) var(--tint-h));
+ --sidebar-foreground: oklch(0.145 0.015 var(--tint-h));
+ --sidebar-primary: oklch(0.21 0.015 var(--tint-h));
+ --sidebar-primary-foreground: oklch(0.985 0.004 var(--tint-h));
+ --sidebar-accent: oklch(0.955 var(--tint-c) var(--tint-h));
+ --sidebar-accent-foreground: oklch(0.21 0.015 var(--tint-h));
+ --sidebar-border: oklch(0.878 0.01 var(--tint-h));
+ --sidebar-ring: oklch(0.65 0.01 var(--tint-h));
--chart-1: oklch(0.646 0.222 41.116);
-
--chart-2: oklch(0.6 0.118 184.704);
-
--chart-3: oklch(0.398 0.07 227.392);
-
--chart-4: oklch(0.828 0.189 84.429);
-
--chart-5: oklch(0.769 0.188 70.08);
- --sidebar: oklch(0.985 0 0);
+ /* Elevation surfaces */
+ --surface-1: oklch(0.975 var(--tint-c) var(--tint-h));
+ --surface-2: oklch(0.955 var(--tint-c) var(--tint-h));
+ --surface-3: oklch(0.93 0.01 var(--tint-h));
+ --surface-4: oklch(0.91 0.012 var(--tint-h));
- --sidebar-foreground: oklch(0.145 0 0);
+ /* Shadows */
+ --elevation-xs: 0 1px 2px oklch(0 0 0 / 0.05);
+ --elevation-sm: 0 1px 3px oklch(0 0 0 / 0.08), 0 1px 2px oklch(0 0 0 / 0.06);
+ --elevation-md: 0 4px 6px -1px oklch(0 0 0 / 0.08), 0 2px 4px -2px oklch(0 0 0 / 0.06);
+ --elevation-lg: 0 10px 15px -3px oklch(0 0 0 / 0.08), 0 4px 6px -4px oklch(0 0 0 / 0.05);
+ --elevation-xl: 0 20px 25px -5px oklch(0 0 0 / 0.1), 0 8px 10px -6px oklch(0 0 0 / 0.06);
+ --elevation-2xl: 0 25px 50px -12px oklch(0 0 0 / 0.18);
- --sidebar-primary: oklch(0.205 0 0);
-
- --sidebar-primary-foreground: oklch(0.985 0 0);
-
- --sidebar-accent: oklch(0.97 0 0);
-
- --sidebar-accent-foreground: oklch(0.205 0 0);
-
- --sidebar-border: oklch(0.922 0 0);
-
- --sidebar-ring: oklch(0.708 0 0);
+ /* Shape */
+ --radius: 0.625rem;
}
.dark {
- --background: oklch(0.145 0 0);
- --foreground: oklch(0.985 0 0);
+ --tint-c: 0.01;
- --card: oklch(0.205 0 0);
- --card-foreground: oklch(0.985 0 0);
+ --background: oklch(0.145 var(--tint-c) var(--tint-h));
+ --foreground: oklch(0.985 0.004 var(--tint-h));
- --primary: oklch(0.922 0 0);
- --primary-foreground: oklch(0.205 0 0);
+ --card: oklch(0.205 0.012 var(--tint-h));
+ --card-foreground: oklch(0.985 0.004 var(--tint-h));
- --secondary: oklch(0.269 0 0);
- --secondary-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0.012 var(--tint-h));
+ --popover-foreground: oklch(0.985 0.004 var(--tint-h));
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
+ --primary: oklch(0.91 var(--tint-c) var(--tint-h));
+ --primary-foreground: oklch(0.205 0.012 var(--tint-h));
- --accent: oklch(0.269 0 0);
- --accent-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.265 0.012 var(--tint-h));
+ --secondary-foreground: oklch(0.985 0.004 var(--tint-h));
+
+ --muted: oklch(0.265 0.012 var(--tint-h));
+ --muted-foreground: oklch(0.68 0.008 var(--tint-h));
+
+ --accent: oklch(0.265 0.012 var(--tint-h));
+ --accent-foreground: oklch(0.985 0.004 var(--tint-h));
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
- --ring: oklch(0.556 0 0);
- --popover: oklch(0.205 0 0);
- --popover-foreground: oklch(0.985 0 0);
+ --ring: oklch(0.52 0.008 var(--tint-h));
+
+ --sidebar: oklch(0.205 0.012 var(--tint-h));
+ --sidebar-foreground: oklch(0.985 0.004 var(--tint-h));
+ --sidebar-primary: oklch(0.91 var(--tint-c) var(--tint-h));
+ --sidebar-primary-foreground: oklch(0.205 0.012 var(--tint-h));
+ --sidebar-accent: oklch(0.265 0.012 var(--tint-h));
+ --sidebar-accent-foreground: oklch(0.985 0.004 var(--tint-h));
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.52 0.008 var(--tint-h));
+
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
- --sidebar: oklch(0.205 0 0);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
+
+ /* Elevation surfaces */
+ --surface-1: oklch(0.205 0.012 var(--tint-h));
+ --surface-2: oklch(0.235 0.012 var(--tint-h));
+ --surface-3: oklch(0.265 0.012 var(--tint-h));
+ --surface-4: oklch(0.295 0.012 var(--tint-h));
+
+ /* Shadows */
+ --elevation-xs: 0 1px 2px oklch(0 0 0 / 0.4);
+ --elevation-sm: 0 1px 3px oklch(0 0 0 / 0.5), 0 1px 2px oklch(0 0 0 / 0.4);
+ --elevation-md: 0 4px 8px -1px oklch(0 0 0 / 0.5), 0 2px 4px -2px oklch(0 0 0 / 0.4);
+ --elevation-lg: 0 10px 20px -3px oklch(0 0 0 / 0.5), 0 4px 8px -4px oklch(0 0 0 / 0.4);
+ --elevation-xl: 0 20px 30px -5px oklch(0 0 0 / 0.6), 0 8px 12px -6px oklch(0 0 0 / 0.4);
+ --elevation-2xl: 0 25px 50px -12px oklch(0 0 0 / 0.7);
+}
+
+/* ── Accent presets ──────────────────────────────────────────
+ Only --tint-h + saturated --primary / --ring need overriding.
+ All neutral surfaces inherit the new hue automatically.
+────────────────────────────────────────────────────────────── */
+html.accent-violet {
+ --tint-h: 292;
+ --primary: oklch(0.491 0.27 292);
+ --primary-foreground: oklch(0.985 0 0);
+ --ring: oklch(0.491 0.27 292);
+ --sidebar-primary: oklch(0.491 0.27 292);
--sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.269 0 0);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.556 0 0);
+}
+html.accent-violet.dark {
+ --primary: oklch(0.72 0.23 292);
+ --primary-foreground: oklch(0.145 0 0);
+ --ring: oklch(0.72 0.23 292);
+ --sidebar-primary: oklch(0.72 0.23 292);
+ --sidebar-primary-foreground: oklch(0.145 0 0);
}
-* {
- border-color: var(--border);
+html.accent-blue {
+ --tint-h: 263;
+ --primary: oklch(0.487 0.25 263);
+ --primary-foreground: oklch(0.985 0 0);
+ --ring: oklch(0.487 0.25 263);
+ --sidebar-primary: oklch(0.487 0.25 263);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+}
+html.accent-blue.dark {
+ --primary: oklch(0.72 0.2 263);
+ --primary-foreground: oklch(0.145 0 0);
+ --ring: oklch(0.72 0.2 263);
+ --sidebar-primary: oklch(0.72 0.2 263);
+ --sidebar-primary-foreground: oklch(0.145 0 0);
}
-body {
- background-color: var(--background);
- color: var(--foreground);
+html.accent-cyan {
+ --tint-h: 197;
+ --primary: oklch(0.52 0.2 197);
+ --primary-foreground: oklch(0.985 0 0);
+ --ring: oklch(0.52 0.2 197);
+ --sidebar-primary: oklch(0.52 0.2 197);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+}
+html.accent-cyan.dark {
+ --primary: oklch(0.75 0.17 197);
+ --primary-foreground: oklch(0.145 0 0);
+ --ring: oklch(0.75 0.17 197);
+ --sidebar-primary: oklch(0.75 0.17 197);
+ --sidebar-primary-foreground: oklch(0.145 0 0);
}
+html.accent-green {
+ --tint-h: 142;
+ --primary: oklch(0.527 0.18 142);
+ --primary-foreground: oklch(0.985 0 0);
+ --ring: oklch(0.527 0.18 142);
+ --sidebar-primary: oklch(0.527 0.18 142);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+}
+html.accent-green.dark {
+ --primary: oklch(0.72 0.18 142);
+ --primary-foreground: oklch(0.145 0 0);
+ --ring: oklch(0.72 0.18 142);
+ --sidebar-primary: oklch(0.72 0.18 142);
+ --sidebar-primary-foreground: oklch(0.145 0 0);
+}
+
+html.accent-amber {
+ --tint-h: 75;
+ --primary: oklch(0.72 0.17 75);
+ --primary-foreground: oklch(0.145 0 0);
+ --ring: oklch(0.72 0.17 75);
+ --sidebar-primary: oklch(0.72 0.17 75);
+ --sidebar-primary-foreground: oklch(0.145 0 0);
+}
+html.accent-amber.dark {
+ --primary: oklch(0.82 0.17 75);
+ --primary-foreground: oklch(0.145 0 0);
+ --ring: oklch(0.82 0.17 75);
+ --sidebar-primary: oklch(0.82 0.17 75);
+ --sidebar-primary-foreground: oklch(0.145 0 0);
+}
+
+html.accent-orange {
+ --tint-h: 42;
+ --primary: oklch(0.64 0.22 42);
+ --primary-foreground: oklch(0.985 0 0);
+ --ring: oklch(0.64 0.22 42);
+ --sidebar-primary: oklch(0.64 0.22 42);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+}
+html.accent-orange.dark {
+ --primary: oklch(0.78 0.18 42);
+ --primary-foreground: oklch(0.145 0 0);
+ --ring: oklch(0.78 0.18 42);
+ --sidebar-primary: oklch(0.78 0.18 42);
+ --sidebar-primary-foreground: oklch(0.145 0 0);
+}
+
+html.accent-rose {
+ --tint-h: 15;
+ --primary: oklch(0.57 0.24 15);
+ --primary-foreground: oklch(0.985 0 0);
+ --ring: oklch(0.57 0.24 15);
+ --sidebar-primary: oklch(0.57 0.24 15);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+}
+html.accent-rose.dark {
+ --primary: oklch(0.73 0.2 15);
+ --primary-foreground: oklch(0.145 0 0);
+ --ring: oklch(0.73 0.2 15);
+ --sidebar-primary: oklch(0.73 0.2 15);
+ --sidebar-primary-foreground: oklch(0.145 0 0);
+}
+
+/* ── Radius presets ──────────────────────────────────────────
+ Applied as a class on . Default is :root value.
+────────────────────────────────────────────────────────────── */
+html.radius-sharp {
+ --radius: 0rem;
+}
+html.radius-rounded {
+ --radius: 1.25rem;
+}
+
+/* ── Tailwind theme bridge ───────────────────────────────────
+ Maps CSS variables → Tailwind utility classes.
+────────────────────────────────────────────────────────────── */
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
+
+ --font-sans: 'Inter Variable', ui-sans-serif, system-ui, -apple-system, sans-serif;
+ --font-serif: 'Fraunces Variable', ui-serif, Georgia, serif;
+ --font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
+
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
@@ -137,6 +297,7 @@ body {
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
@@ -153,6 +314,17 @@ body {
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
+ --color-surface-1: var(--surface-1);
+ --color-surface-2: var(--surface-2);
+ --color-surface-3: var(--surface-3);
+ --color-surface-4: var(--surface-4);
+
+ --shadow-xs: var(--elevation-xs);
+ --shadow-sm: var(--elevation-sm);
+ --shadow-md: var(--elevation-md);
+ --shadow-lg: var(--elevation-lg);
+ --shadow-xl: var(--elevation-xl);
+ --shadow-2xl: var(--elevation-2xl);
}
@layer base {
@@ -160,6 +332,6 @@ body {
@apply border-border outline-ring/50;
}
body {
- @apply bg-background text-foreground;
+ @apply bg-background text-foreground font-sans antialiased;
}
}
diff --git a/client/src/components/AccentPicker.vue b/client/src/components/AccentPicker.vue
new file mode 100644
index 000000000..62f791fa8
--- /dev/null
+++ b/client/src/components/AccentPicker.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/AppHeader.vue b/client/src/components/AppHeader.vue
new file mode 100644
index 000000000..964803daf
--- /dev/null
+++ b/client/src/components/AppHeader.vue
@@ -0,0 +1,153 @@
+
+
+
+
+
diff --git a/client/src/components/AppSidebar.vue b/client/src/components/AppSidebar.vue
new file mode 100644
index 000000000..c1632acb1
--- /dev/null
+++ b/client/src/components/AppSidebar.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/RadiusPicker.vue b/client/src/components/RadiusPicker.vue
new file mode 100644
index 000000000..11077b937
--- /dev/null
+++ b/client/src/components/RadiusPicker.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ThemeToggle.vue b/client/src/components/ThemeToggle.vue
new file mode 100644
index 000000000..4c3bc7acf
--- /dev/null
+++ b/client/src/components/ThemeToggle.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/client/src/components/ui/badge/Badge.vue b/client/src/components/ui/badge/Badge.vue
new file mode 100644
index 000000000..d894dfe08
--- /dev/null
+++ b/client/src/components/ui/badge/Badge.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/badge/index.ts b/client/src/components/ui/badge/index.ts
new file mode 100644
index 000000000..bbc0dfa3b
--- /dev/null
+++ b/client/src/components/ui/badge/index.ts
@@ -0,0 +1,26 @@
+import type { VariantProps } from "class-variance-authority"
+import { cva } from "class-variance-authority"
+
+export { default as Badge } from "./Badge.vue"
+
+export const badgeVariants = cva(
+ "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
+ destructive:
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+)
+export type BadgeVariants = VariantProps
diff --git a/client/src/components/ui/button/Button.vue b/client/src/components/ui/button/Button.vue
new file mode 100644
index 000000000..3763470a1
--- /dev/null
+++ b/client/src/components/ui/button/Button.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/button/index.ts b/client/src/components/ui/button/index.ts
new file mode 100644
index 000000000..26e2c559c
--- /dev/null
+++ b/client/src/components/ui/button/index.ts
@@ -0,0 +1,38 @@
+import type { VariantProps } from "class-variance-authority"
+import { cva } from "class-variance-authority"
+
+export { default as Button } from "./Button.vue"
+
+export const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ "default": "h-9 px-4 py-2 has-[>svg]:px-3",
+ "sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ "lg": "h-10 rounded-md px-6 has-[>svg]:px-4",
+ "icon": "size-9",
+ "icon-sm": "size-8",
+ "icon-lg": "size-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+)
+export type ButtonVariants = VariantProps
diff --git a/client/src/components/ui/dropdown-menu/DropdownMenu.vue b/client/src/components/ui/dropdown-menu/DropdownMenu.vue
new file mode 100644
index 000000000..e1c9ee304
--- /dev/null
+++ b/client/src/components/ui/dropdown-menu/DropdownMenu.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue b/client/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
new file mode 100644
index 000000000..1253078dc
--- /dev/null
+++ b/client/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/dropdown-menu/DropdownMenuContent.vue b/client/src/components/ui/dropdown-menu/DropdownMenuContent.vue
new file mode 100644
index 000000000..7c4301413
--- /dev/null
+++ b/client/src/components/ui/dropdown-menu/DropdownMenuContent.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/dropdown-menu/DropdownMenuGroup.vue b/client/src/components/ui/dropdown-menu/DropdownMenuGroup.vue
new file mode 100644
index 000000000..da634ec09
--- /dev/null
+++ b/client/src/components/ui/dropdown-menu/DropdownMenuGroup.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/dropdown-menu/DropdownMenuItem.vue b/client/src/components/ui/dropdown-menu/DropdownMenuItem.vue
new file mode 100644
index 000000000..f56cae3e7
--- /dev/null
+++ b/client/src/components/ui/dropdown-menu/DropdownMenuItem.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/dropdown-menu/DropdownMenuLabel.vue b/client/src/components/ui/dropdown-menu/DropdownMenuLabel.vue
new file mode 100644
index 000000000..8bca83c43
--- /dev/null
+++ b/client/src/components/ui/dropdown-menu/DropdownMenuLabel.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue b/client/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
new file mode 100644
index 000000000..fe82cadd7
--- /dev/null
+++ b/client/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue b/client/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
new file mode 100644
index 000000000..e03c40c5a
--- /dev/null
+++ b/client/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue b/client/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue
new file mode 100644
index 000000000..1b936c392
--- /dev/null
+++ b/client/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/client/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue b/client/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue
new file mode 100644
index 000000000..60be75cc9
--- /dev/null
+++ b/client/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/dropdown-menu/DropdownMenuSub.vue b/client/src/components/ui/dropdown-menu/DropdownMenuSub.vue
new file mode 100644
index 000000000..7472e77f5
--- /dev/null
+++ b/client/src/components/ui/dropdown-menu/DropdownMenuSub.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue b/client/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue
new file mode 100644
index 000000000..d7c6b087d
--- /dev/null
+++ b/client/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue b/client/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
new file mode 100644
index 000000000..090649116
--- /dev/null
+++ b/client/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue b/client/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue
new file mode 100644
index 000000000..75cd7472c
--- /dev/null
+++ b/client/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/dropdown-menu/index.ts b/client/src/components/ui/dropdown-menu/index.ts
new file mode 100644
index 000000000..955fe3aa0
--- /dev/null
+++ b/client/src/components/ui/dropdown-menu/index.ts
@@ -0,0 +1,16 @@
+export { default as DropdownMenu } from "./DropdownMenu.vue"
+
+export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue"
+export { default as DropdownMenuContent } from "./DropdownMenuContent.vue"
+export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue"
+export { default as DropdownMenuItem } from "./DropdownMenuItem.vue"
+export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue"
+export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue"
+export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue"
+export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue"
+export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue"
+export { default as DropdownMenuSub } from "./DropdownMenuSub.vue"
+export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue"
+export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue"
+export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue"
+export { DropdownMenuPortal } from "reka-ui"
diff --git a/client/src/components/ui/input/Input.vue b/client/src/components/ui/input/Input.vue
new file mode 100644
index 000000000..e5135c143
--- /dev/null
+++ b/client/src/components/ui/input/Input.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
diff --git a/client/src/components/ui/input/index.ts b/client/src/components/ui/input/index.ts
new file mode 100644
index 000000000..9976b86f5
--- /dev/null
+++ b/client/src/components/ui/input/index.ts
@@ -0,0 +1 @@
+export { default as Input } from "./Input.vue"
diff --git a/client/src/components/ui/popover/Popover.vue b/client/src/components/ui/popover/Popover.vue
new file mode 100644
index 000000000..4efdb98a0
--- /dev/null
+++ b/client/src/components/ui/popover/Popover.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/popover/PopoverAnchor.vue b/client/src/components/ui/popover/PopoverAnchor.vue
new file mode 100644
index 000000000..49e01dbc5
--- /dev/null
+++ b/client/src/components/ui/popover/PopoverAnchor.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/popover/PopoverContent.vue b/client/src/components/ui/popover/PopoverContent.vue
new file mode 100644
index 000000000..cf1e55c50
--- /dev/null
+++ b/client/src/components/ui/popover/PopoverContent.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/popover/PopoverTrigger.vue b/client/src/components/ui/popover/PopoverTrigger.vue
new file mode 100644
index 000000000..fd3b4974d
--- /dev/null
+++ b/client/src/components/ui/popover/PopoverTrigger.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/popover/index.ts b/client/src/components/ui/popover/index.ts
new file mode 100644
index 000000000..66edf89f6
--- /dev/null
+++ b/client/src/components/ui/popover/index.ts
@@ -0,0 +1,4 @@
+export { default as Popover } from "./Popover.vue"
+export { default as PopoverAnchor } from "./PopoverAnchor.vue"
+export { default as PopoverContent } from "./PopoverContent.vue"
+export { default as PopoverTrigger } from "./PopoverTrigger.vue"
diff --git a/client/src/components/ui/separator/Separator.vue b/client/src/components/ui/separator/Separator.vue
new file mode 100644
index 000000000..78d60eca1
--- /dev/null
+++ b/client/src/components/ui/separator/Separator.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
diff --git a/client/src/components/ui/separator/index.ts b/client/src/components/ui/separator/index.ts
new file mode 100644
index 000000000..440728730
--- /dev/null
+++ b/client/src/components/ui/separator/index.ts
@@ -0,0 +1 @@
+export { default as Separator } from "./Separator.vue"
diff --git a/client/src/components/ui/sheet/Sheet.vue b/client/src/components/ui/sheet/Sheet.vue
new file mode 100644
index 000000000..8522f84db
--- /dev/null
+++ b/client/src/components/ui/sheet/Sheet.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sheet/SheetClose.vue b/client/src/components/ui/sheet/SheetClose.vue
new file mode 100644
index 000000000..39a942c4d
--- /dev/null
+++ b/client/src/components/ui/sheet/SheetClose.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sheet/SheetContent.vue b/client/src/components/ui/sheet/SheetContent.vue
new file mode 100644
index 000000000..e0c4b8f66
--- /dev/null
+++ b/client/src/components/ui/sheet/SheetContent.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+ Close
+
+
+
+
diff --git a/client/src/components/ui/sheet/SheetDescription.vue b/client/src/components/ui/sheet/SheetDescription.vue
new file mode 100644
index 000000000..6c8ba0a99
--- /dev/null
+++ b/client/src/components/ui/sheet/SheetDescription.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sheet/SheetFooter.vue b/client/src/components/ui/sheet/SheetFooter.vue
new file mode 100644
index 000000000..5fcf751dd
--- /dev/null
+++ b/client/src/components/ui/sheet/SheetFooter.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sheet/SheetHeader.vue b/client/src/components/ui/sheet/SheetHeader.vue
new file mode 100644
index 000000000..b6305ab6c
--- /dev/null
+++ b/client/src/components/ui/sheet/SheetHeader.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sheet/SheetOverlay.vue b/client/src/components/ui/sheet/SheetOverlay.vue
new file mode 100644
index 000000000..220452a7d
--- /dev/null
+++ b/client/src/components/ui/sheet/SheetOverlay.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sheet/SheetTitle.vue b/client/src/components/ui/sheet/SheetTitle.vue
new file mode 100644
index 000000000..889ae54a2
--- /dev/null
+++ b/client/src/components/ui/sheet/SheetTitle.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sheet/SheetTrigger.vue b/client/src/components/ui/sheet/SheetTrigger.vue
new file mode 100644
index 000000000..41b121dba
--- /dev/null
+++ b/client/src/components/ui/sheet/SheetTrigger.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sheet/index.ts b/client/src/components/ui/sheet/index.ts
new file mode 100644
index 000000000..7c70e5d7e
--- /dev/null
+++ b/client/src/components/ui/sheet/index.ts
@@ -0,0 +1,8 @@
+export { default as Sheet } from "./Sheet.vue"
+export { default as SheetClose } from "./SheetClose.vue"
+export { default as SheetContent } from "./SheetContent.vue"
+export { default as SheetDescription } from "./SheetDescription.vue"
+export { default as SheetFooter } from "./SheetFooter.vue"
+export { default as SheetHeader } from "./SheetHeader.vue"
+export { default as SheetTitle } from "./SheetTitle.vue"
+export { default as SheetTrigger } from "./SheetTrigger.vue"
diff --git a/client/src/components/ui/sidebar/Sidebar.vue b/client/src/components/ui/sidebar/Sidebar.vue
new file mode 100644
index 000000000..b4699f874
--- /dev/null
+++ b/client/src/components/ui/sidebar/Sidebar.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarContent.vue b/client/src/components/ui/sidebar/SidebarContent.vue
new file mode 100644
index 000000000..bb6d9f1dd
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarContent.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarFooter.vue b/client/src/components/ui/sidebar/SidebarFooter.vue
new file mode 100644
index 000000000..20dd72f47
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarFooter.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarGroup.vue b/client/src/components/ui/sidebar/SidebarGroup.vue
new file mode 100644
index 000000000..fbb488739
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarGroup.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarGroupAction.vue b/client/src/components/ui/sidebar/SidebarGroupAction.vue
new file mode 100644
index 000000000..f5fa5eb0c
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarGroupAction.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarGroupContent.vue b/client/src/components/ui/sidebar/SidebarGroupContent.vue
new file mode 100644
index 000000000..06e1a92fe
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarGroupContent.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarGroupLabel.vue b/client/src/components/ui/sidebar/SidebarGroupLabel.vue
new file mode 100644
index 000000000..f5da1300d
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarGroupLabel.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarHeader.vue b/client/src/components/ui/sidebar/SidebarHeader.vue
new file mode 100644
index 000000000..79cf88332
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarHeader.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarInput.vue b/client/src/components/ui/sidebar/SidebarInput.vue
new file mode 100644
index 000000000..3afc5da73
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarInput.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarInset.vue b/client/src/components/ui/sidebar/SidebarInset.vue
new file mode 100644
index 000000000..f46b43a87
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarInset.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarMenu.vue b/client/src/components/ui/sidebar/SidebarMenu.vue
new file mode 100644
index 000000000..e1dd39ebc
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarMenu.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarMenuAction.vue b/client/src/components/ui/sidebar/SidebarMenuAction.vue
new file mode 100644
index 000000000..bdffdb7d8
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarMenuAction.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarMenuBadge.vue b/client/src/components/ui/sidebar/SidebarMenuBadge.vue
new file mode 100644
index 000000000..432161842
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarMenuBadge.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarMenuButton.vue b/client/src/components/ui/sidebar/SidebarMenuButton.vue
new file mode 100644
index 000000000..502d39598
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarMenuButton.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tooltip }}
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarMenuButtonChild.vue b/client/src/components/ui/sidebar/SidebarMenuButtonChild.vue
new file mode 100644
index 000000000..4b4ca3ba4
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarMenuButtonChild.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarMenuItem.vue b/client/src/components/ui/sidebar/SidebarMenuItem.vue
new file mode 100644
index 000000000..e2fda5b4d
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarMenuItem.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarMenuSkeleton.vue b/client/src/components/ui/sidebar/SidebarMenuSkeleton.vue
new file mode 100644
index 000000000..b33812c34
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarMenuSkeleton.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarMenuSub.vue b/client/src/components/ui/sidebar/SidebarMenuSub.vue
new file mode 100644
index 000000000..81c2dab9b
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarMenuSub.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarMenuSubButton.vue b/client/src/components/ui/sidebar/SidebarMenuSubButton.vue
new file mode 100644
index 000000000..457cfb634
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarMenuSubButton.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarMenuSubItem.vue b/client/src/components/ui/sidebar/SidebarMenuSubItem.vue
new file mode 100644
index 000000000..d4374f65d
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarMenuSubItem.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarProvider.vue b/client/src/components/ui/sidebar/SidebarProvider.vue
new file mode 100644
index 000000000..04ed975f9
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarProvider.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarRail.vue b/client/src/components/ui/sidebar/SidebarRail.vue
new file mode 100644
index 000000000..1045a55ce
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarRail.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarSeparator.vue b/client/src/components/ui/sidebar/SidebarSeparator.vue
new file mode 100644
index 000000000..91170bc4c
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarSeparator.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/SidebarTrigger.vue b/client/src/components/ui/sidebar/SidebarTrigger.vue
new file mode 100644
index 000000000..d934ccab7
--- /dev/null
+++ b/client/src/components/ui/sidebar/SidebarTrigger.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/client/src/components/ui/sidebar/index.ts b/client/src/components/ui/sidebar/index.ts
new file mode 100644
index 000000000..ef718a9ce
--- /dev/null
+++ b/client/src/components/ui/sidebar/index.ts
@@ -0,0 +1,60 @@
+import type { VariantProps } from "class-variance-authority"
+import type { HTMLAttributes } from "vue"
+import { cva } from "class-variance-authority"
+
+export interface SidebarProps {
+ side?: "left" | "right"
+ variant?: "sidebar" | "floating" | "inset"
+ collapsible?: "offcanvas" | "icon" | "none"
+ class?: HTMLAttributes["class"]
+}
+
+export { default as Sidebar } from "./Sidebar.vue"
+export { default as SidebarContent } from "./SidebarContent.vue"
+export { default as SidebarFooter } from "./SidebarFooter.vue"
+export { default as SidebarGroup } from "./SidebarGroup.vue"
+export { default as SidebarGroupAction } from "./SidebarGroupAction.vue"
+export { default as SidebarGroupContent } from "./SidebarGroupContent.vue"
+export { default as SidebarGroupLabel } from "./SidebarGroupLabel.vue"
+export { default as SidebarHeader } from "./SidebarHeader.vue"
+export { default as SidebarInput } from "./SidebarInput.vue"
+export { default as SidebarInset } from "./SidebarInset.vue"
+export { default as SidebarMenu } from "./SidebarMenu.vue"
+export { default as SidebarMenuAction } from "./SidebarMenuAction.vue"
+export { default as SidebarMenuBadge } from "./SidebarMenuBadge.vue"
+export { default as SidebarMenuButton } from "./SidebarMenuButton.vue"
+export { default as SidebarMenuItem } from "./SidebarMenuItem.vue"
+export { default as SidebarMenuSkeleton } from "./SidebarMenuSkeleton.vue"
+export { default as SidebarMenuSub } from "./SidebarMenuSub.vue"
+export { default as SidebarMenuSubButton } from "./SidebarMenuSubButton.vue"
+export { default as SidebarMenuSubItem } from "./SidebarMenuSubItem.vue"
+export { default as SidebarProvider } from "./SidebarProvider.vue"
+export { default as SidebarRail } from "./SidebarRail.vue"
+export { default as SidebarSeparator } from "./SidebarSeparator.vue"
+export { default as SidebarTrigger } from "./SidebarTrigger.vue"
+
+export { useSidebar } from "./utils"
+
+export const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+)
+
+export type SidebarMenuButtonVariants = VariantProps
diff --git a/client/src/components/ui/sidebar/utils.ts b/client/src/components/ui/sidebar/utils.ts
new file mode 100644
index 000000000..6e57eeb9b
--- /dev/null
+++ b/client/src/components/ui/sidebar/utils.ts
@@ -0,0 +1,19 @@
+import type { ComputedRef, Ref } from "vue"
+import { createContext } from "reka-ui"
+
+export const SIDEBAR_COOKIE_NAME = "sidebar_state"
+export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
+export const SIDEBAR_WIDTH = "16rem"
+export const SIDEBAR_WIDTH_MOBILE = "18rem"
+export const SIDEBAR_WIDTH_ICON = "3rem"
+export const SIDEBAR_KEYBOARD_SHORTCUT = "b"
+
+export const [useSidebar, provideSidebarContext] = createContext<{
+ state: ComputedRef<"expanded" | "collapsed">
+ open: Ref
+ setOpen: (value: boolean) => void
+ isMobile: Ref
+ openMobile: Ref
+ setOpenMobile: (value: boolean) => void
+ toggleSidebar: () => void
+}>("Sidebar")
diff --git a/client/src/components/ui/skeleton/Skeleton.vue b/client/src/components/ui/skeleton/Skeleton.vue
new file mode 100644
index 000000000..0dadcef9a
--- /dev/null
+++ b/client/src/components/ui/skeleton/Skeleton.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/client/src/components/ui/skeleton/index.ts b/client/src/components/ui/skeleton/index.ts
new file mode 100644
index 000000000..e5ce72c35
--- /dev/null
+++ b/client/src/components/ui/skeleton/index.ts
@@ -0,0 +1 @@
+export { default as Skeleton } from "./Skeleton.vue"
diff --git a/client/src/components/ui/tooltip/Tooltip.vue b/client/src/components/ui/tooltip/Tooltip.vue
new file mode 100644
index 000000000..2a393d6da
--- /dev/null
+++ b/client/src/components/ui/tooltip/Tooltip.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/tooltip/TooltipContent.vue b/client/src/components/ui/tooltip/TooltipContent.vue
new file mode 100644
index 000000000..c5d2df9cf
--- /dev/null
+++ b/client/src/components/ui/tooltip/TooltipContent.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/tooltip/TooltipProvider.vue b/client/src/components/ui/tooltip/TooltipProvider.vue
new file mode 100644
index 000000000..395927d52
--- /dev/null
+++ b/client/src/components/ui/tooltip/TooltipProvider.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/tooltip/TooltipTrigger.vue b/client/src/components/ui/tooltip/TooltipTrigger.vue
new file mode 100644
index 000000000..3332950e9
--- /dev/null
+++ b/client/src/components/ui/tooltip/TooltipTrigger.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/ui/tooltip/index.ts b/client/src/components/ui/tooltip/index.ts
new file mode 100644
index 000000000..8f8d514d9
--- /dev/null
+++ b/client/src/components/ui/tooltip/index.ts
@@ -0,0 +1,4 @@
+export { default as Tooltip } from "./Tooltip.vue"
+export { default as TooltipContent } from "./TooltipContent.vue"
+export { default as TooltipProvider } from "./TooltipProvider.vue"
+export { default as TooltipTrigger } from "./TooltipTrigger.vue"
diff --git a/client/src/stores/theme.ts b/client/src/stores/theme.ts
new file mode 100644
index 000000000..314ef20c9
--- /dev/null
+++ b/client/src/stores/theme.ts
@@ -0,0 +1,91 @@
+import { defineStore } from 'pinia'
+import { ref, watch } from 'vue'
+
+type Theme = 'light' | 'dark'
+export type Accent = 'neutral' | 'violet' | 'blue' | 'cyan' | 'green' | 'amber' | 'orange' | 'rose'
+export type Radius = 'sharp' | 'default' | 'rounded'
+
+export const ACCENT_OPTIONS: { id: Accent; label: string; color: string }[] = [
+ { id: 'neutral', label: 'Neutral', color: '#a8956e' },
+ { id: 'violet', label: 'Violet', color: '#7c3aed' },
+ { id: 'blue', label: 'Blue', color: '#2563eb' },
+ { id: 'cyan', label: 'Cyan', color: '#0891b2' },
+ { id: 'green', label: 'Green', color: '#16a34a' },
+ { id: 'amber', label: 'Amber', color: '#d97706' },
+ { id: 'orange', label: 'Orange', color: '#ea580c' },
+ { id: 'rose', label: 'Rose', color: '#e11d48' },
+]
+
+export const RADIUS_OPTIONS: { id: Radius; label: string }[] = [
+ { id: 'sharp', label: 'Sharp' },
+ { id: 'default', label: 'Default' },
+ { id: 'rounded', label: 'Rounded' },
+]
+
+const ACCENT_IDS = ACCENT_OPTIONS.map((a) => a.id)
+const RADIUS_IDS = RADIUS_OPTIONS.map((r) => r.id)
+
+export const useThemeStore = defineStore('theme', () => {
+ const storedTheme = localStorage.getItem('theme') as Theme | null
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
+ const theme = ref(storedTheme ?? (prefersDark ? 'dark' : 'light'))
+
+ const storedAccent = localStorage.getItem('accent') as Accent | null
+ const accent = ref(storedAccent && ACCENT_IDS.includes(storedAccent) ? storedAccent : 'neutral')
+
+ const storedRadius = localStorage.getItem('radius') as Radius | null
+ const radius = ref(storedRadius && RADIUS_IDS.includes(storedRadius) ? storedRadius : 'default')
+
+ function applyTheme(t: Theme) {
+ document.documentElement.classList.toggle('dark', t === 'dark')
+ }
+
+ function applyAccent(a: Accent) {
+ ACCENT_IDS.forEach((id) => document.documentElement.classList.remove(`accent-${id}`))
+ if (a !== 'neutral') document.documentElement.classList.add(`accent-${a}`)
+ }
+
+ function applyRadius(r: Radius) {
+ RADIUS_IDS.forEach((id) => document.documentElement.classList.remove(`radius-${id}`))
+ if (r !== 'default') document.documentElement.classList.add(`radius-${r}`)
+ }
+
+ function toggleTheme() {
+ theme.value = theme.value === 'dark' ? 'light' : 'dark'
+ }
+
+ function setAccent(a: Accent) {
+ accent.value = a
+ }
+
+ function setRadius(r: Radius) {
+ radius.value = r
+ }
+
+ watch(
+ theme,
+ (t) => {
+ applyTheme(t)
+ localStorage.setItem('theme', t)
+ },
+ { immediate: true },
+ )
+ watch(
+ accent,
+ (a) => {
+ applyAccent(a)
+ localStorage.setItem('accent', a)
+ },
+ { immediate: true },
+ )
+ watch(
+ radius,
+ (r) => {
+ applyRadius(r)
+ localStorage.setItem('radius', r)
+ },
+ { immediate: true },
+ )
+
+ return { theme, accent, radius, toggleTheme, setAccent, setRadius }
+})
diff --git a/client/src/views/HomeView.vue b/client/src/views/HomeView.vue
index a3c33452e..079a7d6b9 100644
--- a/client/src/views/HomeView.vue
+++ b/client/src/views/HomeView.vue
@@ -1,12 +1,16 @@
-
-
-
+
+ {{ error }}
-
-
- {{ error }}
+
+
+
+
-
-
-
+
+
+
+
![]()
+
+ {{ book.title ?? '—' }}
+ {{ book.authors.join(', ') }}
+
+
+
-
-
- Loading...
- All {{ total.toLocaleString() }} books loaded
-
-
-
+
+ Loading…
+ All {{ total.toLocaleString() }} books loaded
+
+
+
+
diff --git a/server/src/db/migrations/meta/_journal.json b/server/src/db/migrations/meta/_journal.json
index d1661b4ba..1ab40f279 100644
--- a/server/src/db/migrations/meta/_journal.json
+++ b/server/src/db/migrations/meta/_journal.json
@@ -5,7 +5,7 @@
{
"idx": 0,
"version": "7",
- "when": 1766482574065,
+ "when": 1766491316570,
"tag": "0000_init_schema",
"breakpoints": true
}