Begin switching from daisyui to shadcnui (#492)

* feat: add shadcn

* feat: add themes

* feat: make sidebar use shadcn

* feat: sort bg

* feat: lint fixes

* feat: make daisyui toggleable, add tooltips to sidebar, add work in progress docs page

* fix: theme switching for shadcn

* Fix minor profile.vue issue

* feat: update docs, enlarge SidebarMenuButton and refine profile layout

* feat: add testing page

* feat: update css and remove comments from template

* fix: create dropdown not opening due to tooltip interference also lint

* fix: correct CSS selector for homebox in main.css to ensure proper theming functionality

* feat: make theme switching actually kinda work for shadcn

* fix: sidebar colours

* fix: remove unused router import, made sidebar indicate active page and sort tailwind config linting

* style: update styles

* chore: remove unused duplicate code

* style: refine theme management, CSS variables, get styles closer to original

* feat: implement suggested changes

* feat: better button size

---------

Co-authored-by: Matt Kilgore <tankerkiller125@users.noreply.github.com>
This commit is contained in:
Tonya
2025-02-01 10:32:10 +00:00
committed by GitHub
parent 574079437a
commit e708bd9839
77 changed files with 2988 additions and 123 deletions

View File

@@ -1,3 +1,778 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root,.homebox {
--background: 0 0% 100%; /* base 100 */
--foreground: 0 0% 20%; /* base content */
--muted: 0 0% 81%; /* base 300 */
--muted-foreground: 0 0% 20%; /* base content */
--popover: 0 0% 100%; /* base 100 */
--popover-foreground: 0 0% 20%; /* base content */
--card: 0 0% 100%; /* base 100 */
--card-foreground: 0 0% 20%; /* base content */
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 139 16% 43%; /* primary */
--primary-foreground: 139 100% 89%; /* primary text */
--secondary: 97 37% 93%; /* secondary */
--secondary-foreground: 97 31% 19%; /* secondary text */
--accent: 47 100% 67%; /* accent */
--accent-foreground: 47 100% 13%; /* accent text */
--destructive: 0 84.2% 60.2%; /* error */
--destructive-foreground: 210 40% 98%; /* error text */
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--sidebar-background: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
}
/*
* The below themes are based on the daisyUI themes which are licensed under the MIT License.
* Copyright (c) 2020 Pouya Saadeghi
* The license can be found here https://github.com/saadeghi/daisyui
*
* The themes were converted to CSS variables by n0acar and licensed under the MIT License.
* Copyright (c) 2024 n0acar
* The license can be found here https://github.com/n0acar/tiny-projects
*/
.theme-aqua {
--border: 219 11% 89%;
--input: 219 11% 89%;
--ring: 219 11% 89%;
--background: 219 53% 43%;
--foreground: 219 11% 89%;
--primary: 182 93% 49%;
--primary-foreground: 181 100% 17%;
--secondary: 274 31% 57%;
--secondary-foreground: 274 6% 11%;
--destructive: 5 100% 70%;
--destructive-foreground: 5 24% 15%;
--muted: 219 45% 37%;
--muted-foreground: 219 11% 89%;
--accent: 47 100% 80%;
--accent-foreground: 47 20% 16%;
--popover: 219 53% 43%;
--popover-foreground: 219 11% 89%;
--card: 219 53% 43%;
--card-foreground: 219 11% 89%;
--radius: 0.5rem;
}
.theme-black {
--border: 224 0% 84%;
--input: 224 0% 84%;
--ring: 224 0% 84%;
--background: 0 0% 0%;
--foreground: 224 0% 84%;
--primary: 224 0% 22%;
--primary-foreground: 0 0% 84%;
--secondary: 224 0% 22%;
--secondary-foreground: 0 0% 84%;
--destructive: 0 100% 50%;
--destructive-foreground: 0 20% 10%;
--muted: 224 0% 15%;
--muted-foreground: 224 0% 84%;
--accent: 224 0% 22%;
--accent-foreground: 0 0% 84%;
--popover: 0 0% 0%;
--popover-foreground: 224 0% 84%;
--card: 0 0% 0%;
--card-foreground: 224 0% 84%;
--radius: 0;
}
.theme-bumblebee {
--border: 224 -35% 20%;
--input: 224 -35% 20%;
--ring: 224 -35% 20%;
--background: 0 0% 100%;
--foreground: 224 -35% 20%;
--primary: 51 100% 50%;
--primary-foreground: 49 31% 23%;
--secondary: 39 100% 50%;
--secondary-foreground: 34 59% 23%;
--destructive: 358.25 100% 69%;
--destructive-foreground: 0 0% 0%;
--muted: 224 -148% 86%;
--muted-foreground: 224 -35% 20%;
--accent: 28 100% 67%;
--accent-foreground: 27 24% 14%;
--popover: 0 0% 100%;
--popover-foreground: 224 -35% 20%;
--card: 0 0% 100%;
--card-foreground: 224 -35% 20%;
--radius: 0.5rem;
}
.theme-cmyk {
--border: 224 -35% 20%;
--input: 224 -35% 20%;
--ring: 224 -35% 20%;
--background: 0 0% 100%;
--foreground: 224 -35% 20%;
--primary: 203 83% 60%;
--primary-foreground: 203 17% 12%;
--secondary: 335 78% 60%;
--secondary-foreground: 335 16% 12%;
--destructive: 4 81% 56%;
--destructive-foreground: 4 16% 11%;
--muted: 224 -148% 86%;
--muted-foreground: 224 -35% 20%;
--accent: 56 100% 60%;
--accent-foreground: 56 20% 12%;
--popover: 0 0% 100%;
--popover-foreground: 224 -35% 20%;
--card: 0 0% 100%;
--card-foreground: 224 -35% 20%;
--radius: 0.5rem;
}
.theme-corporate {
--border: 233 27% 13%;
--input: 233 27% 13%;
--ring: 233 27% 13%;
--background: 0 0% 100%;
--foreground: 233 27% 13%;
--primary: 229 100% 65%;
--primary-foreground: 229 22% 13%;
--secondary: 215 26% 59%;
--secondary-foreground: 215 5% 12%;
--destructive: 358.25 100% 69%;
--destructive-foreground: 0 0% 0%;
--muted: 224 -148% 86%;
--muted-foreground: 233 27% 13%;
--accent: 154 49% 60%;
--accent-foreground: 154 10% 12%;
--popover: 0 0% 100%;
--popover-foreground: 233 27% 13%;
--card: 0 0% 100%;
--card-foreground: 233 27% 13%;
--radius: 0.125rem;
}
.theme-cupcake {
--border: 280 46% 14%;
--input: 280 46% 14%;
--ring: 280 46% 14%;
--background: 24 33% 97%;
--foreground: 280 46% 14%;
--primary: 183 47% 59%;
--primary-foreground: 183 9% 12%;
--secondary: 338 71% 78%;
--secondary-foreground: 338 14% 16%;
--destructive: 358.25 100% 69%;
--destructive-foreground: 0 0% 0%;
--muted: 23 14% 89%;
--muted-foreground: 280 46% 14%;
--accent: 39 84% 58%;
--accent-foreground: 39 17% 12%;
--popover: 24 33% 97%;
--popover-foreground: 280 46% 14%;
--card: 24 33% 97%;
--card-foreground: 280 46% 14%;
--radius: 1.9rem;
}
.theme-cyberpunk {
--border: 55 20% 13%;
--input: 55 20% 13%;
--ring: 55 20% 13%;
--background: 56 100% 64%;
--foreground: 55 20% 13%;
--primary: 343 100% 72%;
--primary-foreground: 343 26% 15%;
--secondary: 185 100% 49%;
--secondary-foreground: 184 50% 6%;
--destructive: 358.25 100% 69%;
--destructive-foreground: 0 0% 0%;
--muted: 55 88% 55%;
--muted-foreground: 55 20% 13%;
--accent: 279 100% 73%;
--accent-foreground: 277 22% 15%;
--popover: 56 100% 64%;
--popover-foreground: 55 20% 13%;
--card: 56 100% 64%;
--card-foreground: 55 20% 13%;
--radius: 0;
}
.theme-dark {
--border: 220 13% 69%;
--input: 220 13% 69%;
--ring: 220 13% 69%;
--background: 212 18% 14%;
--foreground: 220 13% 69%;
--primary: 235 100% 73%;
--primary-foreground: 235 22% 15%;
--secondary: 316 100% 69%;
--secondary-foreground: 318 25% 14%;
--destructive: 358.25 100% 69%;
--destructive-foreground: 0 0% 0%;
--muted: 213 18% 10%;
--muted-foreground: 220 13% 69%;
--accent: 174 100% 40%;
--accent-foreground: 176 51% 5%;
--popover: 212 18% 14%;
--popover-foreground: 220 13% 69%;
--card: 212 18% 14%;
--card-foreground: 220 13% 69%;
--radius: 0.5rem;
}
.theme-dracula {
--border: 60 30% 96%;
--input: 60 30% 96%;
--ring: 60 30% 96%;
--background: 231 15% 18%;
--foreground: 60 30% 96%;
--primary: 326 100% 74%;
--primary-foreground: 326 20% 15%;
--secondary: 265 89% 78%;
--secondary-foreground: 265 18% 16%;
--destructive: 0 100% 67%;
--destructive-foreground: 0 20% 13%;
--muted: 231 13% 16%;
--muted-foreground: 60 30% 96%;
--accent: 31 100% 71%;
--accent-foreground: 31 20% 14%;
--popover: 231 15% 18%;
--popover-foreground: 60 30% 96%;
--card: 231 15% 18%;
--card-foreground: 60 30% 96%;
--radius: 0.5rem;
}
.theme-emerald {
--border: 219 20% 25%;
--input: 219 20% 25%;
--ring: 219 20% 25%;
--background: 0 0% 100%;
--foreground: 219 20% 25%;
--primary: 141 50% 60%;
--primary-foreground: 151 28% 19%;
--secondary: 219 96% 60%;
--secondary-foreground: 180 100% 100%;
--destructive: 358.25 100% 69%;
--destructive-foreground: 0 0% 0%;
--muted: 224 -148% 86%;
--muted-foreground: 219 20% 25%;
--accent: 10 89% 68%;
--accent-foreground: 0 0% 0%;
--popover: 0 0% 100%;
--popover-foreground: 219 20% 25%;
--card: 0 0% 100%;
--card-foreground: 219 20% 25%;
--radius: 0.5rem;
}
.theme-fantasy {
--border: 215 28% 17%;
--input: 215 28% 17%;
--ring: 215 28% 17%;
--background: 0 0% 100%;
--foreground: 215 28% 17%;
--primary: 296 100% 23%;
--primary-foreground: 296 26% 84%;
--secondary: 203 100% 37%;
--secondary-foreground: 199 35% 86%;
--destructive: 358.25 100% 69%;
--destructive-foreground: 0 0% 0%;
--muted: 224 -148% 86%;
--muted-foreground: 215 28% 17%;
--accent: 32 100% 50%;
--accent-foreground: 35 29% 9%;
--popover: 0 0% 100%;
--popover-foreground: 215 28% 17%;
--card: 0 0% 100%;
--card-foreground: 215 28% 17%;
--radius: 0.5rem;
}
.theme-forest {
--border: 0 2% 82%;
--input: 0 2% 82%;
--ring: 0 2% 82%;
--background: 0 12% 8%;
--foreground: 0 2% 82%;
--primary: 141 72% 42%;
--primary-foreground: 0 0% 0%;
--secondary: 164 73% 42%;
--secondary-foreground: 164 15% 8%;
--destructive: 358.25 100% 69%;
--destructive-foreground: 0 0% 0%;
--muted: 0 10% 7%;
--muted-foreground: 0 2% 82%;
--accent: 175 73% 42%;
--accent-foreground: 175 15% 8%;
--popover: 0 12% 8%;
--popover-foreground: 0 2% 82%;
--card: 0 12% 8%;
--card-foreground: 0 2% 82%;
--radius: 1.9rem;
}
.theme-garden {
--border: 0 3% 6%;
--input: 0 3% 6%;
--ring: 0 3% 6%;
--background: 0 4% 91%;
--foreground: 0 3% 6%;
--primary: 332 100% 49%;
--primary-foreground: 180 100% 100%;
--secondary: 334 37% 41%;
--secondary-foreground: 334 7% 88%;
--destructive: 358.25 100% 69%;
--destructive-foreground: 0 0% 0%;
--muted: 0 4% 78%;
--muted-foreground: 0 3% 6%;
--accent: 139 16% 43%;
--accent-foreground: 139 3% 9%;
--popover: 0 4% 91%;
--popover-foreground: 0 3% 6%;
--card: 0 4% 91%;
--card-foreground: 0 3% 6%;
--radius: 0.5rem;
}
.theme-halloween {
--border: 0 0% 83%;
--input: 0 0% 83%;
--ring: 0 0% 83%;
--background: 224 0% 13%;
--foreground: 0 0% 83%;
--primary: 34 100% 50%;
--primary-foreground: 180 7% 8%;
--secondary: 278 100% 38%;
--secondary-foreground: 279 24% 87%;
--destructive: 3 87% 62%;
--destructive-foreground: 3 17% 12%;
--muted: 0 0% 11%;
--muted-foreground: 0 0% 83%;
--accent: 96 100% 33%;
--accent-foreground: 0 0% 0%;
--popover: 224 0% 13%;
--popover-foreground: 0 0% 83%;
--card: 224 0% 13%;
--card-foreground: 0 0% 83%;
--radius: 0.5rem;
}
.theme-light {
--border: 215 28% 17%;
--input: 215 28% 17%;
--ring: 215 28% 17%;
--background: 0 0% 100%;
--foreground: 215 28% 17%;
--primary: 257 100% 50%;
--primary-foreground: 258 22% 90%;
--secondary: 313 100% 56%;
--secondary-foreground: 320 100% 99%;
--destructive: 358.25 100% 69%;
--destructive-foreground: 0 0% 0%;
--muted: 180 2% 90%;
--muted-foreground: 215 28% 17%;
--accent: 174 100% 41%;
--accent-foreground: 176 59% 4%;
--popover: 0 0% 100%;
--popover-foreground: 215 28% 17%;
--card: 0 0% 100%;
--card-foreground: 215 28% 17%;
--radius: 0.5rem;
}
.theme-lofi {
--border: 0 0% 0%;
--input: 0 0% 0%;
--ring: 0 0% 0%;
--background: 0 0% 100%;
--foreground: 0 0% 0%;
--primary: 224 0% 5%;
--primary-foreground: 0 0% 100%;
--secondary: 0 2% 10%;
--secondary-foreground: 0 0% 100%;
--destructive: 7 100% 76%;
--destructive-foreground: 7 24% 16%;
--muted: 0 2% 90%;
--muted-foreground: 0 0% 0%;
--accent: 224 0% 15%;
--accent-foreground: 0 0% 100%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 0%;
--card: 0 0% 100%;
--card-foreground: 0 0% 0%;
--radius: 0.125rem;
}
.theme-luxury {
--border: 37 67% 58%;
--input: 37 67% 58%;
--ring: 37 67% 58%;
--background: 240 10% 4%;
--foreground: 37 67% 58%;
--primary: 0 0% 100%;
--primary-foreground: 224 -35% 20%;
--secondary: 218 54% 18%;
--secondary-foreground: 218 11% 84%;
--destructive: 0 100% 72%;
--destructive-foreground: 0 20% 14%;
--muted: 270 2% 18%;
--muted-foreground: 37 67% 58%;
--accent: 319 22% 26%;
--accent-foreground: 319 4% 85%;
--popover: 240 10% 4%;
--popover-foreground: 37 67% 58%;
--card: 240 10% 4%;
--card-foreground: 37 67% 58%;
--radius: 0.5rem;
}
.theme-pastel {
--border: 224 -35% 20%;
--input: 224 -35% 20%;
--ring: 224 -35% 20%;
--background: 0 0% 100%;
--foreground: 224 -35% 20%;
--primary: 284 22% 80%;
--primary-foreground: 284 4% 16%;
--secondary: 352 70% 88%;
--secondary-foreground: 352 14% 18%;
--destructive: 358.25 100% 69%;
--destructive-foreground: 0 0% 0%;
--muted: 216 12% 84%;
--muted-foreground: 224 -35% 20%;
--accent: 158 55% 81%;
--accent-foreground: 158 11% 16%;
--popover: 0 0% 100%;
--popover-foreground: 224 -35% 20%;
--card: 0 0% 100%;
--card-foreground: 224 -35% 20%;
--radius: 1.9rem;
}
.theme-retro {
--border: 345 5% 15%;
--input: 345 5% 15%;
--ring: 345 5% 15%;
--background: 44 47% 86%;
--foreground: 345 5% 15%;
--primary: 3 74% 76%;
--primary-foreground: 345 5% 15%;
--secondary: 145 27% 72%;
--secondary-foreground: 345 5% 15%;
--destructive: 3 87% 62%;
--destructive-foreground: 3 17% 12%;
--muted: 44 47% 73%;
--muted-foreground: 345 5% 15%;
--accent: 24 67% 59%;
--accent-foreground: 345 5% 15%;
--popover: 44 47% 86%;
--popover-foreground: 345 5% 15%;
--card: 44 47% 86%;
--card-foreground: 345 5% 15%;
--radius: 0.4rem;
}
.theme-synthwave {
--border: 260 60% 98%;
--input: 260 60% 98%;
--ring: 260 60% 98%;
--background: 253 58% 15%;
--foreground: 260 60% 98%;
--primary: 321 70% 69%;
--primary-foreground: 321 14% 14%;
--secondary: 197 87% 65%;
--secondary-foreground: 197 17% 13%;
--destructive: 10 75% 70%;
--destructive-foreground: 257 63% 17%;
--muted: 253 50% 13%;
--muted-foreground: 260 60% 98%;
--accent: 50 100% 50%;
--accent-foreground: 51 35% 7%;
--popover: 253 58% 15%;
--popover-foreground: 260 60% 98%;
--card: 253 58% 15%;
--card-foreground: 260 60% 98%;
--radius: 0.5rem;
}
.theme-valentine {
--border: 344 38% 28%;
--input: 344 38% 28%;
--ring: 344 38% 28%;
--background: 319 66% 94%;
--foreground: 344 38% 28%;
--primary: 353 74% 67%;
--primary-foreground: 353 15% 13%;
--secondary: 254 86% 77%;
--secondary-foreground: 254 17% 15%;
--destructive: 5 100% 69%;
--destructive-foreground: 4 25% 14%;
--muted: 319 56% 81%;
--muted-foreground: 344 38% 28%;
--accent: 182 34% 55%;
--accent-foreground: 182 7% 11%;
--popover: 319 66% 94%;
--popover-foreground: 344 38% 28%;
--card: 319 66% 94%;
--card-foreground: 344 38% 28%;
--radius: 1.9rem;
}
.theme-wireframe {
--border: 224 -35% 20%;
--input: 224 -35% 20%;
--ring: 224 -35% 20%;
--background: 0 0% 100%;
--foreground: 224 -35% 20%;
--primary: 224 0% 72%;
--primary-foreground: 0 0% 14%;
--secondary: 224 0% 72%;
--secondary-foreground: 0 0% 14%;
--destructive: 0 100% 50%;
--destructive-foreground: 0 20% 10%;
--muted: 224 0% 87%;
--muted-foreground: 224 -35% 20%;
--accent: 224 0% 72%;
--accent-foreground: 0 0% 14%;
--popover: 0 0% 100%;
--popover-foreground: 224 -35% 20%;
--card: 0 0% 100%;
--card-foreground: 224 -35% 20%;
--radius: 0.2rem;
}
.theme-autumn {
--border: 0 0% 19%;
--input: 0 0% 19%;
--ring: 0 0% 19%;
--background: 224 0% 95%;
--foreground: 0 0% 19%;
--primary: 344 96% 28%;
--primary-foreground: 344 19% 86%;
--secondary: 0 63% 58%;
--secondary-foreground: 0 13% 12%;
--destructive: 353 100% 41%;
--destructive-foreground: 346 29% 87%;
--muted: 0 0% 81%;
--muted-foreground: 0 0% 19%;
--accent: 27 56% 63%;
--accent-foreground: 27 11% 13%;
--popover: 224 0% 95%;
--popover-foreground: 0 0% 19%;
--card: 224 0% 95%;
--card-foreground: 0 0% 19%;
--radius: 0.5rem;
}
.theme-business {
--border: 0 0% 83%;
--input: 0 0% 83%;
--ring: 0 0% 83%;
--background: 224 0% 13%;
--foreground: 0 0% 83%;
--primary: 210 64% 31%;
--primary-foreground: 210 13% 86%;
--secondary: 200 13% 55%;
--secondary-foreground: 200 3% 11%;
--destructive: 6 56% 43%;
--destructive-foreground: 6 11% 89%;
--muted: 0 0% 11%;
--muted-foreground: 0 0% 83%;
--accent: 13 80% 60%;
--accent-foreground: 13 16% 12%;
--popover: 224 0% 13%;
--popover-foreground: 0 0% 83%;
--card: 224 0% 13%;
--card-foreground: 0 0% 83%;
--radius: 0.125rem;
}
.theme-acid {
--border: 0 0% 20%;
--input: 0 0% 20%;
--ring: 0 0% 20%;
--background: 224 0% 98%;
--foreground: 0 0% 20%;
--primary: 300 100% 53%;
--primary-foreground: 302 30% 9%;
--secondary: 28 100% 50%;
--secondary-foreground: 30 29% 9%;
--destructive: 2 100% 51%;
--destructive-foreground: 357 29% 9%;
--muted: 0 0% 84%;
--muted-foreground: 0 0% 20%;
--accent: 73 100% 50%;
--accent-foreground: 70 39% 7%;
--popover: 224 0% 98%;
--popover-foreground: 0 0% 20%;
--card: 224 0% 98%;
--card-foreground: 0 0% 20%;
--radius: 1rem;
}
.theme-lemonade {
--border: 83 16% 19%;
--input: 83 16% 19%;
--ring: 83 16% 19%;
--background: 83 82% 97%;
--foreground: 83 16% 19%;
--primary: 93 100% 29%;
--primary-foreground: 86 36% 4%;
--secondary: 61 100% 38%;
--secondary-foreground: 61 39% 5%;
--destructive: 6 59% 85%;
--destructive-foreground: 6 12% 17%;
--muted: 83 70% 83%;
--muted-foreground: 83 16% 19%;
--accent: 53 100% 46%;
--accent-foreground: 54 35% 7%;
--popover: 83 82% 97%;
--popover-foreground: 83 16% 19%;
--card: 83 82% 97%;
--card-foreground: 83 16% 19%;
--radius: 0.5rem;
}
.theme-night {
--border: 222 9% 82%;
--input: 222 9% 82%;
--ring: 222 9% 82%;
--background: 222 47% 11%;
--foreground: 222 9% 82%;
--primary: 198 93% 60%;
--primary-foreground: 198 19% 12%;
--secondary: 234 89% 74%;
--secondary-foreground: 234 18% 15%;
--destructive: 351 95% 71%;
--destructive-foreground: 351 19% 14%;
--muted: 222 41% 10%;
--muted-foreground: 222 9% 82%;
--accent: 329 86% 70%;
--accent-foreground: 329 17% 14%;
--popover: 222 47% 11%;
--popover-foreground: 222 9% 82%;
--card: 222 47% 11%;
--card-foreground: 222 9% 82%;
--radius: 0.5rem;
}
.theme-coffee {
--border: 37 47% 57%;
--input: 37 47% 57%;
--ring: 37 47% 57%;
--background: 306 19% 11%;
--foreground: 37 47% 57%;
--primary: 30 67% 58%;
--primary-foreground: 30 13% 12%;
--secondary: 182 25% 20%;
--secondary-foreground: 182 5% 84%;
--destructive: 10 95% 75%;
--destructive-foreground: 10 19% 15%;
--muted: 306 16% 9%;
--muted-foreground: 37 47% 57%;
--accent: 194 74% 25%;
--accent-foreground: 194 15% 85%;
--popover: 306 19% 11%;
--popover-foreground: 37 47% 57%;
--card: 306 19% 11%;
--card-foreground: 37 47% 57%;
--radius: 0.5rem;
}
.theme-winter {
--border: 214 30% 32%;
--input: 214 30% 32%;
--ring: 214 30% 32%;
--background: 0 0% 100%;
--foreground: 214 30% 32%;
--primary: 215 100% 50%;
--primary-foreground: 211 28% 89%;
--secondary: 247 47% 43%;
--secondary-foreground: 247 9% 89%;
--destructive: 0 63% 72%;
--destructive-foreground: 0 13% 14%;
--muted: 219 44% 92%;
--muted-foreground: 214 30% 32%;
--accent: 310 49% 52%;
--accent-foreground: 310 10% 10%;
--popover: 0 0% 100%;
--popover-foreground: 214 30% 32%;
--card: 0 0% 100%;
--card-foreground: 214 30% 32%;
--radius: 0.5rem;
}
.theme-dim {
--border: 197 31% 77%;
--input: 197 31% 77%;
--ring: 197 31% 77%;
--background: 220 18% 20%;
--foreground: 197 31% 77%;
--primary: 108 66% 73%;
--primary-foreground: 108 13% 15%;
--secondary: 12 100% 68%;
--secondary-foreground: 12 20% 14%;
--destructive: 11 100% 80%;
--destructive-foreground: 11 20% 16%;
--muted: 219 18% 15%;
--muted-foreground: 197 31% 77%;
--accent: 277 66% 74%;
--accent-foreground: 277 13% 15%;
--popover: 220 18% 20%;
--popover-foreground: 197 31% 77%;
--card: 220 18% 20%;
--card-foreground: 197 31% 77%;
--radius: 0.5rem;
}
.theme-nord {
--border: 220 16% 22%;
--input: 220 16% 22%;
--ring: 220 16% 22%;
--background: 217 27% 94%;
--foreground: 220 16% 22%;
--primary: 213 32% 52%;
--primary-foreground: 213 6% 10%;
--secondary: 210 34% 63%;
--secondary-foreground: 210 7% 13%;
--destructive: 354 42% 56%;
--destructive-foreground: 354 8% 11%;
--muted: 219 28% 88%;
--muted-foreground: 220 16% 22%;
--accent: 193 43% 67%;
--accent-foreground: 193 9% 13%;
--popover: 217 27% 94%;
--popover-foreground: 220 16% 22%;
--card: 217 27% 94%;
--card-foreground: 220 16% 22%;
--radius: 0.2rem;
}
.theme-sunset {
--border: 208 34% 72%;
--input: 208 34% 72%;
--ring: 208 34% 72%;
--background: 204 31% 10%;
--foreground: 208 34% 72%;
--primary: 16 100% 68%;
--primary-foreground: 16 20% 14%;
--secondary: 341 97% 71%;
--secondary-foreground: 341 19% 14%;
--destructive: 358 100% 87%;
--destructive-foreground: 358 20% 17%;
--muted: 204 45% 7%;
--muted-foreground: 208 34% 72%;
--accent: 263 92% 75%;
--accent-foreground: 263 18% 15%;
--popover: 204 31% 10%;
--popover-foreground: 208 34% 72%;
--card: 204 31% 10%;
--card-foreground: 208 34% 72%;
--radius: 0.8rem;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
.text-no-transform {
text-transform: none !important;
}
@@ -35,7 +810,7 @@
.scroll-bg::-webkit-scrollbar-thumb {
border-radius: 0.25rem;
@apply bg-base-300;
@apply bg-muted;
}
.markdown > :first-child {

18
frontend/components.json Normal file
View File

@@ -0,0 +1,18 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "default",
"typescript": true,
"tsConfigPath": "tsconfig.json",
"tailwind": {
"config": "tailwind.config.js",
"css": "assets/css/main.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"framework": "nuxt",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { Primitive, type PrimitiveProps } from "radix-vue";
import { type ButtonVariants, buttonVariants } from ".";
import { cn } from "@/lib/utils";
interface Props extends PrimitiveProps {
variant?: ButtonVariants["variant"];
size?: ButtonVariants["size"];
class?: HTMLAttributes["class"];
}
const props = withDefaults(defineProps<Props>(), {
as: "button",
});
</script>
<template>
<Primitive :as="as" :as-child="asChild" :class="cn(buttonVariants({ variant, size }), props.class)">
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,31 @@
import { cva, type VariantProps } 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 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import {
DropdownMenuRoot,
type DropdownMenuRootEmits,
type DropdownMenuRootProps,
useForwardPropsEmits,
} from "radix-vue";
const props = defineProps<DropdownMenuRootProps>();
const emits = defineEmits<DropdownMenuRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuRoot v-bind="forwarded">
<slot />
</DropdownMenuRoot>
</template>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import { Check } from "lucide-vue-next";
import {
DropdownMenuCheckboxItem,
type DropdownMenuCheckboxItemEmits,
type DropdownMenuCheckboxItemProps,
DropdownMenuItemIndicator,
useForwardPropsEmits,
} from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }>();
const emits = defineEmits<DropdownMenuCheckboxItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuCheckboxItem
v-bind="forwarded"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class
)
"
>
<span class="absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<Check class="size-4" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuCheckboxItem>
</template>

View File

@@ -0,0 +1,40 @@
<script setup lang="ts">
import {
DropdownMenuContent,
type DropdownMenuContentEmits,
type DropdownMenuContentProps,
DropdownMenuPortal,
useForwardPropsEmits,
} from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = withDefaults(defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(), {
sideOffset: 4,
});
const emits = defineEmits<DropdownMenuContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuPortal>
<DropdownMenuContent
v-bind="forwarded"
:class="
cn(
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class
)
"
>
<slot />
</DropdownMenuContent>
</DropdownMenuPortal>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { DropdownMenuGroup, type DropdownMenuGroupProps } from "radix-vue";
const props = defineProps<DropdownMenuGroupProps>();
</script>
<template>
<DropdownMenuGroup v-bind="props">
<slot />
</DropdownMenuGroup>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import { DropdownMenuItem, type DropdownMenuItemProps, useForwardProps } from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<DropdownMenuItemProps & { class?: HTMLAttributes["class"]; inset?: boolean }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuItem
v-bind="forwardedProps"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
inset && 'pl-8',
props.class
)
"
>
<slot />
</DropdownMenuItem>
</template>

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import { DropdownMenuLabel, type DropdownMenuLabelProps, useForwardProps } from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes["class"]; inset?: boolean }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuLabel
v-bind="forwardedProps"
:class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)"
>
<slot />
</DropdownMenuLabel>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import {
DropdownMenuRadioGroup,
type DropdownMenuRadioGroupEmits,
type DropdownMenuRadioGroupProps,
useForwardPropsEmits,
} from "radix-vue";
const props = defineProps<DropdownMenuRadioGroupProps>();
const emits = defineEmits<DropdownMenuRadioGroupEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuRadioGroup v-bind="forwarded">
<slot />
</DropdownMenuRadioGroup>
</template>

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import { Circle } from "lucide-vue-next";
import {
DropdownMenuItemIndicator,
DropdownMenuRadioItem,
type DropdownMenuRadioItemEmits,
type DropdownMenuRadioItemProps,
useForwardPropsEmits,
} from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }>();
const emits = defineEmits<DropdownMenuRadioItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuRadioItem
v-bind="forwarded"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class
)
"
>
<span class="absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<Circle class="size-2 fill-current" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuRadioItem>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { DropdownMenuSeparator, type DropdownMenuSeparatorProps } from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<
DropdownMenuSeparatorProps & {
class?: HTMLAttributes["class"];
}
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<DropdownMenuSeparator v-bind="delegatedProps" :class="cn('-mx-1 my-1 h-px bg-muted', props.class)" />
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<span :class="cn('ml-auto text-xs tracking-widest opacity-60', props.class)">
<slot />
</span>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import {
DropdownMenuSub,
type DropdownMenuSubEmits,
type DropdownMenuSubProps,
useForwardPropsEmits,
} from "radix-vue";
const props = defineProps<DropdownMenuSubProps>();
const emits = defineEmits<DropdownMenuSubEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DropdownMenuSub v-bind="forwarded">
<slot />
</DropdownMenuSub>
</template>

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
import {
DropdownMenuSubContent,
type DropdownMenuSubContentEmits,
type DropdownMenuSubContentProps,
useForwardPropsEmits,
} from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }>();
const emits = defineEmits<DropdownMenuSubContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuSubContent
v-bind="forwarded"
:class="
cn(
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class
)
"
>
<slot />
</DropdownMenuSubContent>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { ChevronRight } from "lucide-vue-next";
import { DropdownMenuSubTrigger, type DropdownMenuSubTriggerProps, useForwardProps } from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuSubTrigger
v-bind="forwardedProps"
:class="
cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
props.class
)
"
>
<slot />
<ChevronRight class="ml-auto size-4" />
</DropdownMenuSubTrigger>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import { DropdownMenuTrigger, type DropdownMenuTriggerProps, useForwardProps } from "radix-vue";
const props = defineProps<DropdownMenuTriggerProps>();
const forwardedProps = useForwardProps(props);
</script>
<template>
<DropdownMenuTrigger class="outline-none" v-bind="forwardedProps">
<slot />
</DropdownMenuTrigger>
</template>

