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,293 @@
1
+ <script lang="ts">
2
+ import { api } from '$lib/api'
3
+ import AuthenticatedLayout from '$lib/components/layout/authenticated-layout.svelte'
4
+ import Header from '$lib/components/layout/header.svelte'
5
+ import Main from '$lib/components/layout/main.svelte'
6
+ import {
7
+ MoreHorizontal,
8
+ UserPlus,
9
+ Mail,
10
+ Pencil,
11
+ Trash2,
12
+ ChevronLeft,
13
+ ChevronRight,
14
+ Search,
15
+ } from 'lucide-svelte'
16
+
17
+ export interface UserType {
18
+ id: number
19
+ name: string
20
+ email: string
21
+ status: string
22
+ created_at: string
23
+ }
24
+
25
+ let {
26
+ appName = 'MantiqJS',
27
+ currentUser = null,
28
+ users: initialUsers = [],
29
+ navigate,
30
+ }: {
31
+ appName?: string
32
+ currentUser?: { id: number; name: string; email: string } | null
33
+ users?: UserType[]
34
+ navigate: (href: string) => void
35
+ [key: string]: any
36
+ } = $props()
37
+
38
+ // Mock data
39
+ const mockUsers: UserType[] = [
40
+ { id: 1, name: 'Olivia Martin', email: 'olivia@example.com', status: 'active', created_at: '2024-01-15' },
41
+ { id: 2, name: 'Jackson Lee', email: 'jackson@example.com', status: 'active', created_at: '2024-02-20' },
42
+ { id: 3, name: 'Isabella Nguyen', email: 'isabella@example.com', status: 'active', created_at: '2024-02-28' },
43
+ { id: 4, name: 'William Kim', email: 'william@example.com', status: 'active', created_at: '2024-03-05' },
44
+ { id: 5, name: 'Sofia Davis', email: 'sofia@example.com', status: 'inactive', created_at: '2024-03-12' },
45
+ { id: 6, name: 'Liam Johnson', email: 'liam@example.com', status: 'active', created_at: '2024-03-20' },
46
+ { id: 7, name: 'Emma Wilson', email: 'emma@example.com', status: 'active', created_at: '2024-04-01' },
47
+ { id: 8, name: 'Noah Brown', email: 'noah@example.com', status: 'inactive', created_at: '2024-04-10' },
48
+ { id: 9, name: 'Ava Garcia', email: 'ava@example.com', status: 'active', created_at: '2024-04-18' },
49
+ { id: 10, name: 'Ethan Martinez', email: 'ethan@example.com', status: 'active', created_at: '2024-05-02' },
50
+ ]
51
+
52
+ const getInitialUsers = (): UserType[] => {
53
+ if (initialUsers && initialUsers.length > 0) {
54
+ return initialUsers.map(u => ({ ...u, status: (u as any).status ?? 'active', created_at: u.created_at ?? '' }))
55
+ }
56
+ return mockUsers
57
+ }
58
+
59
+ let users = $state<UserType[]>(getInitialUsers())
60
+ let loading = $state(false)
61
+ let search = $state('')
62
+ let debouncedSearch = $state('')
63
+ let page = $state(1)
64
+ let perPage = $state(10)
65
+ let total = $state(0)
66
+
67
+ // Action menus
68
+ let openMenuId = $state<number | null>(null)
69
+
70
+ // Debounce search
71
+ let debounceTimer: ReturnType<typeof setTimeout>
72
+ $effect(() => {
73
+ clearTimeout(debounceTimer)
74
+ debounceTimer = setTimeout(() => {
75
+ debouncedSearch = search
76
+ page = 1
77
+ }, 300)
78
+ })
79
+
80
+ // Fetch from API
81
+ async function fetchUsers() {
82
+ loading = true
83
+ const params = new URLSearchParams({
84
+ page: String(page),
85
+ per_page: String(perPage),
86
+ sort: 'created_at',
87
+ dir: 'desc',
88
+ })
89
+ if (debouncedSearch) params.set('search', debouncedSearch)
90
+
91
+ const { ok, data } = await api(`/api/users?${params}`)
92
+ if (ok && data) {
93
+ users = (data.data as any[]).map((u: any) => ({
94
+ id: u.id,
95
+ name: u.name,
96
+ email: u.email,
97
+ status: u.status ?? 'active',
98
+ created_at: u.created_at ?? '',
99
+ }))
100
+ if (data.meta) {
101
+ total = data.meta.total
102
+ }
103
+ }
104
+ loading = false
105
+ }
106
+
107
+ $effect(() => {
108
+ void page, perPage, debouncedSearch
109
+ fetchUsers()
110
+ })
111
+
112
+ function getInitials(name: string): string {
113
+ return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
114
+ }
115
+
116
+ const totalPages = $derived(Math.max(1, Math.ceil(total / perPage)))
117
+
118
+ function handleClickOutside(e: MouseEvent) {
119
+ const target = e.target as HTMLElement
120
+ if (!target.closest('[data-action-menu]')) {
121
+ openMenuId = null
122
+ }
123
+ }
124
+
125
+ $effect(() => {
126
+ if (openMenuId !== null) {
127
+ document.addEventListener('click', handleClickOutside, true)
128
+ return () => document.removeEventListener('click', handleClickOutside, true)
129
+ }
130
+ })
131
+ </script>
132
+
133
+ <AuthenticatedLayout
134
+ {currentUser}
135
+ {appName}
136
+ {navigate}
137
+ activePath="/users"
138
+ >
139
+ <Header fixed {navigate} />
140
+
141
+ <Main>
142
+ <div class="space-y-4">
143
+ <!-- Title row -->
144
+ <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
145
+ <div>
146
+ <h2 class="text-2xl font-bold tracking-tight">User List</h2>
147
+ <p class="text-sm text-muted-foreground">
148
+ {total > 0 ? `${total} users total.` : 'Manage your users here.'}
149
+ </p>
150
+ </div>
151
+ <div class="flex items-center gap-2">
152
+ <button
153
+ type="button"
154
+ disabled
155
+ class="inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-3 py-1.5 text-sm font-medium hover:bg-accent disabled:pointer-events-none disabled:opacity-50"
156
+ >
157
+ <Mail class="h-4 w-4" />
158
+ Invite User
159
+ </button>
160
+ <button
161
+ type="button"
162
+ class="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
163
+ >
164
+ <UserPlus class="h-4 w-4" />
165
+ Add User
166
+ </button>
167
+ </div>
168
+ </div>
169
+
170
+ <!-- Search -->
171
+ <div class="relative max-w-sm">
172
+ <Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
173
+ <input
174
+ type="text"
175
+ placeholder="Filter users..."
176
+ bind:value={search}
177
+ class="h-9 w-full rounded-md border border-input bg-background pl-9 pr-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
178
+ />
179
+ </div>
180
+
181
+ <!-- Table -->
182
+ <div class="rounded-lg border border-border">
183
+ <table class="w-full text-sm">
184
+ <thead>
185
+ <tr class="border-b border-border bg-muted/50">
186
+ <th class="px-4 py-3 text-left font-medium text-muted-foreground">User</th>
187
+ <th class="hidden px-4 py-3 text-left font-medium text-muted-foreground sm:table-cell">Status</th>
188
+ <th class="hidden px-4 py-3 text-left font-medium text-muted-foreground lg:table-cell">Created</th>
189
+ <th class="w-10 px-4 py-3"></th>
190
+ </tr>
191
+ </thead>
192
+ <tbody>
193
+ {#if loading}
194
+ <tr>
195
+ <td colspan="4" class="px-4 py-8 text-center text-muted-foreground">Loading...</td>
196
+ </tr>
197
+ {:else if users.length === 0}
198
+ <tr>
199
+ <td colspan="4" class="px-4 py-8 text-center text-muted-foreground">
200
+ {search ? 'No users match the current filters.' : 'No users found.'}
201
+ </td>
202
+ </tr>
203
+ {:else}
204
+ {#each users as user}
205
+ <tr class="border-b border-border last:border-0 hover:bg-muted/50 transition-colors">
206
+ <td class="px-4 py-3">
207
+ <div class="flex items-center gap-3">
208
+ <div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold">
209
+ {getInitials(user.name)}
210
+ </div>
211
+ <div class="min-w-0">
212
+ <div class="truncate text-sm font-medium">{user.name}</div>
213
+ <div class="truncate text-xs text-muted-foreground">{user.email}</div>
214
+ </div>
215
+ </div>
216
+ </td>
217
+ <td class="hidden px-4 py-3 sm:table-cell">
218
+ <span class="inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium capitalize {user.status === 'active' ? 'bg-primary text-primary-foreground' : 'border border-border text-muted-foreground'}">
219
+ {user.status}
220
+ </span>
221
+ </td>
222
+ <td class="hidden px-4 py-3 text-sm text-muted-foreground lg:table-cell">
223
+ {user.created_at
224
+ ? new Date(user.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
225
+ : '\u2014'}
226
+ </td>
227
+ <td class="px-4 py-3">
228
+ <div class="relative" data-action-menu>
229
+ <button
230
+ type="button"
231
+ onclick={() => openMenuId = openMenuId === user.id ? null : user.id}
232
+ class="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent"
233
+ >
234
+ <MoreHorizontal class="h-4 w-4" />
235
+ <span class="sr-only">Open menu</span>
236
+ </button>
237
+ {#if openMenuId === user.id}
238
+ <div class="absolute right-0 top-full z-50 mt-1 w-40 rounded-md border border-border bg-popover p-1 shadow-lg">
239
+ <button
240
+ type="button"
241
+ class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
242
+ onclick={() => openMenuId = null}
243
+ >
244
+ <Pencil class="h-4 w-4" />
245
+ Edit
246
+ </button>
247
+ <div class="my-1 h-px bg-border"></div>
248
+ <button
249
+ type="button"
250
+ class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-destructive hover:bg-accent transition-colors"
251
+ onclick={() => openMenuId = null}
252
+ >
253
+ <Trash2 class="h-4 w-4" />
254
+ Delete
255
+ </button>
256
+ </div>
257
+ {/if}
258
+ </div>
259
+ </td>
260
+ </tr>
261
+ {/each}
262
+ {/if}
263
+ </tbody>
264
+ </table>
265
+ </div>
266
+
267
+ <!-- Pagination -->
268
+ {#if total > perPage}
269
+ <div class="flex items-center justify-end gap-2">
270
+ <button
271
+ type="button"
272
+ disabled={page <= 1}
273
+ onclick={() => page = Math.max(1, page - 1)}
274
+ class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-input bg-background hover:bg-accent disabled:pointer-events-none disabled:opacity-50"
275
+ >
276
+ <ChevronLeft class="h-4 w-4" />
277
+ </button>
278
+ <span class="text-sm text-muted-foreground">
279
+ Page {page} of {totalPages}
280
+ </span>
281
+ <button
282
+ type="button"
283
+ disabled={page >= totalPages}
284
+ onclick={() => page = Math.min(totalPages, page + 1)}
285
+ class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-input bg-background hover:bg-accent disabled:pointer-events-none disabled:opacity-50"
286
+ >
287
+ <ChevronRight class="h-4 w-4" />
288
+ </button>
289
+ </div>
290
+ {/if}
291
+ </div>
292
+ </Main>
293
+ </AuthenticatedLayout>
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ import AuthenticatedLayout from '$lib/components/layout/authenticated-layout.svelte'
3
+ import Header from '$lib/components/layout/header.svelte'
4
+ import Main from '$lib/components/layout/main.svelte'
5
+ import { User, Lock, Palette } from 'lucide-svelte'
6
+ import type { Component, Snippet } from 'svelte'
7
+
8
+ let {
9
+ children,
10
+ appName,
11
+ currentUser,
12
+ navigate,
13
+ activePath,
14
+ }: {
15
+ children: Snippet
16
+ appName?: string
17
+ currentUser?: any
18
+ navigate: (href: string) => void
19
+ activePath: string
20
+ } = $props()
21
+
22
+ const sidebarNav: { title: string; href: string; icon: Component }[] = [
23
+ { title: 'Profile', href: '/account/profile', icon: User },
24
+ { title: 'Security', href: '/account/security', icon: Lock },
25
+ { title: 'Preferences', href: '/account/preferences', icon: Palette },
26
+ ]
27
+ </script>
28
+
29
+ <AuthenticatedLayout {currentUser} {appName} {navigate} {activePath}>
30
+ <Header fixed {navigate}>
31
+ <h1 class="text-lg font-semibold">Settings</h1>
32
+ </Header>
33
+ <Main>
34
+ <div class="space-y-0.5">
35
+ <h2 class="text-2xl font-bold tracking-tight">Settings</h2>
36
+ <p class="text-muted-foreground">
37
+ Manage your account settings and preferences.
38
+ </p>
39
+ </div>
40
+ <div class="my-6 h-px bg-border"></div>
41
+ <div class="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
42
+ <aside class="lg:w-48">
43
+ <nav class="flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1 overflow-x-auto">
44
+ {#each sidebarNav as item}
45
+ {@const Icon = item.icon}
46
+ <a
47
+ href={item.href}
48
+ class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground {activePath === item.href ? 'bg-accent text-accent-foreground' : 'text-muted-foreground'}"
49
+ >
50
+ <Icon class="w-4 h-4" />
51
+ {item.title}
52
+ </a>
53
+ {/each}
54
+ </nav>
55
+ </aside>
56
+ <div class="flex-1 lg:max-w-2xl">
57
+ {@render children()}
58
+ </div>
59
+ </div>
60
+ </Main>
61
+ </AuthenticatedLayout>
@@ -0,0 +1,81 @@
1
+ <script lang="ts">
2
+ import AccountLayout from './Layout.svelte'
3
+
4
+ let {
5
+ appName,
6
+ currentUser = null,
7
+ navigate,
8
+ }: {
9
+ appName?: string
10
+ currentUser?: { id: number; name: string; email: string } | null
11
+ navigate: (href: string) => void
12
+ [key: string]: any
13
+ } = $props()
14
+
15
+ let theme = $state<'light' | 'dark' | 'system'>(
16
+ typeof localStorage !== 'undefined'
17
+ ? (localStorage.getItem('theme') as any) ?? 'system'
18
+ : 'system'
19
+ )
20
+
21
+ function applyTheme(t: 'light' | 'dark' | 'system') {
22
+ theme = t
23
+ if (t === 'system') {
24
+ localStorage.removeItem('theme')
25
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
26
+ document.documentElement.classList.toggle('dark', prefersDark)
27
+ } else {
28
+ localStorage.setItem('theme', t)
29
+ document.documentElement.classList.toggle('dark', t === 'dark')
30
+ }
31
+ }
32
+
33
+ const themes: { value: 'light' | 'dark' | 'system'; label: string; description: string }[] = [
34
+ { value: 'light', label: 'Light', description: 'A clean, bright appearance.' },
35
+ { value: 'dark', label: 'Dark', description: 'Easy on the eyes in low light.' },
36
+ { value: 'system', label: 'System', description: 'Follows your operating system setting.' },
37
+ ]
38
+ </script>
39
+
40
+ <AccountLayout {appName} {currentUser} {navigate} activePath="/account/preferences">
41
+ <div class="space-y-8">
42
+ <div>
43
+ <h3 class="text-lg font-medium">Preferences</h3>
44
+ <p class="text-sm text-muted-foreground">
45
+ Customize the appearance and behavior of the app.
46
+ </p>
47
+ </div>
48
+
49
+ <!-- Theme -->
50
+ <div class="space-y-3">
51
+ <label class="text-sm font-medium leading-none">Theme</label>
52
+ <p class="text-sm text-muted-foreground">Select your preferred theme.</p>
53
+ <div class="grid grid-cols-3 gap-3">
54
+ {#each themes as t}
55
+ <button
56
+ type="button"
57
+ onclick={() => applyTheme(t.value)}
58
+ class="rounded-lg border p-4 text-left text-sm transition-colors hover:bg-accent {theme === t.value ? 'border-foreground' : 'border-border'}"
59
+ >
60
+ <div class="font-medium">{t.label}</div>
61
+ <div class="text-xs text-muted-foreground mt-1">{t.description}</div>
62
+ </button>
63
+ {/each}
64
+ </div>
65
+ </div>
66
+
67
+ <!-- Language -->
68
+ <div class="space-y-3">
69
+ <label class="text-sm font-medium leading-none">Language</label>
70
+ <p class="text-sm text-muted-foreground">Select the language for the interface.</p>
71
+ <button
72
+ type="button"
73
+ disabled
74
+ class="inline-flex items-center justify-between w-48 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium hover:bg-accent disabled:pointer-events-none disabled:opacity-50"
75
+ >
76
+ English
77
+ <span class="text-xs text-muted-foreground">Default</span>
78
+ </button>
79
+ </div>
80
+ </div>
81
+ </AccountLayout>
@@ -0,0 +1,76 @@
1
+ <script lang="ts">
2
+ import AccountLayout from './Layout.svelte'
3
+
4
+ let {
5
+ appName,
6
+ currentUser = null,
7
+ navigate,
8
+ }: {
9
+ appName?: string
10
+ currentUser?: { id: number; name: string; email: string } | null
11
+ navigate: (href: string) => void
12
+ [key: string]: any
13
+ } = $props()
14
+
15
+ let name = $state((() => currentUser?.name ?? '')())
16
+ let email = $state((() => currentUser?.email ?? '')())
17
+ let saving = $state(false)
18
+ let saved = $state(false)
19
+
20
+ async function handleSave(e: SubmitEvent) {
21
+ e.preventDefault()
22
+ saving = true
23
+ await new Promise(r => setTimeout(r, 500))
24
+ saving = false
25
+ saved = true
26
+ setTimeout(() => saved = false, 3000)
27
+ }
28
+ </script>
29
+
30
+ <AccountLayout {appName} {currentUser} {navigate} activePath="/account/profile">
31
+ <div class="space-y-6">
32
+ <div>
33
+ <h3 class="text-lg font-medium">Profile</h3>
34
+ <p class="text-sm text-muted-foreground">
35
+ This is how others will see you on the site.
36
+ </p>
37
+ </div>
38
+ <form onsubmit={handleSave} class="space-y-6">
39
+ <div class="space-y-2">
40
+ <label for="name" class="text-sm font-medium leading-none">Name</label>
41
+ <input
42
+ id="name"
43
+ bind:value={name}
44
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
45
+ />
46
+ <p class="text-xs text-muted-foreground">
47
+ This is your public display name.
48
+ </p>
49
+ </div>
50
+ <div class="space-y-2">
51
+ <label for="email" class="text-sm font-medium leading-none">Email</label>
52
+ <input
53
+ id="email"
54
+ type="email"
55
+ bind:value={email}
56
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
57
+ />
58
+ <p class="text-xs text-muted-foreground">
59
+ Your email address is used for notifications.
60
+ </p>
61
+ </div>
62
+ <div class="flex items-center gap-3">
63
+ <button
64
+ type="submit"
65
+ disabled={saving}
66
+ class="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50"
67
+ >
68
+ {saving ? 'Saving...' : 'Update profile'}
69
+ </button>
70
+ {#if saved}
71
+ <span class="text-sm text-muted-foreground">Saved.</span>
72
+ {/if}
73
+ </div>
74
+ </form>
75
+ </div>
76
+ </AccountLayout>
@@ -0,0 +1,140 @@
1
+ <script lang="ts">
2
+ import AccountLayout from './Layout.svelte'
3
+
4
+ let {
5
+ appName,
6
+ currentUser = null,
7
+ navigate,
8
+ }: {
9
+ appName?: string
10
+ currentUser?: { id: number; name: string; email: string } | null
11
+ navigate: (href: string) => void
12
+ [key: string]: any
13
+ } = $props()
14
+
15
+ let currentPassword = $state('')
16
+ let newPassword = $state('')
17
+ let confirmPassword = $state('')
18
+ let saving = $state(false)
19
+ let saved = $state(false)
20
+ let error = $state('')
21
+
22
+ async function handleSave(e: SubmitEvent) {
23
+ e.preventDefault()
24
+ error = ''
25
+ if (newPassword.length < 8) { error = 'Password must be at least 8 characters.'; return }
26
+ if (newPassword !== confirmPassword) { error = 'Passwords do not match.'; return }
27
+ saving = true
28
+ await new Promise(r => setTimeout(r, 500))
29
+ saving = false
30
+ saved = true
31
+ currentPassword = ''
32
+ newPassword = ''
33
+ confirmPassword = ''
34
+ setTimeout(() => saved = false, 3000)
35
+ }
36
+ </script>
37
+
38
+ <AccountLayout {appName} {currentUser} {navigate} activePath="/account/security">
39
+ <div class="space-y-8">
40
+ <div>
41
+ <h3 class="text-lg font-medium">Security</h3>
42
+ <p class="text-sm text-muted-foreground">
43
+ Manage your password and security settings.
44
+ </p>
45
+ </div>
46
+
47
+ <!-- Change Password -->
48
+ <form onsubmit={handleSave} class="space-y-4">
49
+ <div class="space-y-2">
50
+ <label for="current" class="text-sm font-medium leading-none">Current password</label>
51
+ <input
52
+ id="current"
53
+ type="password"
54
+ bind:value={currentPassword}
55
+ autocomplete="current-password"
56
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
57
+ />
58
+ </div>
59
+ <div class="space-y-2">
60
+ <label for="new" class="text-sm font-medium leading-none">New password</label>
61
+ <input
62
+ id="new"
63
+ type="password"
64
+ bind:value={newPassword}
65
+ autocomplete="new-password"
66
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
67
+ />
68
+ </div>
69
+ <div class="space-y-2">
70
+ <label for="confirm" class="text-sm font-medium leading-none">Confirm password</label>
71
+ <input
72
+ id="confirm"
73
+ type="password"
74
+ bind:value={confirmPassword}
75
+ autocomplete="new-password"
76
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
77
+ />
78
+ </div>
79
+ {#if error}
80
+ <p class="text-sm text-destructive">{error}</p>
81
+ {/if}
82
+ <div class="flex items-center gap-3">
83
+ <button
84
+ type="submit"
85
+ disabled={saving}
86
+ class="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50"
87
+ >
88
+ {saving ? 'Saving...' : 'Update password'}
89
+ </button>
90
+ {#if saved}
91
+ <span class="text-sm text-muted-foreground">Password updated.</span>
92
+ {/if}
93
+ </div>
94
+ </form>
95
+
96
+ <!-- 2FA -->
97
+ <div class="space-y-3">
98
+ <div>
99
+ <h4 class="text-sm font-medium">Two-factor authentication</h4>
100
+ <p class="text-sm text-muted-foreground">
101
+ Add an additional layer of security to your account.
102
+ </p>
103
+ </div>
104
+ <div class="rounded-xl border border-border bg-card text-card-foreground shadow-sm">
105
+ <div class="p-6 pb-3">
106
+ <h3 class="text-sm font-semibold leading-none tracking-tight">Authenticator app</h3>
107
+ <p class="text-sm text-muted-foreground mt-1.5">
108
+ Use an authenticator app to generate one-time codes.
109
+ </p>
110
+ </div>
111
+ <div class="p-6 pt-0">
112
+ <button
113
+ type="button"
114
+ disabled
115
+ class="inline-flex items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium hover:bg-accent disabled:pointer-events-none disabled:opacity-50"
116
+ >
117
+ Enable 2FA
118
+ </button>
119
+ </div>
120
+ </div>
121
+ </div>
122
+
123
+ <!-- Delete Account -->
124
+ <div class="space-y-3">
125
+ <div>
126
+ <h4 class="text-sm font-medium">Delete account</h4>
127
+ <p class="text-sm text-muted-foreground">
128
+ Permanently remove your account and all associated data. This action cannot be undone.
129
+ </p>
130
+ </div>
131
+ <button
132
+ type="button"
133
+ disabled
134
+ class="inline-flex items-center justify-center rounded-md bg-destructive px-4 py-2 text-sm font-medium text-destructive-foreground shadow hover:bg-destructive/90 disabled:pointer-events-none disabled:opacity-50"
135
+ >
136
+ Delete account
137
+ </button>
138
+ </div>
139
+ </div>
140
+ </AccountLayout>