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,91 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { AccountLayout } from './layout.tsx'
|
|
3
|
+
|
|
4
|
+
interface SecurityProps {
|
|
5
|
+
appName?: string
|
|
6
|
+
currentUser?: { id: number; name: string; email: string } | null
|
|
7
|
+
navigate: (href: string) => void
|
|
8
|
+
[key: string]: any
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function Security({ appName, currentUser, navigate }: SecurityProps) {
|
|
12
|
+
const [currentPassword, setCurrentPassword] = useState('')
|
|
13
|
+
const [newPassword, setNewPassword] = useState('')
|
|
14
|
+
const [confirmPassword, setConfirmPassword] = useState('')
|
|
15
|
+
const [saving, setSaving] = useState(false)
|
|
16
|
+
const [saved, setSaved] = useState(false)
|
|
17
|
+
const [error, setError] = useState('')
|
|
18
|
+
|
|
19
|
+
const handleSave = async (e: React.FormEvent) => {
|
|
20
|
+
e.preventDefault()
|
|
21
|
+
setError('')
|
|
22
|
+
if (newPassword.length < 8) { setError('Password must be at least 8 characters.'); return }
|
|
23
|
+
if (newPassword !== confirmPassword) { setError('Passwords do not match.'); return }
|
|
24
|
+
setSaving(true)
|
|
25
|
+
await new Promise(r => setTimeout(r, 500))
|
|
26
|
+
setSaving(false)
|
|
27
|
+
setSaved(true)
|
|
28
|
+
setCurrentPassword('')
|
|
29
|
+
setNewPassword('')
|
|
30
|
+
setConfirmPassword('')
|
|
31
|
+
setTimeout(() => setSaved(false), 3000)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<AccountLayout appName={appName} currentUser={currentUser} navigate={navigate} activePath="/account/security">
|
|
36
|
+
<div className="space-y-8">
|
|
37
|
+
<div>
|
|
38
|
+
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-50">Security</h3>
|
|
39
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
40
|
+
Manage your password and security settings.
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{/* Change Password */}
|
|
45
|
+
<form onSubmit={handleSave} className="space-y-4">
|
|
46
|
+
<div className="space-y-2">
|
|
47
|
+
<label htmlFor="current" className="text-sm font-medium text-gray-900 dark:text-gray-50">Current password</label>
|
|
48
|
+
<input id="current" type="password" value={currentPassword} onChange={e => setCurrentPassword(e.target.value)} autoComplete="current-password" className="w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm outline-none ring-emerald-500 transition focus:border-emerald-500 focus:ring-2 dark:border-gray-700 dark:bg-gray-900" />
|
|
49
|
+
</div>
|
|
50
|
+
<div className="space-y-2">
|
|
51
|
+
<label htmlFor="new" className="text-sm font-medium text-gray-900 dark:text-gray-50">New password</label>
|
|
52
|
+
<input id="new" type="password" value={newPassword} onChange={e => setNewPassword(e.target.value)} autoComplete="new-password" className="w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm outline-none ring-emerald-500 transition focus:border-emerald-500 focus:ring-2 dark:border-gray-700 dark:bg-gray-900" />
|
|
53
|
+
</div>
|
|
54
|
+
<div className="space-y-2">
|
|
55
|
+
<label htmlFor="confirm" className="text-sm font-medium text-gray-900 dark:text-gray-50">Confirm password</label>
|
|
56
|
+
<input id="confirm" type="password" value={confirmPassword} onChange={e => setConfirmPassword(e.target.value)} autoComplete="new-password" className="w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm outline-none ring-emerald-500 transition focus:border-emerald-500 focus:ring-2 dark:border-gray-700 dark:bg-gray-900" />
|
|
57
|
+
</div>
|
|
58
|
+
{error && <p className="text-sm text-red-600 dark:text-red-400">{error}</p>}
|
|
59
|
+
<div className="flex items-center gap-3">
|
|
60
|
+
<button type="submit" disabled={saving} className="rounded-md bg-emerald-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-emerald-700 disabled:opacity-50">
|
|
61
|
+
{saving ? 'Saving...' : 'Update password'}
|
|
62
|
+
</button>
|
|
63
|
+
{saved && <span className="text-sm text-gray-500 dark:text-gray-400">Password updated.</span>}
|
|
64
|
+
</div>
|
|
65
|
+
</form>
|
|
66
|
+
|
|
67
|
+
{/* 2FA */}
|
|
68
|
+
<div className="space-y-3">
|
|
69
|
+
<div>
|
|
70
|
+
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-50">Two-factor authentication</h4>
|
|
71
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">Add an additional layer of security to your account.</p>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-950">
|
|
74
|
+
<h5 className="text-sm font-medium text-gray-900 dark:text-gray-50">Authenticator app</h5>
|
|
75
|
+
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">Use an authenticator app to generate one-time codes.</p>
|
|
76
|
+
<button type="button" disabled className="mt-3 rounded-md border border-gray-200 px-3 py-1.5 text-sm font-medium text-gray-500 opacity-50 dark:border-gray-700 dark:text-gray-400">Enable 2FA</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Delete Account */}
|
|
81
|
+
<div className="space-y-3">
|
|
82
|
+
<div>
|
|
83
|
+
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-50">Delete account</h4>
|
|
84
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">Permanently remove your account and all associated data. This action cannot be undone.</p>
|
|
85
|
+
</div>
|
|
86
|
+
<button type="button" disabled className="rounded-md bg-red-600 px-3 py-1.5 text-sm font-medium text-white opacity-50">Delete account</button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</AccountLayout>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
3
|
+
|
|
4
|
+
@layer base {
|
|
5
|
+
body {
|
|
6
|
+
@apply bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-50;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@keyframes fadeUp {
|
|
11
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
12
|
+
to { opacity: 1; transform: translateY(0); }
|
|
13
|
+
}
|
|
14
|
+
.animate-fade-up { animation: fadeUp 0.4s ease-out; }
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import NavGroup from './nav-group.svelte'
|
|
3
|
+
import NavUser from './nav-user.svelte'
|
|
4
|
+
import { sidebarData } from './sidebar-data'
|
|
5
|
+
import { PanelLeft } from 'lucide-svelte'
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
user,
|
|
9
|
+
appName,
|
|
10
|
+
activePath,
|
|
11
|
+
navigate,
|
|
12
|
+
onLogout,
|
|
13
|
+
open = $bindable(true),
|
|
14
|
+
mobileOpen = $bindable(false),
|
|
15
|
+
}: {
|
|
16
|
+
user: { name: string; email: string; role?: string }
|
|
17
|
+
appName: string
|
|
18
|
+
activePath: string
|
|
19
|
+
navigate: (href: string) => void
|
|
20
|
+
onLogout: () => void
|
|
21
|
+
open: boolean
|
|
22
|
+
mobileOpen: boolean
|
|
23
|
+
} = $props()
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<!-- Desktop sidebar -->
|
|
27
|
+
<aside
|
|
28
|
+
class="fixed inset-y-0 left-0 z-20 hidden lg:flex flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground transition-[width] duration-200 {open ? 'w-64' : 'w-16'}"
|
|
29
|
+
>
|
|
30
|
+
<!-- Header -->
|
|
31
|
+
<div class="flex h-14 items-center gap-3 border-b border-sidebar-border px-4">
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
onclick={() => navigate('/dashboard')}
|
|
35
|
+
class="flex items-center gap-3 overflow-hidden"
|
|
36
|
+
>
|
|
37
|
+
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground text-xs font-bold">
|
|
38
|
+
M
|
|
39
|
+
</div>
|
|
40
|
+
{#if open}
|
|
41
|
+
<div class="grid text-left text-sm leading-tight">
|
|
42
|
+
<span class="truncate font-semibold">{appName}</span>
|
|
43
|
+
<span class="truncate text-xs text-muted-foreground">Admin Panel</span>
|
|
44
|
+
</div>
|
|
45
|
+
{/if}
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Navigation -->
|
|
50
|
+
<nav class="flex-1 overflow-y-auto px-3 py-4 space-y-6">
|
|
51
|
+
{#each sidebarData as group}
|
|
52
|
+
<NavGroup {group} {activePath} {navigate} collapsed={!open} />
|
|
53
|
+
{/each}
|
|
54
|
+
</nav>
|
|
55
|
+
|
|
56
|
+
<!-- Footer -->
|
|
57
|
+
<div class="border-t border-sidebar-border p-3">
|
|
58
|
+
<NavUser {user} {navigate} {onLogout} collapsed={!open} />
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- Rail toggle -->
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
onclick={() => open = !open}
|
|
65
|
+
class="absolute -right-3 top-20 flex h-6 w-6 items-center justify-center rounded-full border border-sidebar-border bg-sidebar text-sidebar-foreground hover:bg-sidebar-accent"
|
|
66
|
+
aria-label={open ? 'Collapse sidebar' : 'Expand sidebar'}
|
|
67
|
+
>
|
|
68
|
+
<PanelLeft class="h-3.5 w-3.5" />
|
|
69
|
+
</button>
|
|
70
|
+
</aside>
|
|
71
|
+
|
|
72
|
+
<!-- Mobile sidebar -->
|
|
73
|
+
<aside
|
|
74
|
+
class="fixed inset-y-0 left-0 z-40 flex w-64 flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground transition-transform duration-200 lg:hidden {mobileOpen ? 'translate-x-0' : '-translate-x-full'}"
|
|
75
|
+
>
|
|
76
|
+
<!-- Header -->
|
|
77
|
+
<div class="flex h-14 items-center gap-3 border-b border-sidebar-border px-4">
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onclick={() => navigate('/dashboard')}
|
|
81
|
+
class="flex items-center gap-3"
|
|
82
|
+
>
|
|
83
|
+
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground text-xs font-bold">
|
|
84
|
+
M
|
|
85
|
+
</div>
|
|
86
|
+
<div class="grid text-left text-sm leading-tight">
|
|
87
|
+
<span class="truncate font-semibold">{appName}</span>
|
|
88
|
+
<span class="truncate text-xs text-muted-foreground">Admin Panel</span>
|
|
89
|
+
</div>
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<!-- Navigation -->
|
|
94
|
+
<nav class="flex-1 overflow-y-auto px-3 py-4 space-y-6">
|
|
95
|
+
{#each sidebarData as group}
|
|
96
|
+
<NavGroup {group} {activePath} {navigate} collapsed={false} />
|
|
97
|
+
{/each}
|
|
98
|
+
</nav>
|
|
99
|
+
|
|
100
|
+
<!-- Footer -->
|
|
101
|
+
<div class="border-t border-sidebar-border p-3">
|
|
102
|
+
<NavUser {user} {navigate} {onLogout} collapsed={false} />
|
|
103
|
+
</div>
|
|
104
|
+
</aside>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { post } from '$lib/api'
|
|
3
|
+
import AppSidebar from './app-sidebar.svelte'
|
|
4
|
+
import type { Snippet } from 'svelte'
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
children,
|
|
8
|
+
currentUser = null,
|
|
9
|
+
appName = 'Mantiq',
|
|
10
|
+
navigate,
|
|
11
|
+
activePath,
|
|
12
|
+
}: {
|
|
13
|
+
children: Snippet
|
|
14
|
+
currentUser?: { name: string; email: string; role?: string } | null
|
|
15
|
+
appName?: string
|
|
16
|
+
navigate: (href: string) => void
|
|
17
|
+
activePath: string
|
|
18
|
+
} = $props()
|
|
19
|
+
|
|
20
|
+
const user = $derived(currentUser ?? { name: 'User', email: 'user@example.com' })
|
|
21
|
+
|
|
22
|
+
async function handleLogout() {
|
|
23
|
+
await post('/logout', {})
|
|
24
|
+
navigate('/login')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let sidebarOpen = $state(true)
|
|
28
|
+
let mobileOpen = $state(false)
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<div class="flex min-h-screen">
|
|
32
|
+
<AppSidebar
|
|
33
|
+
{user}
|
|
34
|
+
{appName}
|
|
35
|
+
{activePath}
|
|
36
|
+
{navigate}
|
|
37
|
+
onLogout={handleLogout}
|
|
38
|
+
bind:open={sidebarOpen}
|
|
39
|
+
bind:mobileOpen={mobileOpen}
|
|
40
|
+
/>
|
|
41
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
42
|
+
{#if mobileOpen}
|
|
43
|
+
<div
|
|
44
|
+
class="fixed inset-0 z-30 bg-black/50 lg:hidden"
|
|
45
|
+
onclick={() => mobileOpen = false}
|
|
46
|
+
></div>
|
|
47
|
+
{/if}
|
|
48
|
+
<div class="flex flex-1 flex-col {sidebarOpen ? 'lg:ml-64' : 'lg:ml-16'} transition-[margin] duration-200">
|
|
49
|
+
{@render children()}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy } from 'svelte'
|
|
3
|
+
import { cn } from '$lib/utils'
|
|
4
|
+
import ThemeToggle from './theme-toggle.svelte'
|
|
5
|
+
import { Menu, Search } from 'lucide-svelte'
|
|
6
|
+
import type { Snippet } from 'svelte'
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
fixed = false,
|
|
10
|
+
navigate,
|
|
11
|
+
children,
|
|
12
|
+
class: className,
|
|
13
|
+
}: {
|
|
14
|
+
fixed?: boolean
|
|
15
|
+
navigate?: (href: string) => void
|
|
16
|
+
children?: Snippet
|
|
17
|
+
class?: string
|
|
18
|
+
} = $props()
|
|
19
|
+
|
|
20
|
+
let offset = $state(0)
|
|
21
|
+
|
|
22
|
+
function onScroll() {
|
|
23
|
+
offset = document.body.scrollTop || document.documentElement.scrollTop
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
onMount(() => {
|
|
27
|
+
document.addEventListener('scroll', onScroll, { passive: true })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
onDestroy(() => {
|
|
31
|
+
if (typeof window !== 'undefined') {
|
|
32
|
+
document.removeEventListener('scroll', onScroll)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<header
|
|
38
|
+
class={cn(
|
|
39
|
+
'flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear',
|
|
40
|
+
fixed && 'sticky top-0 z-10 bg-background',
|
|
41
|
+
offset > 10 && fixed ? 'border-b' : '',
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
<div class="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
|
46
|
+
<!-- Mobile menu trigger (dispatches custom event) -->
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium hover:bg-accent lg:hidden"
|
|
50
|
+
onclick={() => {
|
|
51
|
+
const evt = new CustomEvent('toggle-mobile-sidebar', { bubbles: true })
|
|
52
|
+
document.dispatchEvent(evt)
|
|
53
|
+
}}
|
|
54
|
+
aria-label="Toggle sidebar"
|
|
55
|
+
>
|
|
56
|
+
<Menu class="h-4 w-4" />
|
|
57
|
+
</button>
|
|
58
|
+
<div class="mx-1 hidden h-4 w-px bg-border md:block"></div>
|
|
59
|
+
{#if children}
|
|
60
|
+
{@render children()}
|
|
61
|
+
{/if}
|
|
62
|
+
<div class="ms-auto flex items-center gap-2">
|
|
63
|
+
<ThemeToggle />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</header>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '$lib/utils'
|
|
3
|
+
import type { Snippet } from 'svelte'
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
fixed = false,
|
|
7
|
+
class: className,
|
|
8
|
+
children,
|
|
9
|
+
}: {
|
|
10
|
+
fixed?: boolean
|
|
11
|
+
class?: string
|
|
12
|
+
children: Snippet
|
|
13
|
+
} = $props()
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<main
|
|
17
|
+
class={cn(
|
|
18
|
+
'peer-[.header-fixed]/header:mt-16',
|
|
19
|
+
fixed && 'flex flex-grow flex-col overflow-hidden',
|
|
20
|
+
className,
|
|
21
|
+
)}
|
|
22
|
+
>
|
|
23
|
+
<div class="px-4 py-6 lg:px-6">
|
|
24
|
+
{@render children()}
|
|
25
|
+
</div>
|
|
26
|
+
</main>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ChevronRight, ExternalLink } from 'lucide-svelte'
|
|
3
|
+
import type { NavGroup as NavGroupData } from './sidebar-data'
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
group,
|
|
7
|
+
activePath,
|
|
8
|
+
navigate,
|
|
9
|
+
collapsed = false,
|
|
10
|
+
}: {
|
|
11
|
+
group: NavGroupData
|
|
12
|
+
activePath: string
|
|
13
|
+
navigate: (href: string) => void
|
|
14
|
+
collapsed?: boolean
|
|
15
|
+
} = $props()
|
|
16
|
+
|
|
17
|
+
function isActive(itemUrl: string, active: string): boolean {
|
|
18
|
+
if (itemUrl === active) return true
|
|
19
|
+
const itemBase = itemUrl.split('?')[0]
|
|
20
|
+
const activeBase = active.split('?')[0]
|
|
21
|
+
return itemBase === activeBase
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isGroupActive(items: NavGroupData['items'][number]['items'], active: string): boolean {
|
|
25
|
+
if (!items) return false
|
|
26
|
+
return items.some((sub) => isActive(sub.url, active))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Track which collapsible groups are open
|
|
30
|
+
let openGroups = $state<Set<string>>(new Set())
|
|
31
|
+
|
|
32
|
+
// Auto-open groups that have active children
|
|
33
|
+
$effect(() => {
|
|
34
|
+
for (const item of group.items) {
|
|
35
|
+
if (item.items && isGroupActive(item.items, activePath)) {
|
|
36
|
+
openGroups.add(item.title)
|
|
37
|
+
openGroups = new Set(openGroups)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
function toggleGroup(title: string) {
|
|
43
|
+
if (openGroups.has(title)) {
|
|
44
|
+
openGroups.delete(title)
|
|
45
|
+
} else {
|
|
46
|
+
openGroups.add(title)
|
|
47
|
+
}
|
|
48
|
+
openGroups = new Set(openGroups)
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<div>
|
|
53
|
+
{#if !collapsed}
|
|
54
|
+
<p class="mb-2 px-3 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
55
|
+
{group.title}
|
|
56
|
+
</p>
|
|
57
|
+
{/if}
|
|
58
|
+
<ul class="space-y-1">
|
|
59
|
+
{#each group.items as item}
|
|
60
|
+
{#if item.items && item.items.length > 0}
|
|
61
|
+
<!-- Group with sub-items -->
|
|
62
|
+
<li>
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
onclick={() => toggleGroup(item.title)}
|
|
66
|
+
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors hover:bg-sidebar-accent {isGroupActive(item.items, activePath) ? 'bg-sidebar-accent text-sidebar-accent-foreground' : 'text-sidebar-foreground'}"
|
|
67
|
+
title={collapsed ? item.title : undefined}
|
|
68
|
+
>
|
|
69
|
+
<item.icon class="h-4 w-4 shrink-0" />
|
|
70
|
+
{#if !collapsed}
|
|
71
|
+
<span class="flex-1 text-left">{item.title}</span>
|
|
72
|
+
<ChevronRight class="h-4 w-4 transition-transform duration-200 {openGroups.has(item.title) ? 'rotate-90' : ''}" />
|
|
73
|
+
{/if}
|
|
74
|
+
</button>
|
|
75
|
+
{#if !collapsed && openGroups.has(item.title)}
|
|
76
|
+
<ul class="ml-6 mt-1 space-y-1 border-l border-sidebar-border pl-3">
|
|
77
|
+
{#each item.items as sub}
|
|
78
|
+
<li>
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
onclick={() => navigate(sub.url)}
|
|
82
|
+
class="block w-full rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-sidebar-accent {isActive(sub.url, activePath) ? 'bg-sidebar-accent text-sidebar-accent-foreground font-medium' : 'text-muted-foreground'}"
|
|
83
|
+
>
|
|
84
|
+
{sub.title}
|
|
85
|
+
</button>
|
|
86
|
+
</li>
|
|
87
|
+
{/each}
|
|
88
|
+
</ul>
|
|
89
|
+
{/if}
|
|
90
|
+
</li>
|
|
91
|
+
{:else if item.external}
|
|
92
|
+
<!-- External link -->
|
|
93
|
+
<li>
|
|
94
|
+
<a
|
|
95
|
+
href={item.url}
|
|
96
|
+
target="_blank"
|
|
97
|
+
rel="noopener noreferrer"
|
|
98
|
+
class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm text-sidebar-foreground transition-colors hover:bg-sidebar-accent"
|
|
99
|
+
title={collapsed ? item.title : undefined}
|
|
100
|
+
>
|
|
101
|
+
<item.icon class="h-4 w-4 shrink-0" />
|
|
102
|
+
{#if !collapsed}
|
|
103
|
+
<span class="flex-1">{item.title}</span>
|
|
104
|
+
<ExternalLink class="h-3 w-3 text-muted-foreground" />
|
|
105
|
+
{/if}
|
|
106
|
+
</a>
|
|
107
|
+
</li>
|
|
108
|
+
{:else}
|
|
109
|
+
<!-- Regular nav item -->
|
|
110
|
+
<li>
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
onclick={() => navigate(item.url)}
|
|
114
|
+
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors hover:bg-sidebar-accent {isActive(item.url, activePath) ? 'bg-sidebar-accent text-sidebar-accent-foreground font-medium' : 'text-sidebar-foreground'}"
|
|
115
|
+
title={collapsed ? item.title : undefined}
|
|
116
|
+
>
|
|
117
|
+
<item.icon class="h-4 w-4 shrink-0" />
|
|
118
|
+
{#if !collapsed}
|
|
119
|
+
<span class="flex-1 text-left">{item.title}</span>
|
|
120
|
+
{#if item.badge}
|
|
121
|
+
<span class="ml-auto inline-flex items-center rounded bg-secondary px-1.5 py-0 text-[10px] font-medium text-secondary-foreground">
|
|
122
|
+
{item.badge}
|
|
123
|
+
</span>
|
|
124
|
+
{/if}
|
|
125
|
+
{/if}
|
|
126
|
+
</button>
|
|
127
|
+
</li>
|
|
128
|
+
{/if}
|
|
129
|
+
{/each}
|
|
130
|
+
</ul>
|
|
131
|
+
</div>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
ChevronsUpDown,
|
|
4
|
+
LogOut,
|
|
5
|
+
User,
|
|
6
|
+
Settings,
|
|
7
|
+
} from 'lucide-svelte'
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
user,
|
|
11
|
+
navigate,
|
|
12
|
+
onLogout,
|
|
13
|
+
collapsed = false,
|
|
14
|
+
}: {
|
|
15
|
+
user: { name: string; email: string; role?: string }
|
|
16
|
+
navigate: (href: string) => void
|
|
17
|
+
onLogout: () => void
|
|
18
|
+
collapsed?: boolean
|
|
19
|
+
} = $props()
|
|
20
|
+
|
|
21
|
+
let menuOpen = $state(false)
|
|
22
|
+
|
|
23
|
+
function getInitials(name: string) {
|
|
24
|
+
return name
|
|
25
|
+
.split(' ')
|
|
26
|
+
.map((n) => n[0])
|
|
27
|
+
.join('')
|
|
28
|
+
.toUpperCase()
|
|
29
|
+
.slice(0, 2)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function handleClickOutside(e: MouseEvent) {
|
|
33
|
+
const target = e.target as HTMLElement
|
|
34
|
+
if (!target.closest('[data-nav-user-menu]')) {
|
|
35
|
+
menuOpen = false
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
$effect(() => {
|
|
40
|
+
if (menuOpen) {
|
|
41
|
+
document.addEventListener('click', handleClickOutside, true)
|
|
42
|
+
return () => document.removeEventListener('click', handleClickOutside, true)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<div class="relative" data-nav-user-menu>
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
onclick={() => menuOpen = !menuOpen}
|
|
51
|
+
class="flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left text-sm hover:bg-sidebar-accent transition-colors"
|
|
52
|
+
>
|
|
53
|
+
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted text-xs font-semibold">
|
|
54
|
+
{getInitials(user.name)}
|
|
55
|
+
</div>
|
|
56
|
+
{#if !collapsed}
|
|
57
|
+
<div class="grid flex-1 text-left text-sm leading-tight">
|
|
58
|
+
<span class="truncate font-semibold">{user.name}</span>
|
|
59
|
+
<span class="truncate text-xs text-muted-foreground">{user.email}</span>
|
|
60
|
+
</div>
|
|
61
|
+
<ChevronsUpDown class="ml-auto h-4 w-4 text-muted-foreground" />
|
|
62
|
+
{/if}
|
|
63
|
+
</button>
|
|
64
|
+
|
|
65
|
+
{#if menuOpen}
|
|
66
|
+
<div class="absolute bottom-full left-0 mb-1 w-56 rounded-lg border border-border bg-popover p-1 shadow-lg z-50">
|
|
67
|
+
<div class="flex items-center gap-2 px-2 py-1.5 text-sm">
|
|
68
|
+
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted text-xs font-semibold">
|
|
69
|
+
{getInitials(user.name)}
|
|
70
|
+
</div>
|
|
71
|
+
<div class="grid flex-1 text-left text-sm leading-tight">
|
|
72
|
+
<span class="truncate font-semibold">{user.name}</span>
|
|
73
|
+
<span class="truncate text-xs text-muted-foreground">{user.email}</span>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="my-1 h-px bg-border"></div>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onclick={() => { menuOpen = false; navigate('/account/profile') }}
|
|
80
|
+
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
|
|
81
|
+
>
|
|
82
|
+
<User class="h-4 w-4" />
|
|
83
|
+
Account
|
|
84
|
+
</button>
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
onclick={() => { menuOpen = false; navigate('/account/preferences') }}
|
|
88
|
+
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
|
|
89
|
+
>
|
|
90
|
+
<Settings class="h-4 w-4" />
|
|
91
|
+
Settings
|
|
92
|
+
</button>
|
|
93
|
+
<div class="my-1 h-px bg-border"></div>
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
onclick={() => { menuOpen = false; onLogout() }}
|
|
97
|
+
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-destructive hover:bg-accent transition-colors"
|
|
98
|
+
>
|
|
99
|
+
<LogOut class="h-4 w-4" />
|
|
100
|
+
Sign out
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
{/if}
|
|
104
|
+
</div>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Home,
|
|
3
|
+
Users,
|
|
4
|
+
Settings,
|
|
5
|
+
User,
|
|
6
|
+
Lock,
|
|
7
|
+
Palette,
|
|
8
|
+
BookOpen,
|
|
9
|
+
Github,
|
|
10
|
+
type Component,
|
|
11
|
+
} from 'lucide-svelte'
|
|
12
|
+
|
|
13
|
+
export interface NavItem {
|
|
14
|
+
title: string
|
|
15
|
+
url: string
|
|
16
|
+
icon: Component
|
|
17
|
+
badge?: string
|
|
18
|
+
external?: boolean
|
|
19
|
+
items?: NavItem[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface NavGroup {
|
|
23
|
+
title: string
|
|
24
|
+
items: NavItem[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const sidebarData: NavGroup[] = [
|
|
28
|
+
{
|
|
29
|
+
title: 'General',
|
|
30
|
+
items: [
|
|
31
|
+
{ title: 'Dashboard', url: '/dashboard', icon: Home },
|
|
32
|
+
{ title: 'Users', url: '/users', icon: Users },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
title: 'Documentation',
|
|
37
|
+
items: [
|
|
38
|
+
{ title: 'Docs', url: 'https://github.com/mantiqjs/mantiq#readme', icon: BookOpen, external: true },
|
|
39
|
+
{ title: 'GitHub', url: 'https://github.com/mantiqjs/mantiq', icon: Github, external: true },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: 'Account',
|
|
44
|
+
items: [
|
|
45
|
+
{
|
|
46
|
+
title: 'Settings',
|
|
47
|
+
url: '/account/profile',
|
|
48
|
+
icon: Settings,
|
|
49
|
+
items: [
|
|
50
|
+
{ title: 'Profile', url: '/account/profile', icon: User },
|
|
51
|
+
{ title: 'Security', url: '/account/security', icon: Lock },
|
|
52
|
+
{ title: 'Preferences', url: '/account/preferences', icon: Palette },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Sun, Moon } from 'lucide-svelte'
|
|
3
|
+
|
|
4
|
+
let isDark = $state(
|
|
5
|
+
typeof document !== 'undefined'
|
|
6
|
+
? document.documentElement.classList.contains('dark')
|
|
7
|
+
: false
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
function toggleTheme() {
|
|
11
|
+
const dark = document.documentElement.classList.toggle('dark')
|
|
12
|
+
localStorage.setItem('theme', dark ? 'dark' : 'light')
|
|
13
|
+
isDark = dark
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
onclick={toggleTheme}
|
|
20
|
+
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm font-medium hover:bg-accent transition-colors"
|
|
21
|
+
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
22
|
+
>
|
|
23
|
+
{#if isDark}
|
|
24
|
+
<Sun class="h-4 w-4" />
|
|
25
|
+
{:else}
|
|
26
|
+
<Moon class="h-4 w-4" />
|
|
27
|
+
{/if}
|
|
28
|
+
</button>
|