View File

@@ -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 "radix-vue";

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { useVModel } from "@vueuse/core";
import { cn } from "@/lib/utils";
const props = defineProps<{
defaultValue?: string | number;
modelValue?: string | number;
class?: HTMLAttributes["class"];
}>();
const emits = defineEmits<{
(e: "update:modelValue", payload: string | number): void;
}>();
const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
});
</script>
<template>
<input
v-model="modelValue"
:class="
cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
props.class
)
"
/>
</template>

View File

@@ -0,0 +1 @@
export { default as Input } from "./Input.vue";

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import { Separator, type SeparatorProps } from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<SeparatorProps & { class?: HTMLAttributes["class"]; label?: string }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<Separator
v-bind="delegatedProps"
:class="
cn('shrink-0 bg-border relative', props.orientation === 'vertical' ? 'w-px h-full' : 'h-px w-full', props.class)
"
>
<span
v-if="props.label"
:class="
cn(
'text-xs text-muted-foreground bg-background absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex justify-center items-center',
props.orientation === 'vertical' ? 'w-[1px] px-1 py-2' : 'h-[1px] py-1 px-2'
)
"
>{{ props.label }}</span
>
</Separator>
</template>

View File

@@ -0,0 +1 @@
export { default as Separator } from "./Separator.vue";

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from "radix-vue";
const props = defineProps<DialogRootProps>();
const emits = defineEmits<DialogRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DialogRoot v-bind="forwarded">
<slot />
</DialogRoot>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { DialogClose, type DialogCloseProps } from "radix-vue";
const props = defineProps<DialogCloseProps>();
</script>
<template>
<DialogClose v-bind="props">
<slot />
</DialogClose>
</template>

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import { X } from "lucide-vue-next";
import {
DialogClose,
DialogContent,
type DialogContentEmits,
type DialogContentProps,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
import { type SheetVariants, sheetVariants } from ".";
import { cn } from "@/lib/utils";
interface SheetContentProps extends DialogContentProps {
class?: HTMLAttributes["class"];
side?: SheetVariants["side"];
}
defineOptions({
inheritAttrs: false,
});
const props = defineProps<SheetContentProps>();
const emits = defineEmits<DialogContentEmits>();
const delegatedProps = computed(() => {
const { class: _, side, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<DialogContent :class="cn(sheetVariants({ side }), props.class)" v-bind="{ ...forwarded, ...$attrs }">
<slot />
<DialogClose
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"
>
<X class="size-4 text-muted-foreground" />
</DialogClose>
</DialogContent>
</DialogPortal>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import { DialogDescription, type DialogDescriptionProps } from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<DialogDescription :class="cn('text-sm text-muted-foreground', props.class)" v-bind="delegatedProps">
<slot />
</DialogDescription>
</template>

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{ class?: HTMLAttributes["class"] }>();
</script>
<template>
<div :class="cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{ class?: HTMLAttributes["class"] }>();
</script>
<template>
<div :class="cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import { DialogTitle, type DialogTitleProps } from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<DialogTitle :class="cn('text-lg font-semibold text-foreground', props.class)" v-bind="delegatedProps">
<slot />
</DialogTitle>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { DialogTrigger, type DialogTriggerProps } from "radix-vue";
const props = defineProps<DialogTriggerProps>();
</script>
<template>
<DialogTrigger v-bind="props">
<slot />
</DialogTrigger>
</template>

View File

@@ -0,0 +1,31 @@
import { cva, type VariantProps } from "class-variance-authority";
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";
export const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
);
export type SheetVariants = VariantProps<typeof sheetVariants>;

View File

@@ -0,0 +1,90 @@
<script setup lang="ts">
import { SIDEBAR_WIDTH_MOBILE, useSidebar } from "./utils";
import type { SidebarProps } from ".";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { cn } from "@/lib/utils";
defineOptions({
inheritAttrs: false,
});
const props = withDefaults(defineProps<SidebarProps>(), {
side: "left",
variant: "sidebar",
collapsible: "offcanvas",
});
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
</script>
<template>
<div
v-if="collapsible === 'none'"
:class="cn('flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground', props.class)"
v-bind="$attrs"
>
<slot />
</div>
<Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="setOpenMobile">
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
:side="side"
class="bg-sidebar text-sidebar-foreground w-[--sidebar-width] p-0 [&>button]:hidden"
:style="{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
}"
>
<div class="flex size-full flex-col">
<slot />
</div>
</SheetContent>
</Sheet>
<div
v-else
class="group peer hidden md:block"
:data-state="state"
:data-collapsible="state === 'collapsed' ? collapsible : ''"
:data-variant="variant"
:data-side="side"
>
<!-- This is what handles the sidebar gap on desktop -->
<div
:class="
cn(
'duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]'
)
"
/>
<div
:class="
cn(
'duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]',
props.class
)
"
v-bind="$attrs"
>
<div
data-sidebar="sidebar"
class="text-sidebar-foreground bg-sidebar group-data-[variant=floating]:border-sidebar-border flex size-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow"
>
<slot />
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div
data-sidebar="content"
:class="
cn('flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', props.class)
"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div data-sidebar="footer" :class="cn('flex flex-col gap-2 p-2', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div data-sidebar="group" :class="cn('relative flex w-full min-w-0 flex-col p-2', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { PrimitiveProps } from "radix-vue";
import type { HTMLAttributes } from "vue";
import { Primitive } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps<
PrimitiveProps & {
class?: HTMLAttributes["class"];
}
>();
</script>
<template>
<Primitive
data-sidebar="group-action"
:as="as"
:as-child="asChild"
:class="
cn(
'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 after:md:hidden',
'group-data-[collapsible=icon]:hidden',
props.class
)
"
>
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div data-sidebar="group-content" :class="cn('w-full text-sm', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { PrimitiveProps } from "radix-vue";
import type { HTMLAttributes } from "vue";
import { Primitive } from "radix-vue";
import { cn } from "@/lib/utils";
const props = defineProps<
PrimitiveProps & {
class?: HTMLAttributes["class"];
}
>();
</script>
<template>
<Primitive
data-sidebar="group-label"
:as="as"
:as-child="asChild"
:class="
cn(
'duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
props.class
)
"
>
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div data-sidebar="header" :class="cn('flex flex-col gap-2 p-2', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<Input
data-sidebar="input"
:class="
cn('h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring', props.class)
"
>
<slot />
</Input>
</template>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<main
:class="
cn(
'relative flex min-h-svh flex-1 flex-col bg-background',
'peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
props.class
)
"
>
<slot />
</main>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<ul data-sidebar="menu" :class="cn('flex w-full min-w-0 flex-col gap-1', props.class)">
<slot />
</ul>
</template>

View File

@@ -0,0 +1,41 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { Primitive, type PrimitiveProps } from "radix-vue";
import { cn } from "@/lib/utils";
const props = withDefaults(
defineProps<
PrimitiveProps & {
showOnHover?: boolean;
class?: HTMLAttributes["class"];
}
>(),
{
as: "button",
}
);
</script>
<template>
<Primitive
data-sidebar="menu-action"
:class="
cn(
'absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 after:md:hidden',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
showOnHover &&
'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0',
props.class
)
"
:as="as"
:as-child="asChild"
>
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<div
data-sidebar="menu-badge"
:class="
cn(
'absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none',
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
props.class
)
"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,54 @@
<script setup lang="ts">
import { type Component, computed } from "vue";
import SidebarMenuButtonChild, { type SidebarMenuButtonProps } from "./SidebarMenuButtonChild.vue";
import { useSidebar } from "./utils";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
defineOptions({
inheritAttrs: false,
});
const props = withDefaults(
defineProps<
SidebarMenuButtonProps & {
tooltip?: string | Component;
hotkey?: string;
}
>(),
{
as: "button",
variant: "default",
size: "default",
}
);
const { isMobile, state } = useSidebar();
const delegatedProps = computed(() => {
const { tooltip, hotkey, ...delegated } = props;
return delegated;
});
</script>
<template>
<SidebarMenuButtonChild v-if="!tooltip" v-bind="{ ...delegatedProps, ...$attrs }">
<slot />
</SidebarMenuButtonChild>
<Tooltip v-else>
<TooltipTrigger as-child>
<SidebarMenuButtonChild v-bind="{ ...delegatedProps, ...$attrs }" :size="state === 'collapsed' ? 'default' : 'lg'" :class="state === 'collapsed' ? '' : 'text-xl'">
<slot />
</SidebarMenuButtonChild>
</TooltipTrigger>
<TooltipContent side="right" align="center" :hidden="state !== 'collapsed' || isMobile">
<template v-if="typeof tooltip === 'string'">
{{ tooltip }}
</template>
<component :is="tooltip" v-else />
</TooltipContent>
<TooltipContent v-if="hotkey" :hidden="isMobile">
{{ hotkey }}
</TooltipContent>
</Tooltip>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { Primitive, type PrimitiveProps } from "radix-vue";
import { type SidebarMenuButtonVariants, sidebarMenuButtonVariants } from ".";
import { cn } from "@/lib/utils";
export interface SidebarMenuButtonProps extends PrimitiveProps {
variant?: SidebarMenuButtonVariants["variant"];
size?: SidebarMenuButtonVariants["size"];
isActive?: boolean;
class?: HTMLAttributes["class"];
}
const props = withDefaults(defineProps<SidebarMenuButtonProps>(), {
as: "button",
variant: "default",
size: "default",
});
</script>
<template>
<Primitive
data-sidebar="menu-button"
:data-size="size"
:data-active="isActive"
:class="cn(sidebarMenuButtonVariants({ variant, size }), props.class)"
:as="as"
:as-child="asChild"
v-bind="$attrs"
>
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<li data-sidebar="menu-item" :class="cn('group/menu-item relative', props.class)">
<slot />
</li>
</template>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import { useSidebar } from "./utils";
import SidebarMenuButton from "./SidebarMenuButton.vue";
const { setOpenMobile } = useSidebar();
const props = defineProps<{
href: string;
}>();
</script>
<template>
<SidebarMenuButton as-child>
<NuxtLink :to="props.href" @click.prevent="setOpenMobile(false)">
<slot />
</NuxtLink>
</SidebarMenuButton>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { computed, type HTMLAttributes } from "vue";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
const props = defineProps<{
showIcon?: boolean;
class?: HTMLAttributes["class"];
}>();
const width = computed(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
});
</script>
<template>
<div data-sidebar="menu-skeleton" :class="cn('rounded-md h-8 flex gap-2 px-2 items-center', props.class)">
<Skeleton v-if="showIcon" class="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
<Skeleton
class="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
:style="{ '--skeleton-width': width }"
/>
</div>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<ul
data-sidebar="menu-badge"
:class="
cn(
'mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5',
'group-data-[collapsible=icon]:hidden',
props.class
)
"
>
<slot />
</ul>
</template>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import type { PrimitiveProps } from "radix-vue";
import type { HTMLAttributes } from "vue";
import { Primitive } from "radix-vue";
import { cn } from "@/lib/utils";
const props = withDefaults(
defineProps<
PrimitiveProps & {
size?: "sm" | "md";
isActive?: boolean;
class?: HTMLAttributes["class"];
}
>(),
{
as: "a",
size: "md",
}
);
</script>
<template>
<Primitive
data-sidebar="menu-sub-button"
:as="as"
:as-child="asChild"
:data-size="size"
:data-active="isActive"
:class="
cn(
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring 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 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
'group-data-[collapsible=icon]:hidden',
props.class
)
"
>
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<li>
<slot />
</li>
</template>

View File

@@ -0,0 +1,90 @@
<script setup lang="ts">
import { useEventListener, useMediaQuery, useVModel } from "@vueuse/core";
import { TooltipProvider } from "radix-vue";
import { computed, type HTMLAttributes, type Ref, ref } from "vue";
import {
provideSidebarContext,
SIDEBAR_COOKIE_MAX_AGE,
SIDEBAR_COOKIE_NAME,
SIDEBAR_KEYBOARD_SHORTCUT,
SIDEBAR_WIDTH,
SIDEBAR_WIDTH_ICON,
} from "./utils";
import { cn } from "@/lib/utils";
const props = withDefaults(
defineProps<{
defaultOpen?: boolean;
open?: boolean;
class?: HTMLAttributes["class"];
}>(),
{
defaultOpen: true,
open: undefined,
}
);
const emits = defineEmits<{
"update:open": [open: boolean];
}>();
const isMobile = useMediaQuery("(max-width: 768px)");
const openMobile = ref(false);
const open = useVModel(props, "open", emits, {
defaultValue: props.defaultOpen ?? false,
passive: (props.open === undefined) as false,
}) as Ref<boolean>;
function setOpen(value: boolean) {
open.value = value; // emits('update:open', value)
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
}
function setOpenMobile(value: boolean) {
openMobile.value = value;
}
// Helper to toggle the sidebar.
function toggleSidebar() {
return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value);
}
useEventListener("keydown", (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
toggleSidebar();
}
});
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = computed(() => (open.value ? "expanded" : "collapsed"));
provideSidebarContext({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
});
</script>
<template>
<TooltipProvider :delay-duration="0">
<div
:style="{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
}"
:class="cn('group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar', props.class)"
v-bind="$attrs"
>
<slot />
</div>
</TooltipProvider>
</template>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { useSidebar } from "./utils";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
const { toggleSidebar } = useSidebar();
</script>
<template>
<button
data-sidebar="rail"
aria-label="Toggle Sidebar"
:tabindex="-1"
title="Toggle Sidebar"
:class="
cn(
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
props.class
)
"
@click="toggleSidebar"
>
<slot />
</button>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<Separator data-sidebar="separator" :class="cn('mx-2 w-auto bg-sidebar-border', props.class)">
<slot />
</Separator>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { useSidebar } from "./utils";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import MdiMenu from "~icons/mdi/menu";
const props = defineProps<{
class?: HTMLAttributes["class"];
variant?: "ghost" | "default";
}>();
const { toggleSidebar } = useSidebar();
</script>
<template>
<Button data-sidebar="trigger" :variant="props.variant ?? 'ghost'" size="icon" :class="cn('size-9 [&_svg]:size-6', props.class)" @click="toggleSidebar">
<MdiMenu class="text-primary-foreground" />
<span class="sr-only">Toggle Sidebar</span>
</Button>
</template>

View File

@@ -0,0 +1,60 @@
import type { HTMLAttributes } from "vue";
import { cva, type VariantProps } 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 SidebarMenuLink } from "./SidebarMenuLink.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 ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none transition-[width,height,padding] focus-visible:ring-2 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]:font-medium group-data-[collapsible=icon]:!size-10 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-6 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] 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<typeof sidebarMenuButtonVariants>;

View File

@@ -0,0 +1,19 @@
import type { ComputedRef, Ref } from "vue";
import { createContext } from "radix-vue";
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 = "3.5rem";
export const SIDEBAR_KEYBOARD_SHORTCUT = "b";
export const [useSidebar, provideSidebarContext] = createContext<{
state: ComputedRef<"expanded" | "collapsed">;
open: Ref<boolean>;
setOpen: (value: boolean) => void;
isMobile: Ref<boolean>;
openMobile: Ref<boolean>;
setOpenMobile: (value: boolean) => void;
toggleSidebar: () => void;
}>("Sidebar");

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
interface SkeletonProps {
class?: HTMLAttributes["class"];
}
const props = defineProps<SkeletonProps>();
</script>
<template>
<div :class="cn('animate-pulse rounded-md bg-muted', props.class)" />
</template>

View File

@@ -0,0 +1 @@
export { default as Skeleton } from "./Skeleton.vue";

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { TooltipRoot, type TooltipRootEmits, type TooltipRootProps, useForwardPropsEmits } from "radix-vue";
const props = defineProps<TooltipRootProps>();
const emits = defineEmits<TooltipRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<TooltipRoot v-bind="forwarded">
<slot />
</TooltipRoot>
</template>

