@motor-cms/ui-admin 1.0.1-alpha.0

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 (103) hide show
  1. package/README.md +77 -0
  2. package/app/components/form/inputs/CategoryTreeInput.vue +154 -0
  3. package/app/components/form/inputs/CategoryTreePicker.vue +355 -0
  4. package/app/components/form/inputs/NestedDraggable.vue +217 -0
  5. package/app/components/form/inputs/QuicklinksInput.vue +186 -0
  6. package/app/lang/de/motor-admin/CLAUDE.md +21 -0
  7. package/app/lang/de/motor-admin/ai_system_prompts.json +12 -0
  8. package/app/lang/de/motor-admin/categories.json +12 -0
  9. package/app/lang/de/motor-admin/category_trees.json +14 -0
  10. package/app/lang/de/motor-admin/clients.json +26 -0
  11. package/app/lang/de/motor-admin/config_variables.json +14 -0
  12. package/app/lang/de/motor-admin/domains.json +19 -0
  13. package/app/lang/de/motor-admin/email_templates.json +38 -0
  14. package/app/lang/de/motor-admin/global.json +5 -0
  15. package/app/lang/de/motor-admin/languages.json +16 -0
  16. package/app/lang/de/motor-admin/permissions.json +14 -0
  17. package/app/lang/de/motor-admin/roles.json +15 -0
  18. package/app/lang/de/motor-admin/users.json +22 -0
  19. package/app/lang/en/motor-admin/CLAUDE.md +7 -0
  20. package/app/lang/en/motor-admin/ai_system_prompts.json +12 -0
  21. package/app/lang/en/motor-admin/categories.json +12 -0
  22. package/app/lang/en/motor-admin/category_trees.json +14 -0
  23. package/app/lang/en/motor-admin/clients.json +26 -0
  24. package/app/lang/en/motor-admin/config_variables.json +14 -0
  25. package/app/lang/en/motor-admin/domains.json +18 -0
  26. package/app/lang/en/motor-admin/email_templates.json +33 -0
  27. package/app/lang/en/motor-admin/global.json +5 -0
  28. package/app/lang/en/motor-admin/languages.json +16 -0
  29. package/app/lang/en/motor-admin/permissions.json +14 -0
  30. package/app/lang/en/motor-admin/roles.json +15 -0
  31. package/app/lang/en/motor-admin/users.json +22 -0
  32. package/app/pages/dashboard.vue +5 -0
  33. package/app/pages/index.vue +39 -0
  34. package/app/pages/login.vue +85 -0
  35. package/app/pages/motor-admin/ai-system-prompts/CLAUDE.md +7 -0
  36. package/app/pages/motor-admin/ai-system-prompts/[id]/edit.vue +48 -0
  37. package/app/pages/motor-admin/ai-system-prompts/create.vue +40 -0
  38. package/app/pages/motor-admin/ai-system-prompts/index.vue +68 -0
  39. package/app/pages/motor-admin/category-trees/CLAUDE.md +7 -0
  40. package/app/pages/motor-admin/category-trees/[id]/CLAUDE.md +7 -0
  41. package/app/pages/motor-admin/category-trees/[id]/categories/[categoryId]/edit.vue +73 -0
  42. package/app/pages/motor-admin/category-trees/[id]/categories/create.vue +64 -0
  43. package/app/pages/motor-admin/category-trees/[id]/edit.vue +45 -0
  44. package/app/pages/motor-admin/category-trees/[id]/index.vue +81 -0
  45. package/app/pages/motor-admin/category-trees/create.vue +37 -0
  46. package/app/pages/motor-admin/category-trees/index.vue +54 -0
  47. package/app/pages/motor-admin/clients/CLAUDE.md +11 -0
  48. package/app/pages/motor-admin/clients/[id]/CLAUDE.md +11 -0
  49. package/app/pages/motor-admin/clients/[id]/edit.vue +45 -0
  50. package/app/pages/motor-admin/clients/create.vue +37 -0
  51. package/app/pages/motor-admin/clients/index.vue +46 -0
  52. package/app/pages/motor-admin/config-variables/CLAUDE.md +11 -0
  53. package/app/pages/motor-admin/config-variables/[id]/edit.vue +44 -0
  54. package/app/pages/motor-admin/config-variables/create.vue +36 -0
  55. package/app/pages/motor-admin/config-variables/index.vue +66 -0
  56. package/app/pages/motor-admin/domains/CLAUDE.md +11 -0
  57. package/app/pages/motor-admin/domains/[id]/edit.vue +54 -0
  58. package/app/pages/motor-admin/domains/create.vue +46 -0
  59. package/app/pages/motor-admin/domains/index.vue +98 -0
  60. package/app/pages/motor-admin/email-templates/CLAUDE.md +12 -0
  61. package/app/pages/motor-admin/email-templates/[id]/CLAUDE.md +7 -0
  62. package/app/pages/motor-admin/email-templates/[id]/edit.vue +56 -0
  63. package/app/pages/motor-admin/email-templates/create.vue +48 -0
  64. package/app/pages/motor-admin/email-templates/index.vue +67 -0
  65. package/app/pages/motor-admin/index.vue +12 -0
  66. package/app/pages/motor-admin/languages/CLAUDE.md +7 -0
  67. package/app/pages/motor-admin/languages/[id]/edit.vue +44 -0
  68. package/app/pages/motor-admin/languages/create.vue +36 -0
  69. package/app/pages/motor-admin/languages/index.vue +44 -0
  70. package/app/pages/motor-admin/permission-groups/CLAUDE.md +14 -0
  71. package/app/pages/motor-admin/permission-groups/[id]/CLAUDE.md +11 -0
  72. package/app/pages/motor-admin/permission-groups/[id]/edit.vue +49 -0
  73. package/app/pages/motor-admin/permission-groups/create.vue +41 -0
  74. package/app/pages/motor-admin/permission-groups/index.vue +43 -0
  75. package/app/pages/motor-admin/roles/CLAUDE.md +7 -0
  76. package/app/pages/motor-admin/roles/[id]/edit.vue +47 -0
  77. package/app/pages/motor-admin/roles/create.vue +40 -0
  78. package/app/pages/motor-admin/roles/index.vue +45 -0
  79. package/app/pages/motor-admin/theme-preview/CLAUDE.md +7 -0
  80. package/app/pages/motor-admin/theme-preview/index.vue +4801 -0
  81. package/app/pages/motor-admin/theme-preview/themes/CLAUDE.md +11 -0
  82. package/app/pages/motor-admin/theme-preview/themes/asymmetric-brutalist.md +381 -0
  83. package/app/pages/motor-admin/theme-preview/themes/bold-modern.md +231 -0
  84. package/app/pages/motor-admin/theme-preview/themes/geometric-minimal.md +778 -0
  85. package/app/pages/motor-admin/theme-preview/themes/gradient-flow.md +1057 -0
  86. package/app/pages/motor-admin/theme-preview/themes/liquid-glass.md +823 -0
  87. package/app/pages/motor-admin/theme-preview/themes/neon-amber.md +1223 -0
  88. package/app/pages/motor-admin/theme-preview/themes/neon-terminal.md +779 -0
  89. package/app/pages/motor-admin/theme-preview/themes/neon-violet.md +1134 -0
  90. package/app/pages/motor-admin/theme-preview/themes/professional-clean.md +232 -0
  91. package/app/pages/motor-admin/theme-preview/themes/refined-brutalist.md +462 -0
  92. package/app/pages/motor-admin/theme-preview/themes/wild-card.md +263 -0
  93. package/app/pages/motor-admin/users/CLAUDE.md +17 -0
  94. package/app/pages/motor-admin/users/[id]/CLAUDE.md +11 -0
  95. package/app/pages/motor-admin/users/[id]/edit.vue +83 -0
  96. package/app/pages/motor-admin/users/create.vue +40 -0
  97. package/app/pages/motor-admin/users/index.vue +66 -0
  98. package/app/pages/profile.vue +363 -0
  99. package/app/pages/search.vue +91 -0
  100. package/app/types/generated/form-meta.ts +258 -0
  101. package/app/types/generated/grid-meta.ts +172 -0
  102. package/nuxt.config.ts +1 -0
  103. package/package.json +26 -0
@@ -0,0 +1,363 @@
1
+ <script setup lang="ts">
2
+ import { z } from 'zod'
3
+ import type { FormSubmitEvent } from '@nuxt/ui'
4
+ import type { User } from '@motor-cms/ui-core/app/types/auth'
5
+
6
+ definePageMeta({
7
+ layout: 'default',
8
+ permission: 'profile.read'
9
+ })
10
+
11
+ const { t } = useI18n()
12
+ const { user, refreshIdentity } = useSanctumAuth<User>()
13
+ const { updateProfile } = useProfileApi()
14
+ const { success, error: notifyError, info } = useNotify()
15
+
16
+ // Test function to demonstrate error notifications
17
+ function testError() {
18
+ try {
19
+ throw new Error('This is a test error to demonstrate error notifications with stack traces')
20
+ } catch (err: unknown) {
21
+ const error = err as Error
22
+ notifyError(t('motor-core.profile.toast_test_error_title'), error.message, {
23
+ message: error.message,
24
+ stack: error.stack,
25
+ status: 500,
26
+ url: '/api/test',
27
+ responseBody: {
28
+ error: 'Internal Server Error',
29
+ details: 'This is mock response data for testing'
30
+ }
31
+ })
32
+ }
33
+ }
34
+
35
+ // Helper to access user data
36
+ const userData = computed(() => user.value?.data)
37
+
38
+ // ============================================
39
+ // Profile Form
40
+ // ============================================
41
+
42
+ const profileSchema = z.object({
43
+ name: z.string().min(1, t('motor-core.profile.validation_name_required')),
44
+ email: z.string().email(t('motor-core.profile.validation_email_invalid')),
45
+ avatar: z.string().nullable().optional()
46
+ })
47
+
48
+ type ProfileSchema = z.output<typeof profileSchema>
49
+
50
+ const profileState = reactive({
51
+ name: '',
52
+ email: ''
53
+ })
54
+
55
+ // Track original values for dirty checking
56
+ const originalProfileState = reactive({
57
+ name: '',
58
+ email: ''
59
+ })
60
+
61
+ const avatarFile = ref<File | null>(null)
62
+
63
+ // Check if profile form has changes
64
+ const isProfileDirty = computed(() => {
65
+ return profileState.name !== originalProfileState.name
66
+ || profileState.email !== originalProfileState.email
67
+ || avatarFile.value !== null
68
+ })
69
+ const avatarPreviewUrl = computed(() => {
70
+ if (avatarFile.value) {
71
+ return URL.createObjectURL(avatarFile.value)
72
+ }
73
+ const avatar = userData.value?.avatar
74
+ if (!avatar) return null
75
+ // Handle conversions as object (e.g., { thumb: { url: '...' } }) or array
76
+ const conversions = avatar.conversions
77
+ if (conversions && typeof conversions === 'object' && !Array.isArray(conversions)) {
78
+ return (conversions as Record<string, { url: string }>).thumb?.url ?? avatar.url
79
+ }
80
+ if (Array.isArray(conversions)) {
81
+ const thumbConversion = conversions.find(c => c.name === 'thumb')
82
+ return thumbConversion?.url ?? avatar.url
83
+ }
84
+ return avatar.url
85
+ })
86
+
87
+ const profileLoading = ref(false)
88
+
89
+ // Initialize form with user data
90
+ watchEffect(() => {
91
+ if (userData.value) {
92
+ profileState.name = userData.value.name || ''
93
+ profileState.email = userData.value.email || ''
94
+ // Store original values for dirty checking
95
+ originalProfileState.name = userData.value.name || ''
96
+ originalProfileState.email = userData.value.email || ''
97
+ }
98
+ })
99
+
100
+ async function onProfileSubmit(event: FormSubmitEvent<ProfileSchema>) {
101
+ // Skip if no changes
102
+ if (!isProfileDirty.value) {
103
+ info(t('motor-core.profile.toast_no_changes_title'), t('motor-core.profile.toast_no_changes_message'))
104
+ return
105
+ }
106
+
107
+ profileLoading.value = true
108
+
109
+ try {
110
+ let avatarPayload: { name: string, dataUrl: string } | null = null
111
+
112
+ if (avatarFile.value) {
113
+ const fullDataUrl = await fileToBase64(avatarFile.value)
114
+ // Extract just the base64 part, removing "data:image/...;base64," prefix
115
+ const base64Data = fullDataUrl.split(',')[1] || fullDataUrl
116
+ avatarPayload = {
117
+ name: avatarFile.value.name,
118
+ dataUrl: base64Data
119
+ }
120
+ } else if (!avatarPreviewUrl.value) {
121
+ // Avatar was removed
122
+ avatarPayload = null
123
+ }
124
+
125
+ const response = await updateProfile({
126
+ name: event.data.name,
127
+ email: event.data.email,
128
+ avatar: avatarPayload
129
+ })
130
+
131
+ // Repopulate form from response and update original state
132
+ profileState.name = response.data.name
133
+ profileState.email = response.data.email
134
+ originalProfileState.name = response.data.name
135
+ originalProfileState.email = response.data.email
136
+ avatarFile.value = null
137
+
138
+ // Refresh global auth state
139
+ await refreshIdentity()
140
+
141
+ success(t('motor-core.profile.toast_profile_updated_title'), t('motor-core.profile.toast_profile_updated_message'))
142
+ } catch (err: unknown) {
143
+ const message = err instanceof Error ? err.message : t('motor-core.profile.toast_profile_error')
144
+ notifyError(t('motor-core.profile.toast_profile_error'), message, {
145
+ message,
146
+ stack: err instanceof Error ? err.stack : undefined,
147
+ url: '/api/profile'
148
+ })
149
+ } finally {
150
+ profileLoading.value = false
151
+ }
152
+ }
153
+
154
+ // ============================================
155
+ // Password Form
156
+ // ============================================
157
+
158
+ const passwordSchema = z.object({
159
+ password: z.string().min(8, t('motor-core.profile.validation_password_min')),
160
+ password_confirmation: z.string()
161
+ }).refine(data => data.password === data.password_confirmation, {
162
+ message: t('motor-core.profile.validation_passwords_mismatch'),
163
+ path: ['password_confirmation']
164
+ })
165
+
166
+ type PasswordSchema = z.output<typeof passwordSchema>
167
+
168
+ const passwordState = reactive({
169
+ password: '',
170
+ password_confirmation: ''
171
+ })
172
+
173
+ const passwordLoading = ref(false)
174
+
175
+ async function onPasswordSubmit(event: FormSubmitEvent<PasswordSchema>) {
176
+ passwordLoading.value = true
177
+
178
+ try {
179
+ await updateProfile({
180
+ name: profileState.name,
181
+ email: profileState.email,
182
+ password: event.data.password
183
+ })
184
+
185
+ // Clear password fields
186
+ passwordState.password = ''
187
+ passwordState.password_confirmation = ''
188
+
189
+ success(t('motor-core.profile.toast_password_updated_title'), t('motor-core.profile.toast_password_updated_message'))
190
+ } catch (err: unknown) {
191
+ const message = err instanceof Error ? err.message : t('motor-core.profile.toast_password_error')
192
+ notifyError(t('motor-core.profile.toast_password_error'), message, {
193
+ message,
194
+ stack: err instanceof Error ? err.stack : undefined,
195
+ url: '/api/profile'
196
+ })
197
+ } finally {
198
+ passwordLoading.value = false
199
+ }
200
+ }
201
+ </script>
202
+
203
+ <template>
204
+ <UDashboardPanel id="profile">
205
+ <template #header>
206
+ <UDashboardNavbar :title="t('motor-core.profile.title')">
207
+ <template #leading>
208
+ <UDashboardSidebarCollapse />
209
+ </template>
210
+ </UDashboardNavbar>
211
+ </template>
212
+
213
+ <template #body>
214
+ <div class="flex flex-col gap-6">
215
+ <!-- Profile Information Card -->
216
+ <UPageCard
217
+ :title="t('motor-core.profile.profile_info_title')"
218
+ :description="t('motor-core.profile.profile_info_description')"
219
+ >
220
+ <UForm
221
+ :schema="profileSchema"
222
+ :state="profileState"
223
+ class="space-y-4"
224
+ @submit="onProfileSubmit"
225
+ >
226
+ <!-- Avatar Row -->
227
+ <div class="grid grid-cols-[150px_1fr] gap-6 items-start">
228
+ <label class="text-sm font-medium text-default pt-2">{{ t('motor-core.profile.avatar_label') }}</label>
229
+ <FileUpload
230
+ v-model="avatarFile"
231
+ :preview-url="avatarPreviewUrl"
232
+ />
233
+ </div>
234
+
235
+ <!-- Name Row -->
236
+ <div class="grid grid-cols-[150px_1fr] gap-6 items-start">
237
+ <label class="text-sm font-medium text-default pt-2">
238
+ {{ t('motor-core.profile.name_label') }} <span class="text-error">*</span>
239
+ </label>
240
+ <UFormField
241
+ name="name"
242
+ :ui="{ wrapper: 'w-full' }"
243
+ >
244
+ <UInput
245
+ v-model="profileState.name"
246
+ :placeholder="t('motor-core.profile.name_placeholder')"
247
+ icon="i-lucide-user"
248
+ class="w-full"
249
+ />
250
+ </UFormField>
251
+ </div>
252
+
253
+ <!-- Email Row -->
254
+ <div class="grid grid-cols-[150px_1fr] gap-6 items-start">
255
+ <label class="text-sm font-medium text-default pt-2">
256
+ {{ t('motor-core.profile.email_label') }} <span class="text-error">*</span>
257
+ </label>
258
+ <UFormField
259
+ name="email"
260
+ :ui="{ wrapper: 'w-full' }"
261
+ >
262
+ <UInput
263
+ v-model="profileState.email"
264
+ type="email"
265
+ :placeholder="t('motor-core.profile.email_placeholder')"
266
+ icon="i-lucide-mail"
267
+ class="w-full"
268
+ />
269
+ </UFormField>
270
+ </div>
271
+
272
+ <!-- Submit Row -->
273
+ <div class="grid grid-cols-[150px_1fr] gap-6">
274
+ <div />
275
+ <div class="flex justify-end gap-2">
276
+ <UButton
277
+ color="error"
278
+ variant="outline"
279
+ icon="i-lucide-bug"
280
+ @click="testError"
281
+ >
282
+ {{ t('motor-core.profile.test_error') }}
283
+ </UButton>
284
+ <UButton
285
+ type="submit"
286
+ :loading="profileLoading"
287
+ icon="i-lucide-save"
288
+ >
289
+ {{ t('motor-core.profile.save_profile') }}
290
+ </UButton>
291
+ </div>
292
+ </div>
293
+ </UForm>
294
+ </UPageCard>
295
+
296
+ <!-- Change Password Card -->
297
+ <UPageCard
298
+ :title="t('motor-core.profile.change_password_title')"
299
+ :description="t('motor-core.profile.change_password_description')"
300
+ >
301
+ <UForm
302
+ :schema="passwordSchema"
303
+ :state="passwordState"
304
+ class="space-y-4"
305
+ @submit="onPasswordSubmit"
306
+ >
307
+ <!-- New Password Row -->
308
+ <div class="grid grid-cols-[150px_1fr] gap-6 items-start">
309
+ <label class="text-sm font-medium text-default pt-2">
310
+ {{ t('motor-core.profile.new_password_label') }} <span class="text-error">*</span>
311
+ </label>
312
+ <UFormField
313
+ name="password"
314
+ :ui="{ wrapper: 'w-full' }"
315
+ >
316
+ <UInput
317
+ v-model="passwordState.password"
318
+ type="password"
319
+ :placeholder="t('motor-core.profile.new_password_placeholder')"
320
+ icon="i-lucide-lock"
321
+ class="w-full"
322
+ />
323
+ </UFormField>
324
+ </div>
325
+
326
+ <!-- Confirm Password Row -->
327
+ <div class="grid grid-cols-[150px_1fr] gap-6 items-start">
328
+ <label class="text-sm font-medium text-default pt-2">
329
+ {{ t('motor-core.profile.confirm_password_label') }} <span class="text-error">*</span>
330
+ </label>
331
+ <UFormField
332
+ name="password_confirmation"
333
+ :ui="{ wrapper: 'w-full' }"
334
+ >
335
+ <UInput
336
+ v-model="passwordState.password_confirmation"
337
+ type="password"
338
+ :placeholder="t('motor-core.profile.confirm_password_placeholder')"
339
+ icon="i-lucide-lock"
340
+ class="w-full"
341
+ />
342
+ </UFormField>
343
+ </div>
344
+
345
+ <!-- Submit Row -->
346
+ <div class="grid grid-cols-[150px_1fr] gap-6">
347
+ <div />
348
+ <div class="flex justify-end">
349
+ <UButton
350
+ type="submit"
351
+ :loading="passwordLoading"
352
+ icon="i-lucide-key"
353
+ >
354
+ {{ t('motor-core.profile.update_password') }}
355
+ </UButton>
356
+ </div>
357
+ </div>
358
+ </UForm>
359
+ </UPageCard>
360
+ </div>
361
+ </template>
362
+ </UDashboardPanel>
363
+ </template>
@@ -0,0 +1,91 @@
1
+ <!-- app/pages/search.vue -->
2
+ <script setup lang="ts">
3
+ import type { SearchGridRow } from '@motor-cms/ui-core/app/types/search'
4
+ import type { ColumnDef } from '@motor-cms/ui-core/app/types/grid'
5
+
6
+ const { t } = useI18n()
7
+
8
+ const ALL_MODULES = '_all'
9
+ const moduleFilter = ref(ALL_MODULES)
10
+
11
+ const moduleFacets = useModuleFacets()
12
+ const moduleOptions = computed(() => {
13
+ const options = [{ label: t('motor-core.search.filter_all_modules'), value: ALL_MODULES }]
14
+ for (const [key, count] of Object.entries(moduleFacets.value)) {
15
+ options.push({ label: `${resolveModuleLabel(key, t)} (${count})`, value: key })
16
+ }
17
+ return options
18
+ })
19
+
20
+ const activeModule = computed(() => moduleFilter.value === ALL_MODULES ? undefined : moduleFilter.value)
21
+ const fetchResults = computed(() => fetchSearchGrid(t, activeModule.value))
22
+
23
+ const columns: ColumnDef<SearchGridRow>[] = [
24
+ {
25
+ key: 'module',
26
+ label: t('motor-core.search.column_module'),
27
+ renderer: 'badge',
28
+ width: 'w-[12%]'
29
+ },
30
+ {
31
+ key: 'index_label',
32
+ label: t('motor-core.search.column_type'),
33
+ renderer: 'badge',
34
+ rendererProps: { color: 'neutral' },
35
+ width: 'w-[12%]'
36
+ },
37
+ {
38
+ key: 'title',
39
+ label: t('motor-core.search.column_title'),
40
+ width: 'w-[30%]'
41
+ },
42
+ {
43
+ key: 'excerpt',
44
+ label: t('motor-core.search.column_excerpt')
45
+ }
46
+ ]
47
+ </script>
48
+
49
+ <template>
50
+ <GridPage
51
+ :title="t('motor-core.search.title')"
52
+ :subtitle="t('motor-core.search.subtitle')"
53
+ >
54
+ <GridBase
55
+ id="search-grid"
56
+ :key="moduleFilter"
57
+ :fetch="fetchResults"
58
+ :columns="columns"
59
+ :searchable="true"
60
+ :row-click-to="(row: any) => row.to"
61
+ :disable-default-actions="true"
62
+ >
63
+ <template #toolbar-extra>
64
+ <USelectMenu
65
+ v-model="moduleFilter"
66
+ :items="moduleOptions"
67
+ value-key="value"
68
+ label-key="label"
69
+ class="w-48"
70
+ :clear="moduleFilter !== ALL_MODULES"
71
+ @clear="moduleFilter = ALL_MODULES"
72
+ />
73
+ </template>
74
+
75
+ <template #empty>
76
+ <div class="flex flex-col items-center justify-center py-12 gap-3">
77
+ <UIcon
78
+ name="i-lucide-search"
79
+ class="size-8 text-muted"
80
+ />
81
+ <p class="text-sm font-medium text-muted">
82
+ {{ t('motor-core.search.min_chars') }}
83
+ </p>
84
+ <p class="text-xs text-dimmed">
85
+ {{ t('motor-core.search.no_results_hint') }}
86
+ </p>
87
+ </div>
88
+ </template>
89
+ </GridBase>
90
+ </GridPage>
91
+ </template>