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.
Files changed (220) hide show
  1. package/package.json +2 -1
  2. package/skeleton/.env.example +64 -0
  3. package/skeleton/README.md +46 -0
  4. package/skeleton/app/Console/Commands/.gitkeep +0 -0
  5. package/skeleton/app/Enums/UserStatus.ts +7 -0
  6. package/skeleton/app/Http/Controllers/HomeController.ts +78 -0
  7. package/skeleton/app/Http/Middleware/.gitkeep +0 -0
  8. package/skeleton/app/Models/User.ts +7 -0
  9. package/skeleton/app/Providers/AppServiceProvider.ts +25 -0
  10. package/skeleton/app/Providers/DatabaseServiceProvider.ts +17 -0
  11. package/skeleton/bootstrap/.gitkeep +0 -0
  12. package/skeleton/config/ai.ts +51 -0
  13. package/skeleton/config/app.ts +108 -0
  14. package/skeleton/config/auth.ts +51 -0
  15. package/skeleton/config/broadcasting.ts +93 -0
  16. package/skeleton/config/cache.ts +61 -0
  17. package/skeleton/config/cors.ts +77 -0
  18. package/skeleton/config/database.ts +120 -0
  19. package/skeleton/config/filesystem.ts +58 -0
  20. package/skeleton/config/hashing.ts +47 -0
  21. package/skeleton/config/heartbeat.ts +112 -0
  22. package/skeleton/config/logging.ts +58 -0
  23. package/skeleton/config/mail.ts +93 -0
  24. package/skeleton/config/notify.ts +141 -0
  25. package/skeleton/config/queue.ts +59 -0
  26. package/skeleton/config/search.ts +96 -0
  27. package/skeleton/config/services.ts +110 -0
  28. package/skeleton/config/session.ts +84 -0
  29. package/skeleton/config/vite.ts +33 -0
  30. package/skeleton/database/factories/.gitkeep +0 -0
  31. package/skeleton/database/migrations/001_create_users_table.ts +19 -0
  32. package/skeleton/database/migrations/002_create_personal_access_tokens_table.ts +22 -0
  33. package/skeleton/database/seeders/DatabaseSeeder.ts +7 -0
  34. package/skeleton/index.ts +20 -0
  35. package/skeleton/mantiq.ts +8 -0
  36. package/skeleton/package.json +34 -0
  37. package/skeleton/public/.gitkeep +0 -0
  38. package/skeleton/routes/api.ts +8 -0
  39. package/skeleton/routes/channels.ts +23 -0
  40. package/skeleton/routes/console.ts +24 -0
  41. package/skeleton/routes/web.ts +6 -0
  42. package/skeleton/storage/cache/.gitkeep +0 -0
  43. package/skeleton/storage/framework/.gitkeep +0 -0
  44. package/skeleton/tests/feature/api.test.ts +14 -0
  45. package/skeleton/tests/feature/home.test.ts +17 -0
  46. package/skeleton/tests/unit/example.test.ts +11 -0
  47. package/skeleton/tsconfig.json +27 -0
  48. package/src/index.ts +289 -25
  49. package/src/templates.ts +141 -945
  50. package/src/terminal.ts +64 -0
  51. package/stubs/api-only/routes/api.ts.stub +24 -0
  52. package/stubs/api-only/tests/feature/token-auth.test.ts.stub +69 -0
  53. package/stubs/auth/api/app/Http/Controllers/ApiAuthController.ts.stub +57 -0
  54. package/stubs/auth/api/routes/api.ts.stub +24 -0
  55. package/stubs/auth/api/tests/feature/token-auth.test.ts.stub +69 -0
  56. package/stubs/auth/shared/app/Http/Requests/LoginRequest.ts.stub +10 -0
  57. package/stubs/auth/shared/app/Http/Requests/RegisterRequest.ts.stub +11 -0
  58. package/stubs/auth/web/app/Http/Controllers/AuthController.ts.stub +43 -0
  59. package/stubs/auth/web/app/Http/Controllers/PageController.ts.stub +66 -0
  60. package/stubs/auth/web/routes/web.ts.stub +25 -0
  61. package/stubs/auth/web/svelte/src/App.svelte.stub +77 -0
  62. package/stubs/auth/web/svelte/src/pages.ts.stub +17 -0
  63. package/stubs/auth/web/tests/feature/auth.test.ts.stub +69 -0
  64. package/stubs/auth/web/vue/src/App.vue.stub +74 -0
  65. package/stubs/auth/web/vue/src/pages.ts.stub +17 -0
  66. package/stubs/manifest.json +630 -2
  67. package/stubs/noauth/app/Http/Controllers/PageController.ts.stub +41 -0
  68. package/stubs/noauth/app/Models/User.ts.stub +5 -0
  69. package/stubs/noauth/database/migrations/001_create_users_table.ts.stub +17 -0
  70. package/stubs/noauth/routes/api.ts.stub +16 -0
  71. package/stubs/noauth/routes/web.ts.stub +15 -0
  72. package/stubs/noauth/svelte/src/App.svelte.stub +68 -0
  73. package/stubs/noauth/svelte/src/pages.ts.stub +7 -0
  74. package/stubs/noauth/vue/src/App.vue.stub +62 -0
  75. package/stubs/noauth/vue/src/pages.ts.stub +7 -0
  76. package/stubs/react/src/App.tsx.stub +4 -2
  77. package/stubs/react/src/components/layout/search-dialog.tsx.stub +2 -2
  78. package/stubs/react/src/components/layout/sidebar-data.ts.stub +2 -2
  79. package/stubs/react/src/lib/api.ts.stub +30 -6
  80. package/stubs/react/src/pages/Login.tsx.stub +3 -3
  81. package/stubs/react/src/pages/users/dialogs.tsx.stub +7 -26
  82. package/stubs/react/vite.config.ts.stub +26 -3
  83. package/stubs/shared/app/Http/Controllers/ApiAuthController.ts.stub +57 -0
  84. package/stubs/shared/app/Http/Controllers/AuthController.ts.stub +14 -38
  85. package/stubs/shared/app/Http/Controllers/PageController.ts.stub +3 -3
  86. package/stubs/shared/app/Http/Controllers/UserController.ts.stub +61 -0
  87. package/stubs/shared/app/Http/Requests/LoginRequest.ts.stub +10 -0
  88. package/stubs/shared/app/Http/Requests/RegisterRequest.ts.stub +11 -0
  89. package/stubs/shared/app/Http/Requests/StoreUserRequest.ts.stub +11 -0
  90. package/stubs/shared/app/Http/Requests/UpdateUserRequest.ts.stub +11 -0
  91. package/stubs/shared/config/app.ts.stub +36 -0
  92. package/stubs/shared/config/vite.ts.stub +8 -0
  93. package/stubs/shared/database/factories/UserFactory.ts.stub +4 -6
  94. package/stubs/shared/routes/api.ts.stub +12 -102
  95. package/stubs/shared/routes/web.ts.stub +5 -3
  96. package/stubs/shared/tests/feature/auth.test.ts.stub +69 -0
  97. package/stubs/shared/tests/feature/users.test.ts.stub +90 -0
  98. package/stubs/svelte/src/App.svelte.stub +1 -1
  99. package/stubs/svelte/src/lib/api.ts.stub +30 -6
  100. package/stubs/svelte/src/main.ts.stub +3 -1
  101. package/stubs/svelte/src/pages/Login.svelte.stub +3 -3
  102. package/stubs/svelte/vite.config.ts.stub +20 -1
  103. package/stubs/tailwind-only/react/src/components/layout/app-sidebar.tsx.stub +68 -0
  104. package/stubs/tailwind-only/react/src/components/layout/authenticated-layout.tsx.stub +57 -0
  105. package/stubs/tailwind-only/react/src/components/layout/header.tsx.stub +52 -0
  106. package/stubs/tailwind-only/react/src/components/layout/main.tsx.stub +21 -0
  107. package/stubs/tailwind-only/react/src/components/layout/nav-group.tsx.stub +185 -0
  108. package/stubs/tailwind-only/react/src/components/layout/nav-user.tsx.stub +106 -0
  109. package/stubs/tailwind-only/react/src/components/layout/sidebar-data.ts.stub +58 -0
  110. package/stubs/tailwind-only/react/src/components/layout/theme-toggle.tsx.stub +36 -0
  111. package/stubs/tailwind-only/react/src/components/layout/top-nav.tsx.stub +72 -0
  112. package/stubs/tailwind-only/react/src/lib/utils.ts.stub +6 -0
  113. package/stubs/tailwind-only/react/src/pages/Dashboard.tsx.stub +205 -0
  114. package/stubs/tailwind-only/react/src/pages/Login.tsx.stub +122 -0
  115. package/stubs/tailwind-only/react/src/pages/Register.tsx.stub +137 -0
  116. package/stubs/tailwind-only/react/src/pages/Users.tsx.stub +426 -0
  117. package/stubs/tailwind-only/react/src/pages/account/layout.tsx.stub +64 -0
  118. package/stubs/tailwind-only/react/src/pages/account/preferences.tsx.stub +80 -0
  119. package/stubs/tailwind-only/react/src/pages/account/profile.tsx.stub +67 -0
  120. package/stubs/tailwind-only/react/src/pages/account/security.tsx.stub +91 -0
  121. package/stubs/tailwind-only/react/src/style.css.stub +14 -0
  122. package/stubs/tailwind-only/svelte/src/lib/components/layout/app-sidebar.svelte.stub +104 -0
  123. package/stubs/tailwind-only/svelte/src/lib/components/layout/authenticated-layout.svelte.stub +51 -0
  124. package/stubs/tailwind-only/svelte/src/lib/components/layout/header.svelte.stub +66 -0
  125. package/stubs/tailwind-only/svelte/src/lib/components/layout/main.svelte.stub +26 -0
  126. package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-group.svelte.stub +131 -0
  127. package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-user.svelte.stub +104 -0
  128. package/stubs/tailwind-only/svelte/src/lib/components/layout/sidebar-data.ts.stub +57 -0
  129. package/stubs/tailwind-only/svelte/src/lib/components/layout/theme-toggle.svelte.stub +28 -0
  130. package/stubs/tailwind-only/svelte/src/lib/components/layout/top-nav.svelte.stub +99 -0
  131. package/stubs/tailwind-only/svelte/src/lib/utils.ts.stub +6 -0
  132. package/stubs/tailwind-only/svelte/src/pages/Dashboard.svelte.stub +192 -0
  133. package/stubs/tailwind-only/svelte/src/pages/Login.svelte.stub +120 -0
  134. package/stubs/tailwind-only/svelte/src/pages/Register.svelte.stub +134 -0
  135. package/stubs/tailwind-only/svelte/src/pages/Users.svelte.stub +293 -0
  136. package/stubs/tailwind-only/svelte/src/pages/account/Layout.svelte.stub +61 -0
  137. package/stubs/tailwind-only/svelte/src/pages/account/Preferences.svelte.stub +81 -0
  138. package/stubs/tailwind-only/svelte/src/pages/account/Profile.svelte.stub +76 -0
  139. package/stubs/tailwind-only/svelte/src/pages/account/Security.svelte.stub +140 -0
  140. package/stubs/tailwind-only/svelte/src/style.css.stub +127 -0
  141. package/stubs/tailwind-only/vue/src/components/layout/AppSidebar.vue.stub +60 -0
  142. package/stubs/tailwind-only/vue/src/components/layout/AuthenticatedLayout.vue.stub +73 -0
  143. package/stubs/tailwind-only/vue/src/components/layout/Header.vue.stub +54 -0
  144. package/stubs/tailwind-only/vue/src/components/layout/Main.vue.stub +22 -0
  145. package/stubs/tailwind-only/vue/src/components/layout/NavGroup.vue.stub +107 -0
  146. package/stubs/tailwind-only/vue/src/components/layout/NavUser.vue.stub +104 -0
  147. package/stubs/tailwind-only/vue/src/components/layout/ThemeToggle.vue.stub +28 -0
  148. package/stubs/tailwind-only/vue/src/components/layout/TopNav.vue.stub +86 -0
  149. package/stubs/tailwind-only/vue/src/components/layout/sidebar-data.ts.stub +57 -0
  150. package/stubs/tailwind-only/vue/src/lib/utils.ts.stub +7 -0
  151. package/stubs/tailwind-only/vue/src/pages/Dashboard.vue.stub +195 -0
  152. package/stubs/tailwind-only/vue/src/pages/Login.vue.stub +121 -0
  153. package/stubs/tailwind-only/vue/src/pages/Register.vue.stub +137 -0
  154. package/stubs/tailwind-only/vue/src/pages/Users.vue.stub +401 -0
  155. package/stubs/tailwind-only/vue/src/pages/account/Layout.vue.stub +56 -0
  156. package/stubs/tailwind-only/vue/src/pages/account/Preferences.vue.stub +81 -0
  157. package/stubs/tailwind-only/vue/src/pages/account/Profile.vue.stub +69 -0
  158. package/stubs/tailwind-only/vue/src/pages/account/Security.vue.stub +85 -0
  159. package/stubs/tailwind-only/vue/src/style.css.stub +26 -0
  160. package/stubs/themes/corporate/react/src/components/layout/app-sidebar.tsx.stub +74 -0
  161. package/stubs/themes/corporate/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  162. package/stubs/themes/corporate/react/src/pages/Dashboard.tsx.stub +310 -0
  163. package/stubs/themes/corporate/react/src/pages/Login.tsx.stub +122 -0
  164. package/stubs/themes/corporate/react/src/pages/Register.tsx.stub +144 -0
  165. package/stubs/themes/corporate/react/src/style.css.stub +135 -0
  166. package/stubs/themes/corporate/svelte/src/pages/Dashboard.svelte.stub +271 -0
  167. package/stubs/themes/corporate/svelte/src/pages/Login.svelte.stub +117 -0
  168. package/stubs/themes/corporate/svelte/src/pages/Register.svelte.stub +138 -0
  169. package/stubs/themes/corporate/svelte/src/style.css.stub +134 -0
  170. package/stubs/themes/corporate/vue/src/pages/Dashboard.vue.stub +325 -0
  171. package/stubs/themes/corporate/vue/src/pages/Login.vue.stub +118 -0
  172. package/stubs/themes/corporate/vue/src/pages/Register.vue.stub +139 -0
  173. package/stubs/themes/corporate/vue/src/style.css.stub +134 -0
  174. package/stubs/themes/minimal/react/src/components/layout/app-sidebar.tsx.stub +68 -0
  175. package/stubs/themes/minimal/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  176. package/stubs/themes/minimal/react/src/pages/Dashboard.tsx.stub +166 -0
  177. package/stubs/themes/minimal/react/src/pages/Login.tsx.stub +95 -0
  178. package/stubs/themes/minimal/react/src/pages/Register.tsx.stub +73 -0
  179. package/stubs/themes/minimal/react/src/style.css.stub +142 -0
  180. package/stubs/themes/minimal/svelte/src/pages/Dashboard.svelte.stub +149 -0
  181. package/stubs/themes/minimal/svelte/src/pages/Login.svelte.stub +90 -0
  182. package/stubs/themes/minimal/svelte/src/pages/Register.svelte.stub +70 -0
  183. package/stubs/themes/minimal/svelte/src/style.css.stub +142 -0
  184. package/stubs/themes/minimal/vue/src/pages/Dashboard.vue.stub +163 -0
  185. package/stubs/themes/minimal/vue/src/pages/Login.vue.stub +91 -0
  186. package/stubs/themes/minimal/vue/src/pages/Register.vue.stub +73 -0
  187. package/stubs/themes/minimal/vue/src/style.css.stub +142 -0
  188. package/stubs/themes/starter/react/src/components/layout/app-sidebar.tsx.stub +74 -0
  189. package/stubs/themes/starter/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  190. package/stubs/themes/starter/react/src/pages/Dashboard.tsx.stub +236 -0
  191. package/stubs/themes/starter/react/src/pages/Login.tsx.stub +131 -0
  192. package/stubs/themes/starter/react/src/pages/Register.tsx.stub +145 -0
  193. package/stubs/themes/starter/react/src/style.css.stub +141 -0
  194. package/stubs/themes/starter/svelte/src/pages/Dashboard.svelte.stub +212 -0
  195. package/stubs/themes/starter/svelte/src/pages/Login.svelte.stub +126 -0
  196. package/stubs/themes/starter/svelte/src/pages/Register.svelte.stub +139 -0
  197. package/stubs/themes/starter/svelte/src/style.css.stub +141 -0
  198. package/stubs/themes/starter/vue/src/pages/Dashboard.vue.stub +228 -0
  199. package/stubs/themes/starter/vue/src/pages/Login.vue.stub +127 -0
  200. package/stubs/themes/starter/vue/src/pages/Register.vue.stub +140 -0
  201. package/stubs/themes/starter/vue/src/style.css.stub +141 -0
  202. package/stubs/themes/workspace/react/src/components/layout/app-sidebar.tsx.stub +97 -0
  203. package/stubs/themes/workspace/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
  204. package/stubs/themes/workspace/react/src/pages/Dashboard.tsx.stub +304 -0
  205. package/stubs/themes/workspace/react/src/pages/Login.tsx.stub +131 -0
  206. package/stubs/themes/workspace/react/src/pages/Register.tsx.stub +82 -0
  207. package/stubs/themes/workspace/react/src/style.css.stub +138 -0
  208. package/stubs/themes/workspace/svelte/src/pages/Dashboard.svelte.stub +215 -0
  209. package/stubs/themes/workspace/svelte/src/pages/Login.svelte.stub +124 -0
  210. package/stubs/themes/workspace/svelte/src/pages/Register.svelte.stub +76 -0
  211. package/stubs/themes/workspace/svelte/src/style.css.stub +134 -0
  212. package/stubs/themes/workspace/vue/src/pages/Dashboard.vue.stub +220 -0
  213. package/stubs/themes/workspace/vue/src/pages/Login.vue.stub +128 -0
  214. package/stubs/themes/workspace/vue/src/pages/Register.vue.stub +80 -0
  215. package/stubs/themes/workspace/vue/src/style.css.stub +134 -0
  216. package/stubs/vue/src/App.vue.stub +2 -1
  217. package/stubs/vue/src/lib/api.ts.stub +30 -6
  218. package/stubs/vue/src/main.ts.stub +3 -1
  219. package/stubs/vue/src/pages/Login.vue.stub +3 -3
  220. 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>