View File

@@ -0,0 +1,45 @@
<script setup lang="ts">
import {
TooltipContent,
type TooltipContentEmits,
type TooltipContentProps,
TooltipPortal,
useForwardPropsEmits,
} from "radix-vue";
import { computed, type HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
defineOptions({
inheritAttrs: false,
});
const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttributes["class"] }>(), {
sideOffset: 4,
});
const emits = defineEmits<TooltipContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<TooltipPortal>
<TooltipContent
v-bind="{ ...forwarded, ...$attrs }"
:class="
cn(
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class
)
"
>
<slot />
</TooltipContent>
</TooltipPortal>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { TooltipProvider, type TooltipProviderProps } from "radix-vue";
const props = defineProps<TooltipProviderProps>();
</script>
<template>
<TooltipProvider v-bind="props">
<slot />
</TooltipProvider>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { TooltipTrigger, type TooltipTriggerProps } from "radix-vue";
const props = defineProps<TooltipTriggerProps>();
</script>
<template>
<TooltipTrigger v-bind="props">
<slot />
</TooltipTrigger>
</template>

View File

@@ -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";

View File

@@ -17,6 +17,9 @@ export function useTheme(): UseTheme {
if (htmlEl) {
htmlEl.value?.setAttribute("data-theme", newTheme);
// FIXME: this is a hack to remove the theme class from the html element
htmlEl.value?.classList.remove(...themes);
htmlEl.value?.classList.add("theme-" + newTheme);
}
themeRef.value = newTheme;
@@ -62,3 +65,39 @@ export function useIsDark() {
return darkthemes.includes(theme.theme.value);
});
}
export const themes = [
"dark",
"theme-aqua",
"theme-black",
"theme-bumblebee",
"theme-cmyk",
"theme-corporate",
"theme-cupcake",
"theme-cyberpunk",
"theme-dark",
"theme-dracula",
"theme-emerald",
"theme-fantasy",
"theme-forest",
"theme-garden",
"theme-halloween",
"theme-light",
"theme-lofi",
"theme-luxury",
"theme-pastel",
"theme-retro",
"theme-synthwave",
"theme-valentine",
"theme-wireframe",
"theme-autumn",
"theme-business",
"theme-acid",
"theme-lemonade",
"theme-night",
"theme-coffee",
"theme-winter",
"theme-dim",
"theme-nord",
"theme-sunset",
];

