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,52 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { cn } from '@/lib/utils'
3
+ import { ThemeToggle } from '@/components/layout/theme-toggle'
4
+ import { useSidebarToggle } from '@/components/layout/authenticated-layout'
5
+
6
+ interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
7
+ fixed?: boolean
8
+ navigate?: (href: string) => void
9
+ }
10
+
11
+ export function Header({ className, fixed, children, navigate, ...props }: HeaderProps) {
12
+ const [offset, setOffset] = useState(0)
13
+ const { toggle } = useSidebarToggle()
14
+
15
+ useEffect(() => {
16
+ const onScroll = () => setOffset(document.body.scrollTop || document.documentElement.scrollTop)
17
+ document.addEventListener('scroll', onScroll, { passive: true })
18
+ return () => document.removeEventListener('scroll', onScroll)
19
+ }, [])
20
+
21
+ return (
22
+ <header
23
+ className={cn(
24
+ 'flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear',
25
+ fixed && 'sticky top-0 z-10 bg-white dark:bg-gray-950',
26
+ offset > 10 && fixed ? 'border-b border-gray-200 dark:border-gray-800' : '',
27
+ className,
28
+ )}
29
+ {...props}
30
+ >
31
+ <div className="flex w-full items-center gap-2 px-4 lg:px-6">
32
+ {/* Sidebar toggle */}
33
+ <button
34
+ type="button"
35
+ onClick={toggle}
36
+ className="-ml-1 inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
37
+ >
38
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
39
+ <rect width="18" height="18" x="3" y="3" rx="2" />
40
+ <path d="M9 3v18" />
41
+ </svg>
42
+ <span className="sr-only">Toggle sidebar</span>
43
+ </button>
44
+ <div className="mx-1 hidden h-4 w-px bg-gray-200 dark:bg-gray-700 md:block" />
45
+ {children}
46
+ <div className="ms-auto flex items-center gap-2">
47
+ <ThemeToggle />
48
+ </div>
49
+ </div>
50
+ </header>
51
+ )
52
+ }
@@ -0,0 +1,21 @@
1
+ import { cn } from '@/lib/utils'
2
+
3
+ interface MainProps extends React.HTMLAttributes<HTMLElement> {
4
+ fixed?: boolean
5
+ }
6
+
7
+ export function Main({ fixed, className, children, ...props }: MainProps) {
8
+ return (
9
+ <main
10
+ className={cn(
11
+ fixed && 'flex flex-grow flex-col overflow-hidden',
12
+ className,
13
+ )}
14
+ {...props}
15
+ >
16
+ <div className="px-4 py-6 lg:px-6">
17
+ {children}
18
+ </div>
19
+ </main>
20
+ )
21
+ }
@@ -0,0 +1,185 @@
1
+ import { useState } from 'react'
2
+ import { cn } from '@/lib/utils'
3
+ import type { NavGroup as NavGroupData } from './sidebar-data'
4
+
5
+ interface NavGroupProps {
6
+ group: NavGroupData
7
+ activePath: string
8
+ navigate: (href: string) => void
9
+ collapsed?: boolean
10
+ }
11
+
12
+ function SvgIcon({ path, className }: { path: string; className?: string }) {
13
+ return (
14
+ <svg
15
+ xmlns="http://www.w3.org/2000/svg"
16
+ viewBox="0 0 24 24"
17
+ fill="none"
18
+ stroke="currentColor"
19
+ strokeWidth="2"
20
+ strokeLinecap="round"
21
+ strokeLinejoin="round"
22
+ className={cn('h-4 w-4', className)}
23
+ >
24
+ {path.split(' M').map((d, i) => (
25
+ <path key={i} d={i === 0 ? d : `M${d}`} />
26
+ ))}
27
+ </svg>
28
+ )
29
+ }
30
+
31
+ function isActive(itemUrl: string, activePath: string): boolean {
32
+ if (itemUrl === activePath) return true
33
+ const itemBase = itemUrl.split('?')[0]
34
+ const activeBase = activePath.split('?')[0]
35
+ return itemBase === activeBase
36
+ }
37
+
38
+ function isGroupActive(
39
+ items: NavGroupData['items'][number]['items'],
40
+ activePath: string,
41
+ ): boolean {
42
+ if (!items) return false
43
+ return items.some((sub) => isActive(sub.url, activePath))
44
+ }
45
+
46
+ export function NavGroup({ group, activePath, navigate, collapsed }: NavGroupProps) {
47
+ return (
48
+ <div className="px-3 py-2">
49
+ {!collapsed && (
50
+ <h3 className="mb-1 px-2 text-[11px] font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
51
+ {group.title}
52
+ </h3>
53
+ )}
54
+ <ul className="space-y-0.5">
55
+ {group.items.map((item) =>
56
+ item.items && item.items.length > 0 ? (
57
+ <CollapsibleNavItem
58
+ key={item.title}
59
+ item={item}
60
+ activePath={activePath}
61
+ navigate={navigate}
62
+ collapsed={collapsed}
63
+ />
64
+ ) : item.external ? (
65
+ <li key={item.title}>
66
+ <a
67
+ href={item.url}
68
+ target="_blank"
69
+ rel="noopener noreferrer"
70
+ className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
71
+ title={collapsed ? item.title : undefined}
72
+ >
73
+ <SvgIcon path={item.iconPath} />
74
+ {!collapsed && (
75
+ <>
76
+ <span className="flex-1">{item.title}</span>
77
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="ml-auto h-3 w-3 text-gray-400">
78
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3" />
79
+ </svg>
80
+ </>
81
+ )}
82
+ </a>
83
+ </li>
84
+ ) : (
85
+ <li key={item.title}>
86
+ <button
87
+ type="button"
88
+ onClick={() => navigate(item.url)}
89
+ className={cn(
90
+ 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',
91
+ isActive(item.url, activePath)
92
+ ? 'bg-gray-100 font-medium text-gray-900 dark:bg-gray-800 dark:text-gray-50'
93
+ : 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800',
94
+ )}
95
+ title={collapsed ? item.title : undefined}
96
+ >
97
+ <SvgIcon path={item.iconPath} />
98
+ {!collapsed && (
99
+ <>
100
+ <span>{item.title}</span>
101
+ {item.badge && (
102
+ <span className="ml-auto rounded bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium text-gray-600 dark:bg-gray-800 dark:text-gray-400">
103
+ {item.badge}
104
+ </span>
105
+ )}
106
+ </>
107
+ )}
108
+ </button>
109
+ </li>
110
+ ),
111
+ )}
112
+ </ul>
113
+ </div>
114
+ )
115
+ }
116
+
117
+ function CollapsibleNavItem({
118
+ item,
119
+ activePath,
120
+ navigate,
121
+ collapsed,
122
+ }: {
123
+ item: NavGroupData['items'][number]
124
+ activePath: string
125
+ navigate: (href: string) => void
126
+ collapsed?: boolean
127
+ }) {
128
+ const childActive = isGroupActive(item.items, activePath)
129
+ const [open, setOpen] = useState(childActive)
130
+
131
+ return (
132
+ <li>
133
+ <button
134
+ type="button"
135
+ onClick={() => collapsed ? navigate(item.url) : setOpen(!open)}
136
+ className={cn(
137
+ 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',
138
+ childActive
139
+ ? 'bg-gray-100 font-medium text-gray-900 dark:bg-gray-800 dark:text-gray-50'
140
+ : 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800',
141
+ )}
142
+ title={collapsed ? item.title : undefined}
143
+ >
144
+ <SvgIcon path={item.iconPath} />
145
+ {!collapsed && (
146
+ <>
147
+ <span className="flex-1 text-left">{item.title}</span>
148
+ <svg
149
+ xmlns="http://www.w3.org/2000/svg"
150
+ viewBox="0 0 24 24"
151
+ fill="none"
152
+ stroke="currentColor"
153
+ strokeWidth="2"
154
+ strokeLinecap="round"
155
+ strokeLinejoin="round"
156
+ className={cn('h-4 w-4 transition-transform', open && 'rotate-90')}
157
+ >
158
+ <path d="m9 18 6-6-6-6" />
159
+ </svg>
160
+ </>
161
+ )}
162
+ </button>
163
+ {open && !collapsed && item.items && (
164
+ <ul className="ml-4 mt-0.5 space-y-0.5 border-l border-gray-200 pl-2 dark:border-gray-700">
165
+ {item.items.map((sub) => (
166
+ <li key={sub.title}>
167
+ <button
168
+ type="button"
169
+ onClick={() => navigate(sub.url)}
170
+ className={cn(
171
+ 'flex w-full items-center rounded-md px-2 py-1.5 text-sm transition-colors',
172
+ isActive(sub.url, activePath)
173
+ ? 'font-medium text-gray-900 dark:text-gray-50'
174
+ : 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-50',
175
+ )}
176
+ >
177
+ {sub.title}
178
+ </button>
179
+ </li>
180
+ ))}
181
+ </ul>
182
+ )}
183
+ </li>
184
+ )
185
+ }
@@ -0,0 +1,106 @@
1
+ import { useState, useRef, useEffect } from 'react'
2
+
3
+ export interface NavUserProps {
4
+ user: {
5
+ name: string
6
+ email: string
7
+ role?: string
8
+ }
9
+ navigate: (href: string) => void
10
+ onLogout: () => void
11
+ collapsed?: boolean
12
+ }
13
+
14
+ function getInitials(name: string) {
15
+ return name
16
+ .split(' ')
17
+ .map((n) => n[0])
18
+ .join('')
19
+ .toUpperCase()
20
+ .slice(0, 2)
21
+ }
22
+
23
+ export function NavUser({ user, navigate, onLogout, collapsed }: NavUserProps) {
24
+ const [open, setOpen] = useState(false)
25
+ const ref = useRef<HTMLDivElement>(null)
26
+
27
+ useEffect(() => {
28
+ const onClick = (e: MouseEvent) => {
29
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
30
+ }
31
+ document.addEventListener('mousedown', onClick)
32
+ return () => document.removeEventListener('mousedown', onClick)
33
+ }, [])
34
+
35
+ return (
36
+ <div ref={ref} className="relative px-3 py-2">
37
+ <button
38
+ type="button"
39
+ onClick={() => setOpen(!open)}
40
+ className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
41
+ >
42
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gray-100 text-xs font-semibold text-gray-700 dark:bg-gray-800 dark:text-gray-300">
43
+ {getInitials(user.name)}
44
+ </div>
45
+ {!collapsed && (
46
+ <>
47
+ <div className="grid flex-1 text-left text-sm leading-tight">
48
+ <span className="truncate font-semibold text-gray-900 dark:text-gray-50">{user.name}</span>
49
+ <span className="truncate text-xs text-gray-500 dark:text-gray-400">{user.email}</span>
50
+ </div>
51
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="ml-auto h-4 w-4 text-gray-400">
52
+ <path d="m7 15 5 5 5-5M7 9l5-5 5 5" />
53
+ </svg>
54
+ </>
55
+ )}
56
+ </button>
57
+
58
+ {open && (
59
+ <div className="absolute bottom-full left-3 right-3 z-50 mb-1 rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-900">
60
+ {/* User info */}
61
+ <div className="flex items-center gap-2 px-3 py-2">
62
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gray-100 text-xs font-semibold text-gray-700 dark:bg-gray-800 dark:text-gray-300">
63
+ {getInitials(user.name)}
64
+ </div>
65
+ <div className="grid flex-1 text-sm leading-tight">
66
+ <span className="truncate font-semibold text-gray-900 dark:text-gray-50">{user.name}</span>
67
+ <span className="truncate text-xs text-gray-500 dark:text-gray-400">{user.email}</span>
68
+ </div>
69
+ </div>
70
+ <div className="my-1 h-px bg-gray-100 dark:bg-gray-800" />
71
+ <button
72
+ type="button"
73
+ onClick={() => { setOpen(false); navigate('/account/profile') }}
74
+ className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
75
+ >
76
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
77
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" />
78
+ </svg>
79
+ Account
80
+ </button>
81
+ <button
82
+ type="button"
83
+ onClick={() => { setOpen(false); navigate('/account?tab=preferences') }}
84
+ className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
85
+ >
86
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
87
+ <path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2zM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" />
88
+ </svg>
89
+ Settings
90
+ </button>
91
+ <div className="my-1 h-px bg-gray-100 dark:bg-gray-800" />
92
+ <button
93
+ type="button"
94
+ onClick={() => { setOpen(false); onLogout() }}
95
+ className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-red-600 transition-colors hover:bg-gray-100 dark:text-red-400 dark:hover:bg-gray-800"
96
+ >
97
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
98
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9" />
99
+ </svg>
100
+ Sign out
101
+ </button>
102
+ </div>
103
+ )}
104
+ </div>
105
+ )
106
+ }
@@ -0,0 +1,58 @@
1
+ export interface NavItem {
2
+ title: string
3
+ url: string
4
+ /** SVG path(s) for a 24x24 viewBox icon */
5
+ iconPath: string
6
+ badge?: string
7
+ external?: boolean
8
+ items?: NavItem[]
9
+ }
10
+
11
+ export interface NavGroup {
12
+ title: string
13
+ items: NavItem[]
14
+ }
15
+
16
+ // SVG paths (stroke icons, 24x24)
17
+ const icons = {
18
+ home: 'M3 9.5L12 3l9 6.5V20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.5z M9 21V12h6v9',
19
+ users: 'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2 M9 7a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M22 21v-2a4 4 0 0 0-3-3.87 M16 3.13a4 4 0 0 1 0 7.75',
20
+ settings: 'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',
21
+ user: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2 M12 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8z',
22
+ lock: 'M19 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2z M7 11V7a5 5 0 0 1 10 0v4',
23
+ palette: 'M12 2a10 10 0 0 0 0 20 2 2 0 0 0 2-2v-.09a1.65 1.65 0 0 1 3 0v.09a2 2 0 0 0 2 2h.44A10 10 0 0 0 12 2z',
24
+ book: 'M4 19.5A2.5 2.5 0 0 1 6.5 17H20 M4 4.5A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5',
25
+ externalLink: 'M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6 M15 3h6v6 M10 14L21 3',
26
+ }
27
+
28
+ export const sidebarData: NavGroup[] = [
29
+ {
30
+ title: 'General',
31
+ items: [
32
+ { title: 'Dashboard', url: '/dashboard', iconPath: icons.home },
33
+ { title: 'Users', url: '/users', iconPath: icons.users },
34
+ ],
35
+ },
36
+ {
37
+ title: 'Documentation',
38
+ items: [
39
+ { title: 'Docs', url: 'https://github.com/mantiqjs/mantiq#readme', iconPath: icons.book, external: true },
40
+ { title: 'GitHub', url: 'https://github.com/mantiqjs/mantiq', iconPath: icons.externalLink, external: true },
41
+ ],
42
+ },
43
+ {
44
+ title: 'Account',
45
+ items: [
46
+ {
47
+ title: 'Settings',
48
+ url: '/account/profile',
49
+ iconPath: icons.settings,
50
+ items: [
51
+ { title: 'Profile', url: '/account/profile', iconPath: icons.user },
52
+ { title: 'Security', url: '/account/security', iconPath: icons.lock },
53
+ { title: 'Preferences', url: '/account/preferences', iconPath: icons.palette },
54
+ ],
55
+ },
56
+ ],
57
+ },
58
+ ]
@@ -0,0 +1,36 @@
1
+ import { useState } from 'react'
2
+
3
+ export function ThemeToggle() {
4
+ const [isDark, setIsDark] = useState(() =>
5
+ typeof document !== 'undefined'
6
+ ? document.documentElement.classList.contains('dark')
7
+ : false,
8
+ )
9
+
10
+ const toggleTheme = () => {
11
+ const dark = document.documentElement.classList.toggle('dark')
12
+ localStorage.setItem('theme', dark ? 'dark' : 'light')
13
+ setIsDark(dark)
14
+ }
15
+
16
+ return (
17
+ <button
18
+ type="button"
19
+ onClick={toggleTheme}
20
+ className="inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-50"
21
+ title={isDark ? 'Light mode' : 'Dark mode'}
22
+ >
23
+ {isDark ? (
24
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
25
+ <circle cx="12" cy="12" r="4" />
26
+ <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
27
+ </svg>
28
+ ) : (
29
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
30
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
31
+ </svg>
32
+ )}
33
+ <span className="sr-only">Toggle theme</span>
34
+ </button>
35
+ )
36
+ }
@@ -0,0 +1,72 @@
1
+ import { cn } from '@/lib/utils'
2
+
3
+ interface TopNavLink {
4
+ title: string
5
+ href: string
6
+ isActive?: boolean
7
+ disabled?: boolean
8
+ }
9
+
10
+ interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
11
+ links: TopNavLink[]
12
+ onLinkClick?: (href: string) => void
13
+ }
14
+
15
+ export function TopNav({
16
+ className,
17
+ links,
18
+ onLinkClick,
19
+ ...props
20
+ }: TopNavProps) {
21
+ const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, href: string) => {
22
+ if (onLinkClick) {
23
+ e.preventDefault()
24
+ onLinkClick(href)
25
+ }
26
+ }
27
+
28
+ return (
29
+ <>
30
+ {/* Desktop navigation */}
31
+ <nav
32
+ className={cn(
33
+ 'hidden items-center gap-4 md:flex lg:gap-6',
34
+ className,
35
+ )}
36
+ {...props}
37
+ >
38
+ {links.map((link) => (
39
+ <a
40
+ key={link.href}
41
+ href={link.href}
42
+ onClick={(e) => handleClick(e, link.href)}
43
+ className={cn(
44
+ 'text-sm font-medium transition-colors hover:text-gray-900 dark:hover:text-gray-50',
45
+ link.isActive
46
+ ? 'text-gray-900 dark:text-gray-50'
47
+ : 'text-gray-500 dark:text-gray-400',
48
+ link.disabled && 'pointer-events-none opacity-50',
49
+ )}
50
+ >
51
+ {link.title}
52
+ </a>
53
+ ))}
54
+ </nav>
55
+
56
+ {/* Mobile navigation */}
57
+ <div className="md:hidden">
58
+ <select
59
+ onChange={(e) => onLinkClick?.(e.target.value)}
60
+ defaultValue={links.find(l => l.isActive)?.href ?? ''}
61
+ className="rounded-md border border-gray-200 bg-white px-3 py-1.5 text-sm dark:border-gray-700 dark:bg-gray-900"
62
+ >
63
+ {links.map((link) => (
64
+ <option key={link.href} value={link.href} disabled={link.disabled}>
65
+ {link.title}
66
+ </option>
67
+ ))}
68
+ </select>
69
+ </div>
70
+ </>
71
+ )
72
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }