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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ 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 }