View File

@@ -12,104 +12,112 @@
<LocationCreateModal v-model="modals.location" />
<QuickMenuModal v-model="modals.quickMenu" :actions="quickMenuActions" />
<AppToast />
<div class="drawer drawer-mobile">
<input id="my-drawer-2" v-model="drawerToggle" type="checkbox" class="drawer-toggle" />
<div class="drawer-content justify-center bg-base-300 pt-20 lg:pt-0">
<AppHeaderDecor v-if="preferences.displayHeaderDecor" class="-mt-10 hidden lg:block" />
<!-- Button -->
<div class="navbar drawer-button fixed top-0 z-[99] bg-primary shadow-md lg:hidden">
<label for="my-drawer-2" class="btn btn-square btn-ghost drawer-button text-base-100 lg:hidden">
<MdiMenu class="size-6" />
</label>
<NuxtLink to="/home">
<h2 class="flex text-3xl font-bold tracking-tight text-base-100">
HomeB
<AppLogo class="-mb-3 w-8" />
x
</h2>
<SidebarProvider>
<Sidebar collapsible="icon">
<SidebarHeader class="items-center bg-base-200">
<SidebarGroupLabel class="text-base">{{ $t("global.welcome", { username: username }) }}</SidebarGroupLabel>
<NuxtLink class="avatar placeholder group-data-[collapsible=icon]:hidden" to="/home">
<div class="w-24 rounded-full bg-base-300 p-4 text-neutral-content">
<AppLogo />
</div>
</NuxtLink>
</div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
class="flex justify-center bg-primary text-primary-foreground shadow hover:bg-primary/90 group-data-[collapsible=icon]:justify-start"
:tooltip="$t('global.create')"
hotkey="Shortcut: Ctrl+`"
>
<MdiPlus />
<span>
{{ $t("global.create") }}
</span>
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent class="min-w-[var(--radix-dropdown-menu-trigger-width)]">
<DropdownMenuItem
v-for="btn in dropdown"
:key="btn.id"
class="group cursor-pointer text-lg"
@click="btn.action"
>
{{ btn.name.value }}
<kbd v-if="btn.shortcut" class="kbd kbd-sm ml-auto hidden text-primary group-hover:inline">{{
btn.shortcut.replaceAll("Shift+", "⇧")
}}</kbd>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarHeader>
<slot></slot>
<footer v-if="status" class="bottom-0 w-full bg-base-300 pb-4 text-center text-secondary-content">
<p class="text-center text-sm">
<a href="https://github.com/sysadminsmedia/homebox/releases/tag/{{ status.build.version }}" target="_blank">
{{ $t("global.version", { version: status.build.version }) }} ~
{{ $t("global.build", { build: status.build.commit }) }}</a
>
~
<a href="https://homebox.software/en/api.html" target="_blank">API</a>
</p>
</footer>
</div>
<SidebarContent class="bg-base-200">
<SidebarGroup>
<SidebarMenu>
<SidebarMenuItem v-for="n in nav" :key="n.id">
<SidebarMenuLink
:href="n.to"
:class="{
'bg-secondary text-secondary-foreground': n.active?.value,
'text-nowrap': typeof locale === 'string' && locale.startsWith('zh-'),
'hover:bg-base-300': !n.active?.value,
}"
:tooltip="n.name.value"
>
<component :is="n.icon" />
<span>{{ n.name.value }}</span>
</SidebarMenuLink>
</SidebarMenuItem>
</SidebarMenu></SidebarGroup
>
</SidebarContent>
<!-- Sidebar -->
<div class="drawer-side shadow-lg">
<label for="my-drawer-2" class="drawer-overlay"></label>
<!-- Top Section -->
<div class="flex min-w-40 max-w-min flex-col bg-base-200 p-5 md:py-10">
<div class="space-y-8">
<div class="flex flex-col items-center gap-4">
<p>{{ $t("global.welcome", { username: username }) }}</p>
<NuxtLink class="avatar placeholder" to="/home">
<div class="w-24 rounded-full bg-base-300 p-4 text-neutral-content">
<AppLogo />
</div>
</NuxtLink>
</div>
<div class="flex flex-col bg-base-200">
<div class="mb-6">
<div class="dropdown tooltip visible w-full" data-tip="Shortcut: Ctrl+`">
<label tabindex="0" class="text-no-transform btn btn-primary btn-block text-lg">
<span>
<MdiPlus class="-ml-1 mr-1" />
</span>
{{ $t("global.create") }}
</label>
<ul tabindex="0" class="dropdown-content menu rounded-box w-full bg-base-100 p-2 shadow">
<li v-for="btn in dropdown" :key="btn.id">
<button class="group" @click="btn.action">
{{ btn.name.value }}
<kbd
v-if="btn.shortcut"
class="kbd kbd-sm ml-auto hidden text-neutral-400 group-hover:inline"
>{{ btn.shortcut.replaceAll("Shift+", "⇧") }}</kbd
>
</button>
</li>
</ul>
</div>
</div>
<ul class="menu mx-auto flex flex-col gap-2">
<li v-for="n in nav" :key="n.id" class="text-xl" @click="unfocus">
<NuxtLink
v-if="n.to"
class="rounded-btn"
:to="n.to"
:class="{
'bg-secondary text-secondary-content': n.active?.value,
'text-nowrap': typeof locale === 'string' && locale.startsWith('zh-'),
}"
>
<component :is="n.icon" class="mr-4 size-6" />
{{ n.name.value }}
</NuxtLink>
</li>
</ul>
</div>
</div>
<!-- Bottom -->
<div class="mx-2 mt-auto flex flex-col">
<button class="rounded-btn p-3 transition-colors hover:bg-base-300" @click="logout">
<SidebarFooter class="bg-base-200">
<SidebarMenuButton
class="flex justify-center hover:bg-base-300 group-data-[collapsible=icon]:justify-start group-data-[collapsible=icon]:bg-destructive group-data-[collapsible=icon]:text-destructive-foreground group-data-[collapsible=icon]:shadow-sm group-data-[collapsible=icon]:hover:bg-destructive/90"
:tooltip="$t('global.sign_out')"
@click="logout"
>
<MdiLogout />
<span>
{{ $t("global.sign_out") }}
</button>
</span>
</SidebarMenuButton>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset class="min-h-screen bg-base-300">
<div class="justify-center pt-20 lg:pt-0">
<AppHeaderDecor v-if="preferences.displayHeaderDecor" class="-mt-10 hidden lg:block" />
<SidebarTrigger class="absolute left-2 top-2 hidden lg:flex" variant="default" />
<div class="fixed top-0 z-20 flex h-16 w-full items-center gap-2 bg-primary p-2 shadow-md lg:hidden">
<SidebarTrigger />
<NuxtLink to="/home">
<h2 class="flex text-3xl font-bold tracking-tight text-base-100">
HomeB
<AppLogo class="-mb-3 w-8" />
x
</h2>
</NuxtLink>
</div>
<slot></slot>
<footer v-if="status" class="bottom-0 w-full bg-base-300 pb-4 text-center text-secondary-content">
<p class="text-center text-sm">
<a
href="https://github.com/sysadminsmedia/homebox/releases/tag/{{ status.build.version }}"
target="_blank"
>
{{ $t("global.version", { version: status.build.version }) }} ~
{{ $t("global.build", { build: status.build.commit }) }}</a
>
~
<a href="https://homebox.software/en/api.html" target="_blank">API</a>
</p>
</footer>
</div>
</div>
</div>
</SidebarInset>
</SidebarProvider>
</div>
</template>
@@ -125,8 +133,30 @@
import MdiAccount from "~icons/mdi/account";
import MdiCog from "~icons/mdi/cog";
import MdiWrench from "~icons/mdi/wrench";
import MdiMenu from "~icons/mdi/menu";
import MdiPlus from "~icons/mdi/plus";
import MdiLogout from "~icons/mdi/logout";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarInset,
SidebarRail,
SidebarTrigger,
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarMenuLink,
} from "@/components/ui/sidebar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
const { t, locale } = useI18n();
const username = computed(() => authCtx.user?.name || "User");
@@ -218,13 +248,6 @@
const route = useRoute();
const drawerToggle = ref();
function unfocus() {
// unfocus current element
drawerToggle.value = false;
}
const nav = [
{
icon: MdiHome,

6
frontend/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -8,7 +8,14 @@ export default defineNuxtConfig({
transpile: ["vue-i18n"],
},
modules: ["@nuxtjs/tailwindcss", "@pinia/nuxt", "@vueuse/nuxt", "@vite-pwa/nuxt", "unplugin-icons/nuxt"],
modules: [
"@nuxtjs/tailwindcss",
"@pinia/nuxt",
"@vueuse/nuxt",
"@vite-pwa/nuxt",
"unplugin-icons/nuxt",
"shadcn-nuxt",
],
nitro: {
devProxy: {

View File

@@ -5,9 +5,9 @@
"dev": "nuxt dev",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"lint": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore .",
"lint:fix": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore . --fix",
"lint:ci": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore . --max-warnings 1",
"lint": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore --ignore-pattern \"components/ui\" .",
"lint:fix": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore --ignore-pattern \"components/ui\" . --fix",
"lint:ci": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore --ignore-pattern \"components/ui\" . --max-warnings 1",
"typecheck": "pnpm vue-tsc --noEmit",
"test:ci": "TEST_SHUTDOWN_API_SERVER=true vitest --run --config ./test/vitest.config.ts",
"test:local": "TEST_SHUTDOWN_API_SERVER=false && vitest --run --config ./test/vitest.config.ts",
@@ -33,6 +33,7 @@
"isomorphic-fetch": "^3.0.0",
"nuxt": "3.12.4",
"prettier": "^3.4.2",
"shadcn-nuxt": "0.11.3",
"typescript": "5.6.2",
"unplugin-icons": "^0.18.5",
"vite-plugin-eslint": "^1.8.1",
@@ -42,6 +43,7 @@
},
"dependencies": {
"@headlessui/vue": "^1.7.23",
"@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/tailwindcss": "^6.12.2",
"@pinia/nuxt": "^0.5.5",
"@tailwindcss/aspect-ratio": "^0.4.2",
@@ -49,20 +51,27 @@
"@tailwindcss/typography": "^0.5.15",
"@types/lunr": "^2.3.7",
"@vuepic/vue-datepicker": "^8.8.1",
"@vueuse/core": "^12.5.0",
"@vueuse/nuxt": "^10.11.1",
"@vueuse/router": "^10.11.1",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"daisyui": "^2.52.0",
"date-fns": "^3.6.0",
"dompurify": "^3.2.3",
"h3": "^1.13.0",
"http-proxy": "^1.18.1",
"lucide-vue-next": "^0.474.0",
"lunr": "^2.3.9",
"markdown-it": "^14.1.0",
"pinia": "^2.3.0",
"postcss": "^8.4.49",
"radix-vue": "^1.9.12",
"semver": "^7.6.3",
"tailwind-merge": "^2.6.0",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"vue": "3.4.8",
"vue-router": "^4.5.0"
}

View File

@@ -478,10 +478,11 @@
{{ $t("profile.display_header", { currentValue: preferences.displayHeaderDecor }) }}
</BaseButton>
</div>
<div class="rounded-box grid grid-cols-1 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
<div class="homebox rounded-box grid grid-cols-1 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
<div
v-for="theme in themes"
:key="theme.value"
:class="'theme-' + theme.value"
class="overflow-hidden rounded-lg border border-base-content/20 outline-2 outline-offset-2 outline-base-content hover:border-base-content/40"
:data-theme="theme.value"
:data-set-theme="theme.value"

View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<div>testing</div>
</template>

229
frontend/pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@headlessui/vue':
specifier: ^1.7.23
version: 1.7.23(vue@3.4.8(typescript@5.6.2))
'@nuxtjs/color-mode':
specifier: ^3.5.2
version: 3.5.2(magicast@0.3.5)(rollup@4.29.1)
'@nuxtjs/tailwindcss':
specifier: ^6.12.2
version: 6.12.2(magicast@0.3.5)(rollup@4.29.1)
@@ -32,6 +35,9 @@ importers:
'@vuepic/vue-datepicker':
specifier: ^8.8.1
version: 8.8.1(vue@3.4.8(typescript@5.6.2))
'@vueuse/core':
specifier: ^12.5.0
version: 12.5.0(typescript@5.6.2)
'@vueuse/nuxt':
specifier: ^10.11.1
version: 10.11.1(magicast@0.3.5)(nuxt@3.12.4(@parcel/watcher@2.5.0)(@types/node@22.10.5)(db0@0.2.1)(eslint@8.57.1)(ioredis@5.4.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.29.1)(terser@5.37.0)(typescript@5.6.2)(vite@5.4.11(@types/node@22.10.5)(terser@5.37.0))(vue-tsc@2.1.6(typescript@5.6.2)))(rollup@4.29.1)(vue@3.4.8(typescript@5.6.2))
@@ -41,6 +47,12 @@ importers:
autoprefixer:
specifier: ^10.4.20
version: 10.4.20(postcss@8.4.49)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
daisyui:
specifier: ^2.52.0
version: 2.52.0(autoprefixer@10.4.20(postcss@8.4.49))(postcss@8.4.49)
@@ -56,6 +68,9 @@ importers:
http-proxy:
specifier: ^1.18.1
version: 1.18.1
lucide-vue-next:
specifier: ^0.474.0
version: 0.474.0(vue@3.4.8(typescript@5.6.2))
lunr:
specifier: ^2.3.9
version: 2.3.9
@@ -68,12 +83,21 @@ importers:
postcss:
specifier: ^8.4.49
version: 8.4.49
radix-vue:
specifier: ^1.9.12
version: 1.9.12(vue@3.4.8(typescript@5.6.2))
semver:
specifier: ^7.6.3
version: 7.6.3
tailwind-merge:
specifier: ^2.6.0
version: 2.6.0
tailwindcss:
specifier: ^3.4.17
version: 3.4.17
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.17)
vue:
specifier: 3.4.8
version: 3.4.8(typescript@5.6.2)
@@ -135,6 +159,9 @@ importers:
prettier:
specifier: ^3.4.2
version: 3.4.2
shadcn-nuxt:
specifier: 0.11.3
version: 0.11.3(magicast@0.3.5)(rollup@4.29.1)
typescript:
specifier: 5.6.2
version: 5.6.2
@@ -1173,6 +1200,18 @@ packages:
resolution: {integrity: sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'}
'@floating-ui/core@1.6.9':
resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==}
'@floating-ui/dom@1.6.13':
resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==}
'@floating-ui/utils@0.2.9':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
'@floating-ui/vue@1.1.6':
resolution: {integrity: sha512-XFlUzGHGv12zbgHNk5FN2mUB7ROul3oG2ENdTpWdE+qMFxyNxWSRmsoyhiEnpmabNm6WnUvR1OvJfUfN4ojC1A==}
'@formatjs/ecma402-abstract@2.3.2':
resolution: {integrity: sha512-6sE5nyvDloULiyOMbOTJEEgWL32w+VHkZQs8S02Lnn8Y/O5aQhjOEXwWzvR7SsBE/exxlSpY2EsWZgqHbtLatg==}
@@ -1216,6 +1255,12 @@ packages:
'@iconify/utils@2.2.1':
resolution: {integrity: sha512-0/7J7hk4PqXmxo5PDBDxmnecw5PxklZJfNjIVG9FM0mEfVrvfudS22rYWsqVk6gR3UJ/mSYS90X4R3znXnqfNA==}
'@internationalized/date@3.7.0':
resolution: {integrity: sha512-VJ5WS3fcVx0bejE/YHfbDKR/yawZgKqn/if+oEeLqNwBtPzVB06olkfcnojTmEMX+gTpH+FlQ69SHNitJ8/erQ==}
'@internationalized/number@3.6.0':
resolution: {integrity: sha512-PtrRcJVy7nw++wn4W2OuePQQfTqDzfusSuY1QTtui4wa7r+rGVtR75pO8CyKvHvzyQYi3Q1uO5sY0AsB4e65Bw==}
'@intlify/bundle-utils@8.0.0':
resolution: {integrity: sha512-1B++zykRnMwQ+20SpsZI1JCnV/YJt9Oq7AGlEurzkWJOFtFAVqaGc/oV36PBRYeiKnTbY9VYfjBimr2Vt42wLQ==}
engines: {node: '>= 14.16'}
@@ -1383,6 +1428,9 @@ packages:
peerDependencies:
vue: ^3.3.4
'@nuxtjs/color-mode@3.5.2':
resolution: {integrity: sha512-cC6RfgZh3guHBMLLjrBB2Uti5eUoGM9KyauOaYS9ETmxNWBMTvpgjvSiSJp1OFljIXPIqVTJ3xtJpSNZiO3ZaA==}
'@nuxtjs/eslint-config-typescript@12.1.0':
resolution: {integrity: sha512-l2fLouDYwdAvCZEEw7wGxOBj+i8TQcHFu3zMPTLqKuv1qu6WcZIr0uztkbaa8ND1uKZ9YPqKx6UlSOjM4Le69Q==}
peerDependencies:
@@ -1396,6 +1444,9 @@ packages:
'@nuxtjs/tailwindcss@6.12.2':
resolution: {integrity: sha512-qPJiFH67CkTj/2kBGBzqXihOD1rQXMsbVS4vdQvfBxOBLPfGhU1yw7AATdhPl2BBjO2krjJLuZj39t7dnDYOwg==}
'@oxc-parser/wasm@0.29.0':
resolution: {integrity: sha512-Ks5yFtJHypJZUdSNLImwtfkDt0/8ll9CDPyfmldhudtKB/1o6F/WQGWA4Oo+bCskDIp2MPKc3HfHccN3ALhtSg==}
'@parcel/watcher-android-arm64@2.5.0':
resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==}
engines: {node: '>= 10.0.0'}
@@ -1723,6 +1774,9 @@ packages:
'@surma/rollup-plugin-off-main-thread@2.2.3':
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@tailwindcss/aspect-ratio@0.4.2':
resolution: {integrity: sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==}
peerDependencies:
@@ -2041,9 +2095,15 @@ packages:
'@vueuse/core@10.11.1':
resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
'@vueuse/core@12.5.0':
resolution: {integrity: sha512-GVyH1iYqNANwcahAx8JBm6awaNgvR/SwZ1fjr10b8l1HIgDp82ngNbfzJUgOgWEoxjL+URAggnlilAEXwCOZtg==}
'@vueuse/metadata@10.11.1':
resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
'@vueuse/metadata@12.5.0':
resolution: {integrity: sha512-Ui7Lo2a7AxrMAXRF+fAp9QsXuwTeeZ8fIB9wsLHqzq9MQk+2gMYE2IGJW48VMJ8ecvCB3z3GsGLKLbSasQ5Qlg==}
'@vueuse/nuxt@10.11.1':
resolution: {integrity: sha512-UiaYSIwOkmUVn8Gl1AqtLWYR12flO+8sEu9X0Y1fNjSR7EWy9jMuiCvOGqwtoeTsqfHrivl0d5HfMzr11GFnMA==}
peerDependencies:
@@ -2057,6 +2117,9 @@ packages:
'@vueuse/shared@10.11.1':
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
'@vueuse/shared@12.5.0':
resolution: {integrity: sha512-vMpcL1lStUU6O+kdj6YdHDixh0odjPAUM15uJ9f7MY781jcYkIwFA4iv2EfoIPO6vBmvutI1HxxAwmf0cx5ISQ==}
abbrev@2.0.0:
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -2152,6 +2215,10 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
aria-hidden@1.2.4:
resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
engines: {node: '>=10'}
array-buffer-byte-length@1.0.2:
resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
engines: {node: '>= 0.4'}
@@ -2381,6 +2448,9 @@ packages:
citty@0.1.6:
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
clean-regexp@1.0.0:
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
engines: {node: '>=4'}
@@ -2396,6 +2466,10 @@ packages:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
cluster-key-slot@1.1.2:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
@@ -3896,6 +3970,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-vue-next@0.474.0:
resolution: {integrity: sha512-bQaSBjfJ33xiPQCxCf4JD3rcUgZFgWZzxSY8SScNa4Mcq2vWGlbvQx6icTL1UXRqsxzfoT13RXawePSmgg4iWw==}
peerDependencies:
vue: '>=3.0.1'
lunr@2.3.9:
resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
@@ -4659,6 +4738,11 @@ packages:
queue-tick@1.0.1:
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
radix-vue@1.9.12:
resolution: {integrity: sha512-zkr66Jqxbej4+oR6O/pZRzyM/VZi66ndbyIBZQjJKAXa1lIoYReZJse6W1EEDZKXknD7rXhpS+jM9Sr23lIqfg==}
peerDependencies:
vue: '>= 3.2.0'
radix3@1.1.2:
resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
@@ -4903,6 +4987,9 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
shadcn-nuxt@0.11.3:
resolution: {integrity: sha512-Q0OxqTEbTmtbm/4wV9jd/7DTQYn65AGHkVJZjjHQdHGHTHCwhAbZ2jlCPnMuzJ8kgSsJyXFGNcImAphDCjjV5w==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -5146,6 +5233,14 @@ packages:
peerDependencies:
tailwindcss: 1 || 2 || 2.0.1-compat || 3
tailwind-merge@2.6.0:
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
tailwindcss-animate@1.0.7:
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders'
tailwindcss@3.4.17:
resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
engines: {node: '>=14.0.0'}
@@ -5307,6 +5402,11 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
typescript@5.7.3:
resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
engines: {node: '>=14.17'}
hasBin: true
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
@@ -6869,6 +6969,26 @@ snapshots:
'@faker-js/faker@8.4.1': {}
'@floating-ui/core@1.6.9':
dependencies:
'@floating-ui/utils': 0.2.9
'@floating-ui/dom@1.6.13':
dependencies:
'@floating-ui/core': 1.6.9
'@floating-ui/utils': 0.2.9
'@floating-ui/utils@0.2.9': {}
'@floating-ui/vue@1.1.6(vue@3.4.8(typescript@5.6.2))':
dependencies:
'@floating-ui/dom': 1.6.13
'@floating-ui/utils': 0.2.9
vue-demi: 0.14.10(vue@3.4.8(typescript@5.6.2))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@formatjs/ecma402-abstract@2.3.2':
dependencies:
'@formatjs/fast-memoize': 2.2.6
@@ -6931,6 +7051,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@internationalized/date@3.7.0':
dependencies:
'@swc/helpers': 0.5.15
'@internationalized/number@3.6.0':
dependencies:
'@swc/helpers': 0.5.15
'@intlify/bundle-utils@8.0.0(vue-i18n@9.14.2(vue@3.4.8(typescript@5.6.2)))':
dependencies:
'@intlify/message-compiler': 9.14.2
@@ -7326,9 +7454,20 @@ snapshots:
- vti
- vue-tsc
'@nuxtjs/color-mode@3.5.2(magicast@0.3.5)(rollup@4.29.1)':
dependencies:
'@nuxt/kit': 3.15.0(magicast@0.3.5)(rollup@4.29.1)
pathe: 1.1.2
pkg-types: 1.3.0
semver: 7.6.3
transitivePeerDependencies:
- magicast
- rollup
- supports-color
'@nuxtjs/eslint-config-typescript@12.1.0(eslint@8.57.1)(typescript@5.6.2)':
dependencies:
'@nuxtjs/eslint-config': 12.0.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1)
'@nuxtjs/eslint-config': 12.0.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1)
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2)
'@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.6.2)
eslint: 8.57.1
@@ -7341,10 +7480,10 @@ snapshots:
- supports-color
- typescript
'@nuxtjs/eslint-config@12.0.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1)':
'@nuxtjs/eslint-config@12.0.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1)':
dependencies:
eslint: 8.57.1
eslint-config-standard: 17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)
eslint-config-standard: 17.1.0(eslint-plugin-import@2.31.0)(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1)
eslint-plugin-n: 15.7.0(eslint@8.57.1)
eslint-plugin-node: 11.1.0(eslint@8.57.1)
@@ -7379,6 +7518,8 @@ snapshots:
- supports-color
- ts-node
'@oxc-parser/wasm@0.29.0': {}
'@parcel/watcher-android-arm64@2.5.0':
optional: true
@@ -7681,6 +7822,10 @@ snapshots:
magic-string: 0.25.9
string.prototype.matchall: 4.0.12
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
'@tailwindcss/aspect-ratio@0.4.2(tailwindcss@3.4.17)':
dependencies:
tailwindcss: 3.4.17
@@ -8168,8 +8313,19 @@ snapshots:
- '@vue/composition-api'
- vue
'@vueuse/core@12.5.0(typescript@5.6.2)':
dependencies:
'@types/web-bluetooth': 0.0.20
'@vueuse/metadata': 12.5.0
'@vueuse/shared': 12.5.0(typescript@5.6.2)
vue: 3.5.13(typescript@5.6.2)
transitivePeerDependencies:
- typescript
'@vueuse/metadata@10.11.1': {}
'@vueuse/metadata@12.5.0': {}
'@vueuse/nuxt@10.11.1(magicast@0.3.5)(nuxt@3.12.4(@parcel/watcher@2.5.0)(@types/node@22.10.5)(db0@0.2.1)(eslint@8.57.1)(ioredis@5.4.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.29.1)(terser@5.37.0)(typescript@5.6.2)(vite@5.4.11(@types/node@22.10.5)(terser@5.37.0))(vue-tsc@2.1.6(typescript@5.6.2)))(rollup@4.29.1)(vue@3.4.8(typescript@5.6.2))':
dependencies:
'@nuxt/kit': 3.15.0(magicast@0.3.5)(rollup@4.29.1)
@@ -8201,6 +8357,12 @@ snapshots:
- '@vue/composition-api'
- vue
'@vueuse/shared@12.5.0(typescript@5.6.2)':
dependencies:
vue: 3.5.13(typescript@5.6.2)
transitivePeerDependencies:
- typescript
abbrev@2.0.0: {}
abort-controller@3.0.0:
@@ -8293,6 +8455,10 @@ snapshots:
argparse@2.0.1: {}
aria-hidden@1.2.4:
dependencies:
tslib: 2.8.1
array-buffer-byte-length@1.0.2:
dependencies:
call-bound: 1.0.3
@@ -8580,6 +8746,10 @@ snapshots:
dependencies:
consola: 3.3.3
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
clean-regexp@1.0.0:
dependencies:
escape-string-regexp: 1.0.5
@@ -8598,6 +8768,8 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
clsx@2.1.1: {}
cluster-key-slot@1.1.2: {}
co@4.6.0: {}
@@ -9160,7 +9332,7 @@ snapshots:
dependencies:
eslint: 8.57.1
eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1):
eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0)(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1):
dependencies:
eslint: 8.57.1
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1)
@@ -9191,7 +9363,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1):
eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -9225,7 +9397,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -10259,6 +10431,10 @@ snapshots:
dependencies:
yallist: 3.1.1
lucide-vue-next@0.474.0(vue@3.4.8(typescript@5.6.2)):
dependencies:
vue: 3.4.8(typescript@5.6.2)
lunr@2.3.9: {}
magic-string-ast@0.6.3:
@@ -10596,7 +10772,7 @@ snapshots:
unenv: 1.10.0
unimport: 3.14.5(rollup@4.29.1)
unplugin: 1.16.0
unplugin-vue-router: 0.10.9(rollup@4.29.1)(vue-router@4.5.0(vue@3.5.13(typescript@5.6.2)))(vue@3.5.13(typescript@5.6.2))
unplugin-vue-router: 0.10.9(rollup@4.29.1)(vue-router@4.5.0(vue@3.4.8(typescript@5.6.2)))(vue@3.5.13(typescript@5.6.2))
unstorage: 1.14.4(db0@0.2.1)(ioredis@5.4.2)
untyped: 1.5.2
vue: 3.5.13(typescript@5.6.2)
@@ -11144,6 +11320,23 @@ snapshots:
queue-tick@1.0.1: {}
radix-vue@1.9.12(vue@3.4.8(typescript@5.6.2)):
dependencies:
'@floating-ui/dom': 1.6.13
'@floating-ui/vue': 1.1.6(vue@3.4.8(typescript@5.6.2))
'@internationalized/date': 3.7.0
'@internationalized/number': 3.6.0
'@tanstack/vue-virtual': 3.11.2(vue@3.4.8(typescript@5.6.2))
'@vueuse/core': 10.11.1(vue@3.4.8(typescript@5.6.2))
'@vueuse/shared': 10.11.1(vue@3.4.8(typescript@5.6.2))
aria-hidden: 1.2.4
defu: 6.1.4
fast-deep-equal: 3.1.3
nanoid: 5.0.9
vue: 3.4.8(typescript@5.6.2)
transitivePeerDependencies:
- '@vue/composition-api'
radix3@1.1.2: {}
randombytes@2.1.0:
@@ -11442,6 +11635,16 @@ snapshots:
setprototypeof@1.2.0: {}
shadcn-nuxt@0.11.3(magicast@0.3.5)(rollup@4.29.1):
dependencies:
'@nuxt/kit': 3.15.0(magicast@0.3.5)(rollup@4.29.1)
'@oxc-parser/wasm': 0.29.0
typescript: 5.7.3
transitivePeerDependencies:
- magicast
- rollup
- supports-color
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@@ -11715,6 +11918,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
tailwind-merge@2.6.0: {}
tailwindcss-animate@1.0.7(tailwindcss@3.4.17):
dependencies:
tailwindcss: 3.4.17
tailwindcss@3.4.17:
dependencies:
'@alloc/quick-lru': 5.2.0
@@ -11902,6 +12111,8 @@ snapshots:
typescript@5.6.2: {}
typescript@5.7.3: {}
uc.micro@2.1.0: {}
ufo@1.5.4: {}
@@ -11993,7 +12204,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
unplugin-vue-router@0.10.9(rollup@4.29.1)(vue-router@4.5.0(vue@3.5.13(typescript@5.6.2)))(vue@3.5.13(typescript@5.6.2)):
unplugin-vue-router@0.10.9(rollup@4.29.1)(vue-router@4.5.0(vue@3.4.8(typescript@5.6.2)))(vue@3.5.13(typescript@5.6.2)):
dependencies:
'@babel/types': 7.26.3
'@rollup/pluginutils': 5.1.4(rollup@4.29.1)
@@ -12010,7 +12221,7 @@ snapshots:
unplugin: 2.0.0-beta.1
yaml: 2.7.0
optionalDependencies:
vue-router: 4.5.0(vue@3.5.13(typescript@5.6.2))
vue-router: 4.5.0(vue@3.4.8(typescript@5.6.2))
transitivePeerDependencies:
- rollup
- vue

