create-mantiq 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/skeleton/.env.example +64 -0
- package/skeleton/README.md +46 -0
- package/skeleton/app/Console/Commands/.gitkeep +0 -0
- package/skeleton/app/Enums/UserStatus.ts +7 -0
- package/skeleton/app/Http/Controllers/HomeController.ts +78 -0
- package/skeleton/app/Http/Middleware/.gitkeep +0 -0
- package/skeleton/app/Models/User.ts +7 -0
- package/skeleton/app/Providers/AppServiceProvider.ts +25 -0
- package/skeleton/app/Providers/DatabaseServiceProvider.ts +17 -0
- package/skeleton/bootstrap/.gitkeep +0 -0
- package/skeleton/config/ai.ts +51 -0
- package/skeleton/config/app.ts +108 -0
- package/skeleton/config/auth.ts +51 -0
- package/skeleton/config/broadcasting.ts +93 -0
- package/skeleton/config/cache.ts +61 -0
- package/skeleton/config/cors.ts +77 -0
- package/skeleton/config/database.ts +120 -0
- package/skeleton/config/filesystem.ts +58 -0
- package/skeleton/config/hashing.ts +47 -0
- package/skeleton/config/heartbeat.ts +112 -0
- package/skeleton/config/logging.ts +58 -0
- package/skeleton/config/mail.ts +93 -0
- package/skeleton/config/notify.ts +141 -0
- package/skeleton/config/queue.ts +59 -0
- package/skeleton/config/search.ts +96 -0
- package/skeleton/config/services.ts +110 -0
- package/skeleton/config/session.ts +84 -0
- package/skeleton/config/vite.ts +33 -0
- package/skeleton/database/factories/.gitkeep +0 -0
- package/skeleton/database/migrations/001_create_users_table.ts +19 -0
- package/skeleton/database/migrations/002_create_personal_access_tokens_table.ts +22 -0
- package/skeleton/database/seeders/DatabaseSeeder.ts +7 -0
- package/skeleton/index.ts +20 -0
- package/skeleton/mantiq.ts +8 -0
- package/skeleton/package.json +34 -0
- package/skeleton/public/.gitkeep +0 -0
- package/skeleton/routes/api.ts +8 -0
- package/skeleton/routes/channels.ts +23 -0
- package/skeleton/routes/console.ts +24 -0
- package/skeleton/routes/web.ts +6 -0
- package/skeleton/storage/cache/.gitkeep +0 -0
- package/skeleton/storage/framework/.gitkeep +0 -0
- package/skeleton/tests/feature/api.test.ts +14 -0
- package/skeleton/tests/feature/home.test.ts +17 -0
- package/skeleton/tests/unit/example.test.ts +11 -0
- package/skeleton/tsconfig.json +27 -0
- package/src/index.ts +289 -25
- package/src/templates.ts +141 -945
- package/src/terminal.ts +64 -0
- package/stubs/api-only/routes/api.ts.stub +24 -0
- package/stubs/api-only/tests/feature/token-auth.test.ts.stub +69 -0
- package/stubs/auth/api/app/Http/Controllers/ApiAuthController.ts.stub +57 -0
- package/stubs/auth/api/routes/api.ts.stub +24 -0
- package/stubs/auth/api/tests/feature/token-auth.test.ts.stub +69 -0
- package/stubs/auth/shared/app/Http/Requests/LoginRequest.ts.stub +10 -0
- package/stubs/auth/shared/app/Http/Requests/RegisterRequest.ts.stub +11 -0
- package/stubs/auth/web/app/Http/Controllers/AuthController.ts.stub +43 -0
- package/stubs/auth/web/app/Http/Controllers/PageController.ts.stub +66 -0
- package/stubs/auth/web/routes/web.ts.stub +25 -0
- package/stubs/auth/web/svelte/src/App.svelte.stub +77 -0
- package/stubs/auth/web/svelte/src/pages.ts.stub +17 -0
- package/stubs/auth/web/tests/feature/auth.test.ts.stub +69 -0
- package/stubs/auth/web/vue/src/App.vue.stub +74 -0
- package/stubs/auth/web/vue/src/pages.ts.stub +17 -0
- package/stubs/manifest.json +630 -2
- package/stubs/noauth/app/Http/Controllers/PageController.ts.stub +41 -0
- package/stubs/noauth/app/Models/User.ts.stub +5 -0
- package/stubs/noauth/database/migrations/001_create_users_table.ts.stub +17 -0
- package/stubs/noauth/routes/api.ts.stub +16 -0
- package/stubs/noauth/routes/web.ts.stub +15 -0
- package/stubs/noauth/svelte/src/App.svelte.stub +68 -0
- package/stubs/noauth/svelte/src/pages.ts.stub +7 -0
- package/stubs/noauth/vue/src/App.vue.stub +62 -0
- package/stubs/noauth/vue/src/pages.ts.stub +7 -0
- package/stubs/react/src/App.tsx.stub +4 -2
- package/stubs/react/src/components/layout/search-dialog.tsx.stub +2 -2
- package/stubs/react/src/components/layout/sidebar-data.ts.stub +2 -2
- package/stubs/react/src/lib/api.ts.stub +30 -6
- package/stubs/react/src/pages/Login.tsx.stub +3 -3
- package/stubs/react/src/pages/users/dialogs.tsx.stub +7 -26
- package/stubs/react/vite.config.ts.stub +26 -3
- package/stubs/shared/app/Http/Controllers/ApiAuthController.ts.stub +57 -0
- package/stubs/shared/app/Http/Controllers/AuthController.ts.stub +14 -38
- package/stubs/shared/app/Http/Controllers/PageController.ts.stub +3 -3
- package/stubs/shared/app/Http/Controllers/UserController.ts.stub +61 -0
- package/stubs/shared/app/Http/Requests/LoginRequest.ts.stub +10 -0
- package/stubs/shared/app/Http/Requests/RegisterRequest.ts.stub +11 -0
- package/stubs/shared/app/Http/Requests/StoreUserRequest.ts.stub +11 -0
- package/stubs/shared/app/Http/Requests/UpdateUserRequest.ts.stub +11 -0
- package/stubs/shared/config/app.ts.stub +36 -0
- package/stubs/shared/config/vite.ts.stub +8 -0
- package/stubs/shared/database/factories/UserFactory.ts.stub +4 -6
- package/stubs/shared/routes/api.ts.stub +12 -102
- package/stubs/shared/routes/web.ts.stub +5 -3
- package/stubs/shared/tests/feature/auth.test.ts.stub +69 -0
- package/stubs/shared/tests/feature/users.test.ts.stub +90 -0
- package/stubs/svelte/src/App.svelte.stub +1 -1
- package/stubs/svelte/src/lib/api.ts.stub +30 -6
- package/stubs/svelte/src/main.ts.stub +3 -1
- package/stubs/svelte/src/pages/Login.svelte.stub +3 -3
- package/stubs/svelte/vite.config.ts.stub +20 -1
- package/stubs/tailwind-only/react/src/components/layout/app-sidebar.tsx.stub +68 -0
- package/stubs/tailwind-only/react/src/components/layout/authenticated-layout.tsx.stub +57 -0
- package/stubs/tailwind-only/react/src/components/layout/header.tsx.stub +52 -0
- package/stubs/tailwind-only/react/src/components/layout/main.tsx.stub +21 -0
- package/stubs/tailwind-only/react/src/components/layout/nav-group.tsx.stub +185 -0
- package/stubs/tailwind-only/react/src/components/layout/nav-user.tsx.stub +106 -0
- package/stubs/tailwind-only/react/src/components/layout/sidebar-data.ts.stub +58 -0
- package/stubs/tailwind-only/react/src/components/layout/theme-toggle.tsx.stub +36 -0
- package/stubs/tailwind-only/react/src/components/layout/top-nav.tsx.stub +72 -0
- package/stubs/tailwind-only/react/src/lib/utils.ts.stub +6 -0
- package/stubs/tailwind-only/react/src/pages/Dashboard.tsx.stub +205 -0
- package/stubs/tailwind-only/react/src/pages/Login.tsx.stub +122 -0
- package/stubs/tailwind-only/react/src/pages/Register.tsx.stub +137 -0
- package/stubs/tailwind-only/react/src/pages/Users.tsx.stub +426 -0
- package/stubs/tailwind-only/react/src/pages/account/layout.tsx.stub +64 -0
- package/stubs/tailwind-only/react/src/pages/account/preferences.tsx.stub +80 -0
- package/stubs/tailwind-only/react/src/pages/account/profile.tsx.stub +67 -0
- package/stubs/tailwind-only/react/src/pages/account/security.tsx.stub +91 -0
- package/stubs/tailwind-only/react/src/style.css.stub +14 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/app-sidebar.svelte.stub +104 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/authenticated-layout.svelte.stub +51 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/header.svelte.stub +66 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/main.svelte.stub +26 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-group.svelte.stub +131 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-user.svelte.stub +104 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/sidebar-data.ts.stub +57 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/theme-toggle.svelte.stub +28 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/top-nav.svelte.stub +99 -0
- package/stubs/tailwind-only/svelte/src/lib/utils.ts.stub +6 -0
- package/stubs/tailwind-only/svelte/src/pages/Dashboard.svelte.stub +192 -0
- package/stubs/tailwind-only/svelte/src/pages/Login.svelte.stub +120 -0
- package/stubs/tailwind-only/svelte/src/pages/Register.svelte.stub +134 -0
- package/stubs/tailwind-only/svelte/src/pages/Users.svelte.stub +293 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Layout.svelte.stub +61 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Preferences.svelte.stub +81 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Profile.svelte.stub +76 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Security.svelte.stub +140 -0
- package/stubs/tailwind-only/svelte/src/style.css.stub +127 -0
- package/stubs/tailwind-only/vue/src/components/layout/AppSidebar.vue.stub +60 -0
- package/stubs/tailwind-only/vue/src/components/layout/AuthenticatedLayout.vue.stub +73 -0
- package/stubs/tailwind-only/vue/src/components/layout/Header.vue.stub +54 -0
- package/stubs/tailwind-only/vue/src/components/layout/Main.vue.stub +22 -0
- package/stubs/tailwind-only/vue/src/components/layout/NavGroup.vue.stub +107 -0
- package/stubs/tailwind-only/vue/src/components/layout/NavUser.vue.stub +104 -0
- package/stubs/tailwind-only/vue/src/components/layout/ThemeToggle.vue.stub +28 -0
- package/stubs/tailwind-only/vue/src/components/layout/TopNav.vue.stub +86 -0
- package/stubs/tailwind-only/vue/src/components/layout/sidebar-data.ts.stub +57 -0
- package/stubs/tailwind-only/vue/src/lib/utils.ts.stub +7 -0
- package/stubs/tailwind-only/vue/src/pages/Dashboard.vue.stub +195 -0
- package/stubs/tailwind-only/vue/src/pages/Login.vue.stub +121 -0
- package/stubs/tailwind-only/vue/src/pages/Register.vue.stub +137 -0
- package/stubs/tailwind-only/vue/src/pages/Users.vue.stub +401 -0
- package/stubs/tailwind-only/vue/src/pages/account/Layout.vue.stub +56 -0
- package/stubs/tailwind-only/vue/src/pages/account/Preferences.vue.stub +81 -0
- package/stubs/tailwind-only/vue/src/pages/account/Profile.vue.stub +69 -0
- package/stubs/tailwind-only/vue/src/pages/account/Security.vue.stub +85 -0
- package/stubs/tailwind-only/vue/src/style.css.stub +26 -0
- package/stubs/themes/corporate/react/src/components/layout/app-sidebar.tsx.stub +74 -0
- package/stubs/themes/corporate/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/corporate/react/src/pages/Dashboard.tsx.stub +310 -0
- package/stubs/themes/corporate/react/src/pages/Login.tsx.stub +122 -0
- package/stubs/themes/corporate/react/src/pages/Register.tsx.stub +144 -0
- package/stubs/themes/corporate/react/src/style.css.stub +135 -0
- package/stubs/themes/corporate/svelte/src/pages/Dashboard.svelte.stub +271 -0
- package/stubs/themes/corporate/svelte/src/pages/Login.svelte.stub +117 -0
- package/stubs/themes/corporate/svelte/src/pages/Register.svelte.stub +138 -0
- package/stubs/themes/corporate/svelte/src/style.css.stub +134 -0
- package/stubs/themes/corporate/vue/src/pages/Dashboard.vue.stub +325 -0
- package/stubs/themes/corporate/vue/src/pages/Login.vue.stub +118 -0
- package/stubs/themes/corporate/vue/src/pages/Register.vue.stub +139 -0
- package/stubs/themes/corporate/vue/src/style.css.stub +134 -0
- package/stubs/themes/minimal/react/src/components/layout/app-sidebar.tsx.stub +68 -0
- package/stubs/themes/minimal/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/minimal/react/src/pages/Dashboard.tsx.stub +166 -0
- package/stubs/themes/minimal/react/src/pages/Login.tsx.stub +95 -0
- package/stubs/themes/minimal/react/src/pages/Register.tsx.stub +73 -0
- package/stubs/themes/minimal/react/src/style.css.stub +142 -0
- package/stubs/themes/minimal/svelte/src/pages/Dashboard.svelte.stub +149 -0
- package/stubs/themes/minimal/svelte/src/pages/Login.svelte.stub +90 -0
- package/stubs/themes/minimal/svelte/src/pages/Register.svelte.stub +70 -0
- package/stubs/themes/minimal/svelte/src/style.css.stub +142 -0
- package/stubs/themes/minimal/vue/src/pages/Dashboard.vue.stub +163 -0
- package/stubs/themes/minimal/vue/src/pages/Login.vue.stub +91 -0
- package/stubs/themes/minimal/vue/src/pages/Register.vue.stub +73 -0
- package/stubs/themes/minimal/vue/src/style.css.stub +142 -0
- package/stubs/themes/starter/react/src/components/layout/app-sidebar.tsx.stub +74 -0
- package/stubs/themes/starter/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/starter/react/src/pages/Dashboard.tsx.stub +236 -0
- package/stubs/themes/starter/react/src/pages/Login.tsx.stub +131 -0
- package/stubs/themes/starter/react/src/pages/Register.tsx.stub +145 -0
- package/stubs/themes/starter/react/src/style.css.stub +141 -0
- package/stubs/themes/starter/svelte/src/pages/Dashboard.svelte.stub +212 -0
- package/stubs/themes/starter/svelte/src/pages/Login.svelte.stub +126 -0
- package/stubs/themes/starter/svelte/src/pages/Register.svelte.stub +139 -0
- package/stubs/themes/starter/svelte/src/style.css.stub +141 -0
- package/stubs/themes/starter/vue/src/pages/Dashboard.vue.stub +228 -0
- package/stubs/themes/starter/vue/src/pages/Login.vue.stub +127 -0
- package/stubs/themes/starter/vue/src/pages/Register.vue.stub +140 -0
- package/stubs/themes/starter/vue/src/style.css.stub +141 -0
- package/stubs/themes/workspace/react/src/components/layout/app-sidebar.tsx.stub +97 -0
- package/stubs/themes/workspace/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/workspace/react/src/pages/Dashboard.tsx.stub +304 -0
- package/stubs/themes/workspace/react/src/pages/Login.tsx.stub +131 -0
- package/stubs/themes/workspace/react/src/pages/Register.tsx.stub +82 -0
- package/stubs/themes/workspace/react/src/style.css.stub +138 -0
- package/stubs/themes/workspace/svelte/src/pages/Dashboard.svelte.stub +215 -0
- package/stubs/themes/workspace/svelte/src/pages/Login.svelte.stub +124 -0
- package/stubs/themes/workspace/svelte/src/pages/Register.svelte.stub +76 -0
- package/stubs/themes/workspace/svelte/src/style.css.stub +134 -0
- package/stubs/themes/workspace/vue/src/pages/Dashboard.vue.stub +220 -0
- package/stubs/themes/workspace/vue/src/pages/Login.vue.stub +128 -0
- package/stubs/themes/workspace/vue/src/pages/Register.vue.stub +80 -0
- package/stubs/themes/workspace/vue/src/style.css.stub +134 -0
- package/stubs/vue/src/App.vue.stub +2 -1
- package/stubs/vue/src/lib/api.ts.stub +30 -6
- package/stubs/vue/src/main.ts.stub +3 -1
- package/stubs/vue/src/pages/Login.vue.stub +3 -3
- package/stubs/vue/vite.config.ts.stub +20 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
3
|
+
|
|
4
|
+
@theme inline {
|
|
5
|
+
--color-background: var(--background);
|
|
6
|
+
--color-foreground: var(--foreground);
|
|
7
|
+
--color-card: var(--card);
|
|
8
|
+
--color-card-foreground: var(--card-foreground);
|
|
9
|
+
--color-popover: var(--popover);
|
|
10
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
11
|
+
--color-primary: var(--primary);
|
|
12
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
13
|
+
--color-secondary: var(--secondary);
|
|
14
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
15
|
+
--color-muted: var(--muted);
|
|
16
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
17
|
+
--color-accent: var(--accent);
|
|
18
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
19
|
+
--color-destructive: var(--destructive);
|
|
20
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
21
|
+
--color-border: var(--border);
|
|
22
|
+
--color-input: var(--input);
|
|
23
|
+
--color-ring: var(--ring);
|
|
24
|
+
--color-chart-1: var(--chart-1);
|
|
25
|
+
--color-chart-2: var(--chart-2);
|
|
26
|
+
--color-chart-3: var(--chart-3);
|
|
27
|
+
--color-chart-4: var(--chart-4);
|
|
28
|
+
--color-chart-5: var(--chart-5);
|
|
29
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
30
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
31
|
+
--radius-lg: var(--radius);
|
|
32
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
33
|
+
--color-sidebar: var(--sidebar);
|
|
34
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
35
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
36
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
37
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
38
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
39
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
40
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
:root {
|
|
44
|
+
--background: oklch(1 0 0);
|
|
45
|
+
--foreground: oklch(0.145 0 0);
|
|
46
|
+
--card: oklch(1 0 0);
|
|
47
|
+
--card-foreground: oklch(0.145 0 0);
|
|
48
|
+
--popover: oklch(1 0 0);
|
|
49
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
50
|
+
--primary: oklch(0.205 0 0);
|
|
51
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
52
|
+
--secondary: oklch(0.97 0 0);
|
|
53
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
54
|
+
--muted: oklch(0.97 0 0);
|
|
55
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
56
|
+
--accent: oklch(0.97 0 0);
|
|
57
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
58
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
59
|
+
--destructive-foreground: oklch(0.577 0.245 27.325);
|
|
60
|
+
--border: oklch(0.922 0 0);
|
|
61
|
+
--input: oklch(0.922 0 0);
|
|
62
|
+
--ring: oklch(0.708 0 0);
|
|
63
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
64
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
65
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
66
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
67
|
+
--chart-5: oklch(0.769 0.188 70.067);
|
|
68
|
+
--radius: 0.625rem;
|
|
69
|
+
--sidebar: oklch(0.985 0 0);
|
|
70
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
71
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
72
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
73
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
74
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
75
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
76
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.dark {
|
|
80
|
+
--background: oklch(0.145 0 0);
|
|
81
|
+
--foreground: oklch(0.985 0 0);
|
|
82
|
+
--card: oklch(0.145 0 0);
|
|
83
|
+
--card-foreground: oklch(0.985 0 0);
|
|
84
|
+
--popover: oklch(0.145 0 0);
|
|
85
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
86
|
+
--primary: oklch(0.985 0 0);
|
|
87
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
88
|
+
--secondary: oklch(0.269 0 0);
|
|
89
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
90
|
+
--muted: oklch(0.269 0 0);
|
|
91
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
92
|
+
--accent: oklch(0.269 0 0);
|
|
93
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
94
|
+
--destructive: oklch(0.396 0.141 25.723);
|
|
95
|
+
--destructive-foreground: oklch(0.637 0.237 25.331);
|
|
96
|
+
--border: oklch(0.269 0 0);
|
|
97
|
+
--input: oklch(0.269 0 0);
|
|
98
|
+
--ring: oklch(0.439 0 0);
|
|
99
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
100
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
101
|
+
--chart-3: oklch(0.769 0.188 70.067);
|
|
102
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
103
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
104
|
+
--sidebar: oklch(0.205 0 0);
|
|
105
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
106
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
107
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
108
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
109
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
110
|
+
--sidebar-border: oklch(0.269 0 0);
|
|
111
|
+
--sidebar-ring: oklch(0.439 0 0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@layer base {
|
|
115
|
+
* {
|
|
116
|
+
@apply border-border;
|
|
117
|
+
}
|
|
118
|
+
body {
|
|
119
|
+
@apply bg-background text-foreground;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@keyframes fadeUp {
|
|
124
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
125
|
+
to { opacity: 1; transform: translateY(0); }
|
|
126
|
+
}
|
|
127
|
+
.animate-fade-up { animation: fadeUp 0.4s ease-out; }
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Command } from 'lucide-vue-next'
|
|
3
|
+
import NavGroup from './NavGroup.vue'
|
|
4
|
+
import NavUser from './NavUser.vue'
|
|
5
|
+
import type { NavUserUser } from './NavUser.vue'
|
|
6
|
+
import { sidebarData } from './sidebar-data'
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
user: NavUserUser
|
|
10
|
+
appName: string
|
|
11
|
+
activePath: string
|
|
12
|
+
navigate: (href: string) => void
|
|
13
|
+
onLogout: () => void
|
|
14
|
+
collapsed?: boolean
|
|
15
|
+
}>()
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<aside
|
|
20
|
+
class="flex h-full flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground transition-[width] duration-200"
|
|
21
|
+
:class="collapsed ? 'w-14' : 'w-64'"
|
|
22
|
+
>
|
|
23
|
+
<!-- Header -->
|
|
24
|
+
<div class="flex h-14 items-center gap-2 border-b border-sidebar-border px-3">
|
|
25
|
+
<button
|
|
26
|
+
class="flex items-center gap-2 rounded-md p-1.5 hover:bg-sidebar-accent"
|
|
27
|
+
:title="appName"
|
|
28
|
+
@click="navigate('/dashboard')"
|
|
29
|
+
>
|
|
30
|
+
<span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
|
31
|
+
<Command class="h-4 w-4" />
|
|
32
|
+
</span>
|
|
33
|
+
<template v-if="!collapsed">
|
|
34
|
+
<span class="grid flex-1 text-left text-sm leading-tight">
|
|
35
|
+
<span class="truncate font-semibold">{{ appName }}</span>
|
|
36
|
+
<span class="truncate text-xs text-muted-foreground">Admin Panel</span>
|
|
37
|
+
</span>
|
|
38
|
+
</template>
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- Content -->
|
|
43
|
+
<nav class="flex-1 overflow-y-auto py-2">
|
|
44
|
+
<NavGroup
|
|
45
|
+
v-for="group in sidebarData"
|
|
46
|
+
:key="group.title"
|
|
47
|
+
:group="group"
|
|
48
|
+
:active-path="activePath"
|
|
49
|
+
:navigate="navigate"
|
|
50
|
+
:collapsed="collapsed"
|
|
51
|
+
class="mb-2"
|
|
52
|
+
/>
|
|
53
|
+
</nav>
|
|
54
|
+
|
|
55
|
+
<!-- Footer -->
|
|
56
|
+
<div class="border-t border-sidebar-border p-2">
|
|
57
|
+
<NavUser :user="user" :navigate="navigate" :on-logout="onLogout" :collapsed="collapsed" />
|
|
58
|
+
</div>
|
|
59
|
+
</aside>
|
|
60
|
+
</template>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, provide, onMounted, onUnmounted } from 'vue'
|
|
3
|
+
import { post } from '@/lib/api'
|
|
4
|
+
import AppSidebar from './AppSidebar.vue'
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(defineProps<{
|
|
7
|
+
currentUser?: { name: string; email: string; role?: string } | null
|
|
8
|
+
appName?: string
|
|
9
|
+
navigate: (href: string) => void
|
|
10
|
+
activePath: string
|
|
11
|
+
}>(), {
|
|
12
|
+
appName: 'Mantiq',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const mobileOpen = ref(false)
|
|
16
|
+
|
|
17
|
+
function toggleMobileSidebar() {
|
|
18
|
+
mobileOpen.value = !mobileOpen.value
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
provide('toggleMobileSidebar', toggleMobileSidebar)
|
|
22
|
+
|
|
23
|
+
async function handleLogout() {
|
|
24
|
+
await post('/logout', {})
|
|
25
|
+
props.navigate('/login')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const user = props.currentUser ?? { name: 'User', email: 'user@example.com' }
|
|
29
|
+
|
|
30
|
+
function onClickOutside(e: MouseEvent) {
|
|
31
|
+
const target = e.target as HTMLElement
|
|
32
|
+
if (mobileOpen.value && !target.closest('[data-sidebar]')) {
|
|
33
|
+
mobileOpen.value = false
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onMounted(() => document.addEventListener('click', onClickOutside))
|
|
38
|
+
onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<div class="flex min-h-screen">
|
|
43
|
+
<!-- Desktop sidebar -->
|
|
44
|
+
<div class="hidden md:flex" data-sidebar>
|
|
45
|
+
<AppSidebar
|
|
46
|
+
:user="user"
|
|
47
|
+
:app-name="appName"
|
|
48
|
+
:active-path="activePath"
|
|
49
|
+
:navigate="navigate"
|
|
50
|
+
:on-logout="handleLogout"
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<!-- Mobile sidebar overlay -->
|
|
55
|
+
<div v-if="mobileOpen" class="fixed inset-0 z-40 md:hidden">
|
|
56
|
+
<div class="fixed inset-0 bg-black/50" @click="mobileOpen = false" />
|
|
57
|
+
<div class="fixed inset-y-0 left-0 z-50 w-64" data-sidebar @click.stop>
|
|
58
|
+
<AppSidebar
|
|
59
|
+
:user="user"
|
|
60
|
+
:app-name="appName"
|
|
61
|
+
:active-path="activePath"
|
|
62
|
+
:navigate="(href: string) => { mobileOpen = false; navigate(href) }"
|
|
63
|
+
:on-logout="handleLogout"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- Main content -->
|
|
69
|
+
<div class="flex flex-1 flex-col">
|
|
70
|
+
<slot />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, inject, onMounted, onUnmounted } from 'vue'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import { PanelLeft } from 'lucide-vue-next'
|
|
5
|
+
import ThemeToggle from './ThemeToggle.vue'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
fixed?: boolean
|
|
9
|
+
class?: string
|
|
10
|
+
navigate?: (href: string) => void
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const toggleMobileSidebar = inject<() => void>('toggleMobileSidebar', () => {})
|
|
14
|
+
|
|
15
|
+
const offset = ref(0)
|
|
16
|
+
|
|
17
|
+
function onScroll() {
|
|
18
|
+
offset.value = document.body.scrollTop || document.documentElement.scrollTop
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
onMounted(() => {
|
|
22
|
+
document.addEventListener('scroll', onScroll, { passive: true })
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
onUnmounted(() => {
|
|
26
|
+
document.removeEventListener('scroll', onScroll)
|
|
27
|
+
})
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<header
|
|
32
|
+
:class="cn(
|
|
33
|
+
'flex h-16 shrink-0 items-center gap-2',
|
|
34
|
+
fixed && 'sticky top-0 z-10 bg-background',
|
|
35
|
+
offset > 10 && fixed ? 'border-b' : '',
|
|
36
|
+
props.class,
|
|
37
|
+
)"
|
|
38
|
+
>
|
|
39
|
+
<div class="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
|
40
|
+
<button
|
|
41
|
+
class="-ml-1 inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground md:hidden"
|
|
42
|
+
@click="toggleMobileSidebar"
|
|
43
|
+
>
|
|
44
|
+
<PanelLeft class="h-4 w-4" />
|
|
45
|
+
<span class="sr-only">Toggle sidebar</span>
|
|
46
|
+
</button>
|
|
47
|
+
<div class="mx-1 hidden h-4 w-px bg-border md:block" />
|
|
48
|
+
<slot />
|
|
49
|
+
<div class="ms-auto flex items-center gap-2">
|
|
50
|
+
<ThemeToggle />
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</header>
|
|
54
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { cn } from '@/lib/utils'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
fixed?: boolean
|
|
6
|
+
class?: string
|
|
7
|
+
}>()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<main
|
|
12
|
+
:class="cn(
|
|
13
|
+
'peer-[.header-fixed]/header:mt-16',
|
|
14
|
+
fixed && 'flex flex-grow flex-col overflow-hidden',
|
|
15
|
+
props.class,
|
|
16
|
+
)"
|
|
17
|
+
>
|
|
18
|
+
<div class="px-4 py-6 lg:px-6">
|
|
19
|
+
<slot />
|
|
20
|
+
</div>
|
|
21
|
+
</main>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { ChevronRight, ExternalLink } from 'lucide-vue-next'
|
|
4
|
+
import type { NavGroup as NavGroupData } from './sidebar-data'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
group: NavGroupData
|
|
8
|
+
activePath: string
|
|
9
|
+
navigate: (href: string) => void
|
|
10
|
+
collapsed?: boolean
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const expandedItems = ref<Record<string, boolean>>({})
|
|
14
|
+
|
|
15
|
+
function isActive(itemUrl: string, activePath: string): boolean {
|
|
16
|
+
if (itemUrl === activePath) return true
|
|
17
|
+
const itemBase = itemUrl.split('?')[0]
|
|
18
|
+
const activeBase = activePath.split('?')[0]
|
|
19
|
+
return itemBase === activeBase
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isGroupActive(items: NavGroupData['items'][number]['items'], activePath: string): boolean {
|
|
23
|
+
if (!items) return false
|
|
24
|
+
return items.some((sub) => isActive(sub.url, activePath))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toggleExpanded(title: string) {
|
|
28
|
+
expandedItems.value[title] = !expandedItems.value[title]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isExpanded(item: NavGroupData['items'][number]): boolean {
|
|
32
|
+
if (expandedItems.value[item.title] !== undefined) return expandedItems.value[item.title]
|
|
33
|
+
return isGroupActive(item.items, props.activePath)
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<div class="space-y-1 px-2">
|
|
39
|
+
<p v-if="!collapsed" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
|
40
|
+
{{ group.title }}
|
|
41
|
+
</p>
|
|
42
|
+
<template v-for="item in group.items" :key="item.title">
|
|
43
|
+
<!-- Items with sub-items: collapsible -->
|
|
44
|
+
<template v-if="item.items && item.items.length > 0">
|
|
45
|
+
<div>
|
|
46
|
+
<button
|
|
47
|
+
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
48
|
+
:class="isGroupActive(item.items, activePath) ? 'bg-sidebar-accent text-sidebar-accent-foreground' : 'text-sidebar-foreground'"
|
|
49
|
+
:title="item.title"
|
|
50
|
+
@click="toggleExpanded(item.title)"
|
|
51
|
+
>
|
|
52
|
+
<component :is="item.icon" class="h-4 w-4 shrink-0" />
|
|
53
|
+
<span v-if="!collapsed" class="flex-1 truncate text-left">{{ item.title }}</span>
|
|
54
|
+
<ChevronRight
|
|
55
|
+
v-if="!collapsed"
|
|
56
|
+
class="h-4 w-4 shrink-0 transition-transform duration-200"
|
|
57
|
+
:class="isExpanded(item) && 'rotate-90'"
|
|
58
|
+
/>
|
|
59
|
+
</button>
|
|
60
|
+
<div v-if="!collapsed && isExpanded(item)" class="ml-4 mt-0.5 space-y-0.5 border-l border-sidebar-border pl-2">
|
|
61
|
+
<button
|
|
62
|
+
v-for="sub in item.items"
|
|
63
|
+
:key="sub.title"
|
|
64
|
+
class="flex w-full items-center rounded-md px-2 py-1 text-sm transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
65
|
+
:class="isActive(sub.url, activePath) ? 'bg-sidebar-accent text-sidebar-accent-foreground font-medium' : 'text-muted-foreground'"
|
|
66
|
+
@click.prevent="navigate(sub.url)"
|
|
67
|
+
>
|
|
68
|
+
{{ sub.title }}
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</template>
|
|
73
|
+
|
|
74
|
+
<!-- External link -->
|
|
75
|
+
<a
|
|
76
|
+
v-else-if="item.external"
|
|
77
|
+
:href="item.url"
|
|
78
|
+
target="_blank"
|
|
79
|
+
rel="noopener noreferrer"
|
|
80
|
+
class="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-foreground transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
81
|
+
:title="item.title"
|
|
82
|
+
>
|
|
83
|
+
<component :is="item.icon" class="h-4 w-4 shrink-0" />
|
|
84
|
+
<span v-if="!collapsed" class="flex-1 truncate">{{ item.title }}</span>
|
|
85
|
+
<ExternalLink v-if="!collapsed" class="ml-auto h-3 w-3 text-muted-foreground" />
|
|
86
|
+
</a>
|
|
87
|
+
|
|
88
|
+
<!-- Regular item -->
|
|
89
|
+
<button
|
|
90
|
+
v-else
|
|
91
|
+
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
92
|
+
:class="isActive(item.url, activePath) ? 'bg-sidebar-accent text-sidebar-accent-foreground' : 'text-sidebar-foreground'"
|
|
93
|
+
:title="item.title"
|
|
94
|
+
@click.prevent="navigate(item.url)"
|
|
95
|
+
>
|
|
96
|
+
<component :is="item.icon" class="h-4 w-4 shrink-0" />
|
|
97
|
+
<span v-if="!collapsed" class="flex-1 truncate text-left">{{ item.title }}</span>
|
|
98
|
+
<span
|
|
99
|
+
v-if="item.badge && !collapsed"
|
|
100
|
+
class="ml-auto rounded-full bg-secondary px-1.5 py-0 text-[10px] font-medium text-secondary-foreground"
|
|
101
|
+
>
|
|
102
|
+
{{ item.badge }}
|
|
103
|
+
</span>
|
|
104
|
+
</button>
|
|
105
|
+
</template>
|
|
106
|
+
</div>
|
|
107
|
+
</template>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, onUnmounted } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
ChevronsUpDown,
|
|
5
|
+
LogOut,
|
|
6
|
+
User,
|
|
7
|
+
Settings,
|
|
8
|
+
} from 'lucide-vue-next'
|
|
9
|
+
|
|
10
|
+
export interface NavUserUser {
|
|
11
|
+
name: string
|
|
12
|
+
email: string
|
|
13
|
+
role?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const props = defineProps<{
|
|
17
|
+
user: NavUserUser
|
|
18
|
+
navigate: (href: string) => void
|
|
19
|
+
onLogout: () => void
|
|
20
|
+
collapsed?: boolean
|
|
21
|
+
}>()
|
|
22
|
+
|
|
23
|
+
const menuOpen = ref(false)
|
|
24
|
+
|
|
25
|
+
function getInitials(name: string) {
|
|
26
|
+
return name
|
|
27
|
+
.split(' ')
|
|
28
|
+
.map((n) => n[0])
|
|
29
|
+
.join('')
|
|
30
|
+
.toUpperCase()
|
|
31
|
+
.slice(0, 2)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function onClickOutside(e: MouseEvent) {
|
|
35
|
+
const target = e.target as HTMLElement
|
|
36
|
+
if (!target.closest('[data-nav-user]')) {
|
|
37
|
+
menuOpen.value = false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onMounted(() => document.addEventListener('click', onClickOutside))
|
|
42
|
+
onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<div class="relative" data-nav-user>
|
|
47
|
+
<button
|
|
48
|
+
class="flex w-full items-center gap-2 rounded-md p-2 text-left text-sm hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
49
|
+
@click.stop="menuOpen = !menuOpen"
|
|
50
|
+
>
|
|
51
|
+
<span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted text-xs font-semibold">
|
|
52
|
+
{{ getInitials(user.name) }}
|
|
53
|
+
</span>
|
|
54
|
+
<template v-if="!collapsed">
|
|
55
|
+
<span class="grid flex-1 text-left text-sm leading-tight">
|
|
56
|
+
<span class="truncate font-semibold">{{ user.name }}</span>
|
|
57
|
+
<span class="truncate text-xs text-muted-foreground">{{ user.email }}</span>
|
|
58
|
+
</span>
|
|
59
|
+
<ChevronsUpDown class="ml-auto h-4 w-4 text-muted-foreground" />
|
|
60
|
+
</template>
|
|
61
|
+
</button>
|
|
62
|
+
|
|
63
|
+
<div
|
|
64
|
+
v-if="menuOpen"
|
|
65
|
+
class="absolute bottom-full left-0 z-50 mb-1 min-w-[14rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
|
|
66
|
+
>
|
|
67
|
+
<!-- User info -->
|
|
68
|
+
<div class="px-2 py-1.5">
|
|
69
|
+
<div class="flex items-center gap-2">
|
|
70
|
+
<span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted text-xs font-semibold">
|
|
71
|
+
{{ getInitials(user.name) }}
|
|
72
|
+
</span>
|
|
73
|
+
<span class="grid flex-1 text-left text-sm leading-tight">
|
|
74
|
+
<span class="truncate font-semibold">{{ user.name }}</span>
|
|
75
|
+
<span class="truncate text-xs text-muted-foreground">{{ user.email }}</span>
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="my-1 h-px bg-border" />
|
|
80
|
+
<button
|
|
81
|
+
class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground"
|
|
82
|
+
@click="menuOpen = false; navigate('/account/profile')"
|
|
83
|
+
>
|
|
84
|
+
<User class="h-4 w-4" />
|
|
85
|
+
Account
|
|
86
|
+
</button>
|
|
87
|
+
<button
|
|
88
|
+
class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground"
|
|
89
|
+
@click="menuOpen = false; navigate('/account/preferences')"
|
|
90
|
+
>
|
|
91
|
+
<Settings class="h-4 w-4" />
|
|
92
|
+
Settings
|
|
93
|
+
</button>
|
|
94
|
+
<div class="my-1 h-px bg-border" />
|
|
95
|
+
<button
|
|
96
|
+
class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-destructive outline-none hover:bg-destructive/10"
|
|
97
|
+
@click="menuOpen = false; onLogout()"
|
|
98
|
+
>
|
|
99
|
+
<LogOut class="h-4 w-4" />
|
|
100
|
+
Sign out
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</template>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { Sun, Moon } from 'lucide-vue-next'
|
|
4
|
+
|
|
5
|
+
const isDark = ref(
|
|
6
|
+
typeof document !== 'undefined'
|
|
7
|
+
? document.documentElement.classList.contains('dark')
|
|
8
|
+
: false,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
function toggleTheme() {
|
|
12
|
+
const dark = document.documentElement.classList.toggle('dark')
|
|
13
|
+
localStorage.setItem('theme', dark ? 'dark' : 'light')
|
|
14
|
+
isDark.value = dark
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<button
|
|
20
|
+
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
21
|
+
title="Toggle theme"
|
|
22
|
+
@click="toggleTheme"
|
|
23
|
+
>
|
|
24
|
+
<Sun v-if="isDark" class="h-4 w-4" />
|
|
25
|
+
<Moon v-else class="h-4 w-4" />
|
|
26
|
+
<span class="sr-only">Toggle theme</span>
|
|
27
|
+
</button>
|
|
28
|
+
</template>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, onUnmounted } from 'vue'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import { Menu } from 'lucide-vue-next'
|
|
5
|
+
|
|
6
|
+
export interface TopNavLink {
|
|
7
|
+
title: string
|
|
8
|
+
href: string
|
|
9
|
+
isActive?: boolean
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
links: TopNavLink[]
|
|
15
|
+
class?: string
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
linkClick: [href: string]
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const mobileOpen = ref(false)
|
|
23
|
+
|
|
24
|
+
function handleClick(e: MouseEvent, href: string) {
|
|
25
|
+
e.preventDefault()
|
|
26
|
+
emit('linkClick', href)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function onClickOutside(e: MouseEvent) {
|
|
30
|
+
const target = e.target as HTMLElement
|
|
31
|
+
if (!target.closest('[data-top-nav-mobile]')) {
|
|
32
|
+
mobileOpen.value = false
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onMounted(() => document.addEventListener('click', onClickOutside))
|
|
37
|
+
onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<!-- Desktop navigation -->
|
|
42
|
+
<nav
|
|
43
|
+
:class="cn('hidden items-center gap-4 md:flex lg:gap-6', props.class)"
|
|
44
|
+
>
|
|
45
|
+
<a
|
|
46
|
+
v-for="link in links"
|
|
47
|
+
:key="link.href"
|
|
48
|
+
:href="link.href"
|
|
49
|
+
:class="cn(
|
|
50
|
+
'text-sm font-medium transition-colors hover:text-primary',
|
|
51
|
+
link.isActive ? 'text-foreground' : 'text-muted-foreground',
|
|
52
|
+
link.disabled && 'pointer-events-none opacity-50',
|
|
53
|
+
)"
|
|
54
|
+
@click="handleClick($event, link.href)"
|
|
55
|
+
>
|
|
56
|
+
{{ link.title }}
|
|
57
|
+
</a>
|
|
58
|
+
</nav>
|
|
59
|
+
|
|
60
|
+
<!-- Mobile navigation -->
|
|
61
|
+
<div class="relative md:hidden" data-top-nav-mobile>
|
|
62
|
+
<button
|
|
63
|
+
class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-input bg-background text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
64
|
+
@click.stop="mobileOpen = !mobileOpen"
|
|
65
|
+
>
|
|
66
|
+
<Menu class="h-4 w-4" />
|
|
67
|
+
<span class="sr-only">Toggle navigation</span>
|
|
68
|
+
</button>
|
|
69
|
+
<div
|
|
70
|
+
v-if="mobileOpen"
|
|
71
|
+
class="absolute left-0 top-full z-50 mt-1 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
|
|
72
|
+
>
|
|
73
|
+
<button
|
|
74
|
+
v-for="link in links"
|
|
75
|
+
:key="link.href"
|
|
76
|
+
:disabled="link.disabled"
|
|
77
|
+
class="flex w-full items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
78
|
+
@click="mobileOpen = false; emit('linkClick', link.href)"
|
|
79
|
+
>
|
|
80
|
+
<span :class="cn(!link.isActive && 'text-muted-foreground')">
|
|
81
|
+
{{ link.title }}
|
|
82
|
+
</span>
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</template>
|