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,401 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted } from 'vue'
3
+ import { api, post, del } from '@/lib/api'
4
+ import { AuthenticatedLayout, Header, Main } from '@/components/layout'
5
+ import { Plus, Search, MoreHorizontal, ArrowUpDown, ChevronLeft, ChevronRight, X } from 'lucide-vue-next'
6
+
7
+ const props = withDefaults(defineProps<{
8
+ appName?: string
9
+ currentUser?: { id: number; name: string; email: string } | null
10
+ navigate: (href: string) => void
11
+ }>(), {
12
+ appName: 'Mantiq',
13
+ })
14
+
15
+ interface UserRecord {
16
+ id: number
17
+ name: string
18
+ email: string
19
+ created_at?: string
20
+ }
21
+
22
+ const users = ref<UserRecord[]>([])
23
+ const loading = ref(false)
24
+ const search = ref('')
25
+ const page = ref(1)
26
+ const perPage = ref(10)
27
+ const sortBy = ref('created_at')
28
+ const sortDir = ref<'asc' | 'desc'>('desc')
29
+ const meta = ref({ total: 0, filtered_total: 0, page: 1, per_page: 10, last_page: 1 })
30
+
31
+ // Dialogs
32
+ const addOpen = ref(false)
33
+ const editOpen = ref(false)
34
+ const deleteOpen = ref(false)
35
+ const selectedUser = ref<UserRecord | null>(null)
36
+
37
+ // Action dropdown
38
+ const actionMenuId = ref<number | null>(null)
39
+
40
+ // Form state
41
+ const formName = ref('')
42
+ const formEmail = ref('')
43
+ const formPassword = ref('')
44
+ const formError = ref('')
45
+ const formLoading = ref(false)
46
+
47
+ async function fetchUsers() {
48
+ loading.value = true
49
+ const params = new URLSearchParams({
50
+ search: search.value,
51
+ page: String(page.value),
52
+ per_page: String(perPage.value),
53
+ sort: sortBy.value,
54
+ dir: sortDir.value,
55
+ })
56
+ const { ok, data } = await api(`/api/users?${params}`)
57
+ if (ok) {
58
+ users.value = data.data ?? []
59
+ meta.value = data.meta ?? meta.value
60
+ }
61
+ loading.value = false
62
+ }
63
+
64
+ function toggleSort(col: string) {
65
+ if (sortBy.value === col) {
66
+ sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc'
67
+ } else {
68
+ sortBy.value = col
69
+ sortDir.value = 'asc'
70
+ }
71
+ page.value = 1
72
+ fetchUsers()
73
+ }
74
+
75
+ let searchTimeout: ReturnType<typeof setTimeout> | null = null
76
+ function onSearchInput() {
77
+ if (searchTimeout) clearTimeout(searchTimeout)
78
+ searchTimeout = setTimeout(() => {
79
+ page.value = 1
80
+ fetchUsers()
81
+ }, 300)
82
+ }
83
+
84
+ function openAdd() {
85
+ formName.value = ''
86
+ formEmail.value = ''
87
+ formPassword.value = ''
88
+ formError.value = ''
89
+ addOpen.value = true
90
+ }
91
+
92
+ function openEdit(user: UserRecord) {
93
+ selectedUser.value = user
94
+ formName.value = user.name
95
+ formEmail.value = user.email
96
+ formPassword.value = ''
97
+ formError.value = ''
98
+ editOpen.value = true
99
+ }
100
+
101
+ function openDelete(user: UserRecord) {
102
+ selectedUser.value = user
103
+ formError.value = ''
104
+ deleteOpen.value = true
105
+ }
106
+
107
+ async function handleAdd() {
108
+ formError.value = ''
109
+ formLoading.value = true
110
+ const { ok, data } = await post('/api/users', {
111
+ name: formName.value,
112
+ email: formEmail.value,
113
+ password: formPassword.value,
114
+ })
115
+ formLoading.value = false
116
+ if (ok) {
117
+ addOpen.value = false
118
+ fetchUsers()
119
+ } else {
120
+ formError.value = data?.error ?? 'Failed to create user.'
121
+ }
122
+ }
123
+
124
+ async function handleEdit() {
125
+ if (!selectedUser.value) return
126
+ formError.value = ''
127
+ formLoading.value = true
128
+ const body: Record<string, string> = { name: formName.value, email: formEmail.value }
129
+ if (formPassword.value) body.password = formPassword.value
130
+ const { ok, data } = await api(`/api/users/${selectedUser.value.id}`, {
131
+ method: 'PUT',
132
+ headers: { 'Content-Type': 'application/json' },
133
+ body: JSON.stringify(body),
134
+ })
135
+ formLoading.value = false
136
+ if (ok) {
137
+ editOpen.value = false
138
+ fetchUsers()
139
+ } else {
140
+ formError.value = data?.error ?? 'Failed to update user.'
141
+ }
142
+ }
143
+
144
+ async function handleDelete() {
145
+ if (!selectedUser.value) return
146
+ formError.value = ''
147
+ formLoading.value = true
148
+ const { ok, data } = await del(`/api/users/${selectedUser.value.id}`)
149
+ formLoading.value = false
150
+ if (ok) {
151
+ deleteOpen.value = false
152
+ fetchUsers()
153
+ } else {
154
+ formError.value = data?.error ?? 'Failed to delete user.'
155
+ }
156
+ }
157
+
158
+ onMounted(fetchUsers)
159
+ </script>
160
+
161
+ <template>
162
+ <AuthenticatedLayout
163
+ :current-user="currentUser"
164
+ :app-name="appName"
165
+ :navigate="navigate"
166
+ active-path="/users"
167
+ >
168
+ <Header :navigate="navigate" />
169
+ <Main>
170
+ <div class="space-y-4">
171
+ <div class="flex items-center justify-between">
172
+ <h2 class="text-3xl font-bold tracking-tight">Users</h2>
173
+ <button
174
+ class="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90"
175
+ @click="openAdd"
176
+ >
177
+ <Plus class="h-4 w-4" />
178
+ Add User
179
+ </button>
180
+ </div>
181
+
182
+ <!-- Search -->
183
+ <div class="flex items-center gap-2">
184
+ <div class="relative max-w-sm flex-1">
185
+ <Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
186
+ <input
187
+ v-model="search"
188
+ placeholder="Search users..."
189
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 pl-9 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
190
+ @input="onSearchInput"
191
+ />
192
+ </div>
193
+ <span class="text-sm text-muted-foreground">
194
+ {{ loading ? 'Loading...' : `${meta.filtered_total} of ${meta.total} users` }}
195
+ </span>
196
+ </div>
197
+
198
+ <!-- Table -->
199
+ <div class="w-full overflow-auto rounded-md border">
200
+ <table class="w-full caption-bottom text-sm">
201
+ <thead class="border-b bg-muted/50">
202
+ <tr>
203
+ <th
204
+ class="h-10 px-4 text-left align-middle font-medium text-muted-foreground cursor-pointer select-none"
205
+ @click="toggleSort('name')"
206
+ >
207
+ <span class="flex items-center gap-1">
208
+ Name
209
+ <ArrowUpDown class="h-3 w-3" />
210
+ </span>
211
+ </th>
212
+ <th
213
+ class="h-10 px-4 text-left align-middle font-medium text-muted-foreground cursor-pointer select-none"
214
+ @click="toggleSort('email')"
215
+ >
216
+ <span class="flex items-center gap-1">
217
+ Email
218
+ <ArrowUpDown class="h-3 w-3" />
219
+ </span>
220
+ </th>
221
+ <th class="h-10 w-10 px-4 text-left align-middle font-medium text-muted-foreground" />
222
+ </tr>
223
+ </thead>
224
+ <tbody>
225
+ <tr
226
+ v-for="user in users"
227
+ :key="user.id"
228
+ class="border-b transition-colors hover:bg-muted/50"
229
+ >
230
+ <td class="p-4 align-middle font-medium">{{ user.name }}</td>
231
+ <td class="p-4 align-middle">{{ user.email }}</td>
232
+ <td class="p-4 align-middle">
233
+ <div class="relative">
234
+ <button
235
+ class="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
236
+ @click.stop="actionMenuId = actionMenuId === user.id ? null : user.id"
237
+ >
238
+ <MoreHorizontal class="h-4 w-4" />
239
+ </button>
240
+ <div
241
+ v-if="actionMenuId === user.id"
242
+ class="absolute right-0 top-full z-50 mt-1 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
243
+ >
244
+ <button
245
+ class="flex w-full items-center rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground"
246
+ @click="actionMenuId = null; openEdit(user)"
247
+ >
248
+ Edit
249
+ </button>
250
+ <button
251
+ class="flex w-full items-center rounded-sm px-2 py-1.5 text-sm text-destructive hover:bg-destructive/10"
252
+ @click="actionMenuId = null; openDelete(user)"
253
+ >
254
+ Delete
255
+ </button>
256
+ </div>
257
+ </div>
258
+ </td>
259
+ </tr>
260
+ <tr v-if="users.length === 0 && !loading">
261
+ <td colspan="3" class="h-24 text-center text-muted-foreground">
262
+ No users found.
263
+ </td>
264
+ </tr>
265
+ </tbody>
266
+ </table>
267
+ </div>
268
+
269
+ <!-- Pagination -->
270
+ <div class="flex items-center justify-between">
271
+ <p class="text-sm text-muted-foreground">
272
+ Page {{ meta.page }} of {{ meta.last_page }}
273
+ </p>
274
+ <div class="flex items-center gap-2">
275
+ <button
276
+ class="inline-flex items-center gap-1 rounded-md border border-input bg-background px-3 py-1.5 text-sm font-medium shadow-sm hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
277
+ :disabled="meta.page <= 1"
278
+ @click="page--; fetchUsers()"
279
+ >
280
+ <ChevronLeft class="h-4 w-4" />
281
+ Previous
282
+ </button>
283
+ <button
284
+ class="inline-flex items-center gap-1 rounded-md border border-input bg-background px-3 py-1.5 text-sm font-medium shadow-sm hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
285
+ :disabled="meta.page >= meta.last_page"
286
+ @click="page++; fetchUsers()"
287
+ >
288
+ Next
289
+ <ChevronRight class="h-4 w-4" />
290
+ </button>
291
+ </div>
292
+ </div>
293
+ </div>
294
+ </Main>
295
+
296
+ <!-- Add User Dialog -->
297
+ <Teleport to="body">
298
+ <div v-if="addOpen" class="fixed inset-0 z-50 flex items-center justify-center">
299
+ <div class="fixed inset-0 bg-black/50" @click="addOpen = false" />
300
+ <div class="relative z-50 w-full max-w-[425px] rounded-lg border bg-background p-6 shadow-lg" @click.stop>
301
+ <button class="absolute right-4 top-4 text-muted-foreground hover:text-foreground" @click="addOpen = false">
302
+ <X class="h-4 w-4" />
303
+ </button>
304
+ <div class="mb-4">
305
+ <h2 class="text-lg font-semibold leading-none tracking-tight">Add User</h2>
306
+ <p class="mt-1.5 text-sm text-muted-foreground">Create a new user account.</p>
307
+ </div>
308
+ <form class="space-y-4" @submit.prevent="handleAdd">
309
+ <div v-if="formError" class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
310
+ {{ formError }}
311
+ </div>
312
+ <div class="space-y-2">
313
+ <label for="add-name" class="text-sm font-medium leading-none">Name</label>
314
+ <input id="add-name" v-model="formName" required placeholder="Full name" 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-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
315
+ </div>
316
+ <div class="space-y-2">
317
+ <label for="add-email" class="text-sm font-medium leading-none">Email</label>
318
+ <input id="add-email" v-model="formEmail" type="email" required placeholder="user@example.com" 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-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
319
+ </div>
320
+ <div class="space-y-2">
321
+ <label for="add-password" class="text-sm font-medium leading-none">Password</label>
322
+ <input id="add-password" v-model="formPassword" type="password" required placeholder="Min 6 characters" 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-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
323
+ </div>
324
+ <div class="flex justify-end gap-2">
325
+ <button type="button" class="rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm hover:bg-accent hover:text-accent-foreground" @click="addOpen = false">Cancel</button>
326
+ <button type="submit" :disabled="formLoading" class="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">
327
+ {{ formLoading ? 'Creating...' : 'Create User' }}
328
+ </button>
329
+ </div>
330
+ </form>
331
+ </div>
332
+ </div>
333
+ </Teleport>
334
+
335
+ <!-- Edit User Dialog -->
336
+ <Teleport to="body">
337
+ <div v-if="editOpen" class="fixed inset-0 z-50 flex items-center justify-center">
338
+ <div class="fixed inset-0 bg-black/50" @click="editOpen = false" />
339
+ <div class="relative z-50 w-full max-w-[425px] rounded-lg border bg-background p-6 shadow-lg" @click.stop>
340
+ <button class="absolute right-4 top-4 text-muted-foreground hover:text-foreground" @click="editOpen = false">
341
+ <X class="h-4 w-4" />
342
+ </button>
343
+ <div class="mb-4">
344
+ <h2 class="text-lg font-semibold leading-none tracking-tight">Edit User</h2>
345
+ <p class="mt-1.5 text-sm text-muted-foreground">Update user information.</p>
346
+ </div>
347
+ <form class="space-y-4" @submit.prevent="handleEdit">
348
+ <div v-if="formError" class="rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
349
+ {{ formError }}
350
+ </div>
351
+ <div class="space-y-2">
352
+ <label for="edit-name" class="text-sm font-medium leading-none">Name</label>
353
+ <input id="edit-name" v-model="formName" required placeholder="Full name" 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-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
354
+ </div>
355
+ <div class="space-y-2">
356
+ <label for="edit-email" class="text-sm font-medium leading-none">Email</label>
357
+ <input id="edit-email" v-model="formEmail" type="email" required placeholder="user@example.com" 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-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
358
+ </div>
359
+ <div class="space-y-2">
360
+ <label for="edit-password" class="text-sm font-medium leading-none">Password</label>
361
+ <input id="edit-password" v-model="formPassword" type="password" placeholder="Leave blank to keep current" 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-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
362
+ </div>
363
+ <div class="flex justify-end gap-2">
364
+ <button type="button" class="rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm hover:bg-accent hover:text-accent-foreground" @click="editOpen = false">Cancel</button>
365
+ <button type="submit" :disabled="formLoading" class="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">
366
+ {{ formLoading ? 'Saving...' : 'Save Changes' }}
367
+ </button>
368
+ </div>
369
+ </form>
370
+ </div>
371
+ </div>
372
+ </Teleport>
373
+
374
+ <!-- Delete User Dialog -->
375
+ <Teleport to="body">
376
+ <div v-if="deleteOpen" class="fixed inset-0 z-50 flex items-center justify-center">
377
+ <div class="fixed inset-0 bg-black/50" @click="deleteOpen = false" />
378
+ <div class="relative z-50 w-full max-w-[425px] rounded-lg border bg-background p-6 shadow-lg" @click.stop>
379
+ <button class="absolute right-4 top-4 text-muted-foreground hover:text-foreground" @click="deleteOpen = false">
380
+ <X class="h-4 w-4" />
381
+ </button>
382
+ <div class="mb-4">
383
+ <h2 class="text-lg font-semibold leading-none tracking-tight">Delete User</h2>
384
+ <p class="mt-1.5 text-sm text-muted-foreground">
385
+ Are you sure you want to delete <strong>{{ selectedUser?.name }}</strong>? This action cannot be undone.
386
+ </p>
387
+ </div>
388
+ <div v-if="formError" class="mb-4 rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
389
+ {{ formError }}
390
+ </div>
391
+ <div class="flex justify-end gap-2">
392
+ <button type="button" class="rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm hover:bg-accent hover:text-accent-foreground" @click="deleteOpen = false">Cancel</button>
393
+ <button :disabled="formLoading" class="rounded-md bg-destructive px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-destructive/90 disabled:pointer-events-none disabled:opacity-50" @click="handleDelete">
394
+ {{ formLoading ? 'Deleting...' : 'Delete User' }}
395
+ </button>
396
+ </div>
397
+ </div>
398
+ </div>
399
+ </Teleport>
400
+ </AuthenticatedLayout>
401
+ </template>
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ import { AuthenticatedLayout, Header, Main } from '@/components/layout'
3
+
4
+ const props = withDefaults(defineProps<{
5
+ appName?: string
6
+ currentUser?: { name: string; email: string; role?: string } | null
7
+ navigate: (href: string) => void
8
+ activePath: string
9
+ title: string
10
+ description: string
11
+ }>(), {
12
+ appName: 'Mantiq',
13
+ })
14
+
15
+ const tabs = [
16
+ { title: 'Profile', href: '/account/profile' },
17
+ { title: 'Security', href: '/account/security' },
18
+ { title: 'Preferences', href: '/account/preferences' },
19
+ ]
20
+ </script>
21
+
22
+ <template>
23
+ <AuthenticatedLayout
24
+ :current-user="currentUser"
25
+ :app-name="appName"
26
+ :navigate="navigate"
27
+ :active-path="activePath"
28
+ >
29
+ <Header :navigate="navigate" />
30
+ <Main>
31
+ <div class="space-y-6">
32
+ <div>
33
+ <h2 class="text-3xl font-bold tracking-tight">{{ title }}</h2>
34
+ <p class="text-muted-foreground">{{ description }}</p>
35
+ </div>
36
+
37
+ <!-- Tab nav -->
38
+ <nav class="flex gap-2 border-b">
39
+ <button
40
+ v-for="tab in tabs"
41
+ :key="tab.href"
42
+ class="relative px-4 py-2 text-sm font-medium transition-colors"
43
+ :class="activePath === tab.href
44
+ ? 'text-foreground after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-foreground'
45
+ : 'text-muted-foreground hover:text-foreground'"
46
+ @click="navigate(tab.href)"
47
+ >
48
+ {{ tab.title }}
49
+ </button>
50
+ </nav>
51
+
52
+ <slot />
53
+ </div>
54
+ </Main>
55
+ </AuthenticatedLayout>
56
+ </template>
@@ -0,0 +1,81 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted } from 'vue'
3
+ import AccountLayout from './layout.vue'
4
+
5
+ const props = withDefaults(defineProps<{
6
+ appName?: string
7
+ currentUser?: { name: string; email: string; role?: string } | null
8
+ navigate: (href: string) => void
9
+ }>(), {
10
+ appName: 'Mantiq',
11
+ })
12
+
13
+ const theme = ref<'light' | 'dark' | 'system'>('system')
14
+ const success = ref('')
15
+
16
+ onMounted(() => {
17
+ const stored = localStorage.getItem('theme')
18
+ if (stored === 'light' || stored === 'dark') {
19
+ theme.value = stored
20
+ } else {
21
+ theme.value = 'system'
22
+ }
23
+ })
24
+
25
+ function setTheme(value: 'light' | 'dark' | 'system') {
26
+ theme.value = value
27
+ if (value === 'system') {
28
+ localStorage.removeItem('theme')
29
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
30
+ document.documentElement.classList.toggle('dark', prefersDark)
31
+ } else {
32
+ localStorage.setItem('theme', value)
33
+ document.documentElement.classList.toggle('dark', value === 'dark')
34
+ }
35
+ success.value = 'Preferences saved.'
36
+ }
37
+ </script>
38
+
39
+ <template>
40
+ <AccountLayout
41
+ :app-name="appName"
42
+ :current-user="currentUser"
43
+ :navigate="navigate"
44
+ active-path="/account/preferences"
45
+ title="Settings"
46
+ description="Manage your account settings."
47
+ >
48
+ <div class="rounded-xl border bg-card text-card-foreground shadow-sm">
49
+ <div class="p-6 pb-2">
50
+ <h3 class="text-lg font-semibold leading-none tracking-tight">Preferences</h3>
51
+ <p class="mt-1.5 text-sm text-muted-foreground">Customize your experience.</p>
52
+ </div>
53
+ <div class="p-6 pt-4">
54
+ <div
55
+ v-if="success"
56
+ class="mb-4 rounded-md border border-green-500/30 bg-green-500/10 px-4 py-3 text-sm text-green-600 dark:text-green-400"
57
+ >
58
+ {{ success }}
59
+ </div>
60
+ <div class="space-y-4 max-w-md">
61
+ <div class="space-y-2">
62
+ <label class="text-sm font-medium leading-none">Theme</label>
63
+ <div class="flex gap-2">
64
+ <button
65
+ v-for="opt in (['light', 'dark', 'system'] as const)"
66
+ :key="opt"
67
+ class="inline-flex h-8 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors"
68
+ :class="theme === opt
69
+ ? 'bg-primary text-primary-foreground shadow'
70
+ : 'border border-input bg-background hover:bg-accent hover:text-accent-foreground'"
71
+ @click="setTheme(opt)"
72
+ >
73
+ {{ opt.charAt(0).toUpperCase() + opt.slice(1) }}
74
+ </button>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </AccountLayout>
81
+ </template>
@@ -0,0 +1,69 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import AccountLayout from './layout.vue'
4
+
5
+ const props = withDefaults(defineProps<{
6
+ appName?: string
7
+ currentUser?: { name: string; email: string; role?: string } | null
8
+ navigate: (href: string) => void
9
+ }>(), {
10
+ appName: 'Mantiq',
11
+ })
12
+
13
+ const name = ref(props.currentUser?.name ?? '')
14
+ const email = ref(props.currentUser?.email ?? '')
15
+ const success = ref('')
16
+ const error = ref('')
17
+
18
+ async function handleSubmit() {
19
+ success.value = ''
20
+ error.value = ''
21
+ // Profile update would call an API endpoint
22
+ success.value = 'Profile updated successfully.'
23
+ }
24
+ </script>
25
+
26
+ <template>
27
+ <AccountLayout
28
+ :app-name="appName"
29
+ :current-user="currentUser"
30
+ :navigate="navigate"
31
+ active-path="/account/profile"
32
+ title="Settings"
33
+ description="Manage your account settings."
34
+ >
35
+ <div class="rounded-xl border bg-card text-card-foreground shadow-sm">
36
+ <div class="p-6 pb-2">
37
+ <h3 class="text-lg font-semibold leading-none tracking-tight">Profile</h3>
38
+ <p class="mt-1.5 text-sm text-muted-foreground">Update your personal information.</p>
39
+ </div>
40
+ <div class="p-6 pt-4">
41
+ <div
42
+ v-if="success"
43
+ class="mb-4 rounded-md border border-green-500/30 bg-green-500/10 px-4 py-3 text-sm text-green-600 dark:text-green-400"
44
+ >
45
+ {{ success }}
46
+ </div>
47
+ <div
48
+ v-if="error"
49
+ class="mb-4 rounded-md border border-destructive px-4 py-3 text-sm text-destructive"
50
+ >
51
+ {{ error }}
52
+ </div>
53
+ <form class="space-y-4 max-w-md" @submit.prevent="handleSubmit">
54
+ <div class="space-y-2">
55
+ <label for="profile-name" class="text-sm font-medium leading-none">Name</label>
56
+ <input id="profile-name" v-model="name" required 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-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
57
+ </div>
58
+ <div class="space-y-2">
59
+ <label for="profile-email" class="text-sm font-medium leading-none">Email</label>
60
+ <input id="profile-email" v-model="email" type="email" required 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-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
61
+ </div>
62
+ <button type="submit" class="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90">
63
+ Save Changes
64
+ </button>
65
+ </form>
66
+ </div>
67
+ </div>
68
+ </AccountLayout>
69
+ </template>