View File

@@ -3,7 +3,10 @@ try {
const theme = JSON.parse(
localStorage.getItem('homebox/preferences/location')
).theme;
if (theme) document.documentElement.setAttribute('data-theme', theme);
if (theme) {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.classList.add('theme-' + theme);
}
} catch (e) {
console.error('Failed to set theme', e);
}

View File

@@ -1,8 +1,128 @@
import { config } from "dotenv";
config();
// check if DISABLE_DAISYUI is set to true in the environment
const isDisabled = process.env.DISABLE_DAISYUI === "true";
if (isDisabled) {
console.log("DAISYUI DISABLED");
}
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./app.vue", "./{components,pages,layouts}/**/*.{vue,js,ts,jsx,tsx}"],
darkMode: "class", // or 'media' or 'class'
darkMode: ["class"],
safelist: [
"dark",
"theme-aqua",
"theme-black",
"theme-bumblebee",
"theme-cmyk",
"theme-corporate",
"theme-cupcake",
"theme-cyberpunk",
"theme-dark",
"theme-dracula",
"theme-emerald",
"theme-fantasy",
"theme-forest",
"theme-garden",
"theme-halloween",
"theme-light",
"theme-lofi",
"theme-luxury",
"theme-pastel",
"theme-retro",
"theme-synthwave",
"theme-valentine",
"theme-wireframe",
"theme-autumn",
"theme-business",
"theme-acid",
"theme-lemonade",
"theme-night",
"theme-coffee",
"theme-winter",
"theme-dim",
"theme-nord",
"theme-sunset",
],
prefix: "",
theme: {
extend: {},
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
xl: "calc(var(--radius) + 4px)",
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
"collapsible-down": {
from: { height: 0 },
to: { height: "var(--radix-collapsible-content-height)" },
},
"collapsible-up": {
from: { height: "var(--radix-collapsible-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"collapsible-down": "collapsible-down 0.2s ease-in-out",
"collapsible-up": "collapsible-up 0.2s ease-in-out",
},
},
},
daisyui: {
themes: [
@@ -50,8 +170,12 @@ module.exports = {
"winter",
],
},
variants: {
extend: {},
},
plugins: [require("@tailwindcss/aspect-ratio"), require("@tailwindcss/typography"), require("daisyui")],
plugins: isDisabled
? [require("@tailwindcss/aspect-ratio"), require("@tailwindcss/typography"), require("tailwindcss-animate")]
: [
require("@tailwindcss/aspect-ratio"),
require("@tailwindcss/typography"),
require("daisyui"),
require("tailwindcss-animate"),
],
};