@bobbykim/manguito-cms-admin 0.1.1 → 0.3.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 (90) hide show
  1. package/dist/assets/{ChangePasswordView-8UW4RWd8.js → ChangePasswordView-B0wg3O_I.js} +1 -1
  2. package/dist/assets/{EnumSelect-BOrBkA76.js → ComputedDisplay-C2FpwSB1.js} +2 -2
  3. package/dist/assets/ContentFormView-D92O0ICh.js +1 -0
  4. package/dist/assets/{ContentListView-BiPHUBHa.js → ContentListView-DM4xggDx.js} +1 -1
  5. package/dist/assets/{ContentSettingsView-DPKF5MRL.js → ContentSettingsView-B9UHNleT.js} +1 -1
  6. package/dist/assets/{LoginView-WELPH4Vh.js → LoginView-DPpdT3Ro.js} +1 -1
  7. package/dist/assets/{MediaDetailView-BT66fV9b.js → MediaDetailView-OUzzhO1B.js} +1 -1
  8. package/dist/assets/{MediaLibraryView-BcdiJ_7a.js → MediaLibraryView-Dx9t8r8T.js} +1 -1
  9. package/dist/assets/TaxonomyFormView-CZlOjxOY.js +1 -0
  10. package/dist/assets/{TaxonomyListView-BB7iTfjb.js → TaxonomyListView-Dkga11a3.js} +1 -1
  11. package/dist/assets/{UserFormView-C2p-FV0A.js → UserFormView-CaOcoKX5.js} +1 -1
  12. package/dist/assets/{UserListView-BnlU2Ovr.js → UserListView-B6_Hdemt.js} +1 -1
  13. package/dist/assets/{index-BkazhAun.js → index-s-2X8XCa.js} +2 -2
  14. package/dist/assets/{useNotification-DU6J5vms.js → useNotification-CEPVkHak.js} +1 -1
  15. package/dist/codegen/index.cjs +7 -2
  16. package/dist/codegen/index.js +7 -2
  17. package/dist/index.html +1 -1
  18. package/index.html +13 -0
  19. package/package.json +11 -7
  20. package/public/favicon.ico +0 -0
  21. package/src/App.vue +147 -0
  22. package/src/adapters/admin.ts +13 -0
  23. package/src/components/.gitkeep +0 -0
  24. package/src/components/fields/BooleanToggle.vue +63 -0
  25. package/src/components/fields/ComputedDisplay.vue +23 -0
  26. package/src/components/fields/DatePicker.vue +61 -0
  27. package/src/components/fields/EnumSelect.vue +69 -0
  28. package/src/components/fields/MediaUpload.vue +445 -0
  29. package/src/components/fields/NumberInput.vue +66 -0
  30. package/src/components/fields/ParagraphEmbed.vue +149 -0
  31. package/src/components/fields/ReferenceSelect.vue +275 -0
  32. package/src/components/fields/RichTextEditor.vue +293 -0
  33. package/src/components/fields/TextInput.vue +93 -0
  34. package/src/components/fields/__tests__/BooleanToggle.test.ts +37 -0
  35. package/src/components/fields/__tests__/ComputedDisplay.test.ts +34 -0
  36. package/src/components/fields/__tests__/DatePicker.test.ts +37 -0
  37. package/src/components/fields/__tests__/EnumSelect.test.ts +53 -0
  38. package/src/components/fields/__tests__/NumberInput.test.ts +64 -0
  39. package/src/components/fields/__tests__/ReferenceSelect.test.ts +196 -0
  40. package/src/components/fields/__tests__/RichTextEditor.test.ts +81 -0
  41. package/src/components/fields/__tests__/TextInput.test.ts +72 -0
  42. package/src/components/layout/AppShell.vue +33 -0
  43. package/src/components/layout/Footer.vue +11 -0
  44. package/src/components/layout/Sidebar.vue +254 -0
  45. package/src/components/layout/Topbar.vue +46 -0
  46. package/src/components/shared/ConfirmDialog.vue +38 -0
  47. package/src/components/shared/MediaSelectModal.vue +273 -0
  48. package/src/components/shared/Pagination.vue +93 -0
  49. package/src/components/shared/StatusBadge.vue +14 -0
  50. package/src/components/shared/ToastContainer.vue +44 -0
  51. package/src/composables/.gitkeep +0 -0
  52. package/src/composables/__tests__/useApiClient.test.ts +90 -0
  53. package/src/composables/__tests__/useFormValidation.test.ts +83 -0
  54. package/src/composables/__tests__/usePermission.test.ts +116 -0
  55. package/src/composables/useApiClient.ts +68 -0
  56. package/src/composables/useDirtyState.ts +28 -0
  57. package/src/composables/useFormValidation.ts +107 -0
  58. package/src/composables/useNotification.ts +26 -0
  59. package/src/composables/usePermission.ts +20 -0
  60. package/src/composables/useTabIndicator.ts +25 -0
  61. package/src/env.d.ts +2 -0
  62. package/src/main.ts +21 -0
  63. package/src/router/index.ts +174 -0
  64. package/src/stores/auth.ts +82 -0
  65. package/src/stores/content.ts +40 -0
  66. package/src/stores/media.ts +31 -0
  67. package/src/stores/schema.ts +130 -0
  68. package/src/stores/taxonomy.ts +40 -0
  69. package/src/stores/ui.ts +94 -0
  70. package/src/stores/users.ts +44 -0
  71. package/src/style.css +46 -0
  72. package/src/test-utils/server.ts +54 -0
  73. package/src/types/index.ts +0 -0
  74. package/src/views/.gitkeep +0 -0
  75. package/src/views/ChangePasswordView.vue +134 -0
  76. package/src/views/LoginView.vue +187 -0
  77. package/src/views/RolesView.vue +84 -0
  78. package/src/views/content/ContentFormView.vue +553 -0
  79. package/src/views/content/ContentListView.vue +236 -0
  80. package/src/views/content/ContentSettingsView.vue +122 -0
  81. package/src/views/media/MediaDetailView.vue +222 -0
  82. package/src/views/media/MediaLibraryView.vue +492 -0
  83. package/src/views/taxonomy/TaxonomyFormView.vue +286 -0
  84. package/src/views/taxonomy/TaxonomyListView.vue +198 -0
  85. package/src/views/users/UserFormView.vue +307 -0
  86. package/src/views/users/UserListView.vue +127 -0
  87. package/src/vite-env.d.ts +1 -0
  88. package/vite.config.ts +17 -0
  89. package/dist/assets/ContentFormView-vaIInaYs.js +0 -1
  90. package/dist/assets/TaxonomyFormView-BdRO9L-7.js +0 -1
@@ -0,0 +1,307 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+ import { useApiClient } from '../../composables/useApiClient'
5
+ import { usePermission } from '../../composables/usePermission'
6
+ import { useNotification } from '../../composables/useNotification'
7
+ import { useAuthStore } from '../../stores/auth'
8
+ import { useUsersStore } from '../../stores/users'
9
+ import type { UserResponse } from '../../stores/users'
10
+ import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
11
+
12
+ const route = useRoute()
13
+ const router = useRouter()
14
+ const api = useApiClient()
15
+ const { can, rolesBelow } = usePermission()
16
+ const notify = useNotification()
17
+ const authStore = useAuthStore()
18
+ const usersStore = useUsersStore()
19
+
20
+ // ── Mode ──────────────────────────────────────────────────────────────────────
21
+
22
+ const routeId = computed(() => route.params.id as string | undefined)
23
+ const isEdit = computed(() => !!routeId.value)
24
+ const isOwnProfile = computed(() => authStore.id === routeId.value)
25
+
26
+ // ── Form state ────────────────────────────────────────────────────────────────
27
+
28
+ const email = ref('')
29
+ const selectedRole = ref('')
30
+ const loading = ref(false)
31
+ const saving = ref(false)
32
+ const formError = ref('')
33
+
34
+ // Shown once after successful user creation.
35
+ const temporaryPassword = ref<string | null>(null)
36
+ const copied = ref(false)
37
+
38
+ // Password reset result.
39
+ const resetPassword = ref<string | null>(null)
40
+ const resetting = ref(false)
41
+
42
+ const showDeleteConfirm = ref(false)
43
+
44
+ // ── Load user (edit mode) ─────────────────────────────────────────────────────
45
+
46
+ onMounted(async () => {
47
+ if (!isEdit.value || !routeId.value) {
48
+ // Create mode: default role to first available below current user.
49
+ selectedRole.value = rolesBelow()[0]?.name ?? ''
50
+ return
51
+ }
52
+
53
+ loading.value = true
54
+ const cached = usersStore.users.find(u => u.id === routeId.value)
55
+ if (cached) {
56
+ email.value = cached.email
57
+ selectedRole.value = cached.role
58
+ loading.value = false
59
+ return
60
+ }
61
+
62
+ const res = await api.get<UserResponse>(`/users/${routeId.value}`)
63
+ loading.value = false
64
+ if (res.ok) {
65
+ email.value = res.data.email
66
+ selectedRole.value = res.data.role
67
+ usersStore.setUser(res.data)
68
+ }
69
+ })
70
+
71
+ // ── Submit ────────────────────────────────────────────────────────────────────
72
+
73
+ async function onSubmit() {
74
+ formError.value = ''
75
+ saving.value = true
76
+
77
+ if (isEdit.value) {
78
+ // Edit: patch email and role (role hidden for own profile so selectedRole stays unchanged).
79
+ const res = await api.patch<UserResponse>(`/users/${routeId.value!}`, {
80
+ email: email.value.trim(),
81
+ role: selectedRole.value,
82
+ })
83
+ saving.value = false
84
+ if (!res.ok) {
85
+ formError.value = res.error.message
86
+ return
87
+ }
88
+ usersStore.setUser(res.data)
89
+ notify.success('User updated.')
90
+ } else {
91
+ // Create: POST and show temp password.
92
+ const res = await api.post<UserResponse & { temporary_password?: string }>('/users', {
93
+ email: email.value.trim(),
94
+ role: selectedRole.value,
95
+ })
96
+ saving.value = false
97
+ if (!res.ok) {
98
+ formError.value = res.error.message
99
+ return
100
+ }
101
+ usersStore.setUser(res.data)
102
+ temporaryPassword.value = res.data.temporary_password ?? null
103
+ notify.success('User created.')
104
+ }
105
+ }
106
+
107
+ // ── Copy to clipboard ─────────────────────────────────────────────────────────
108
+
109
+ async function copyPassword(text: string) {
110
+ try {
111
+ await navigator.clipboard.writeText(text)
112
+ copied.value = true
113
+ setTimeout(() => { copied.value = false }, 2000)
114
+ } catch {
115
+ // Fallback: select the input text.
116
+ }
117
+ }
118
+
119
+ // ── Reset password ────────────────────────────────────────────────────────────
120
+
121
+ async function doResetPassword() {
122
+ if (!routeId.value) return
123
+ resetting.value = true
124
+ const res = await api.post<{ temporary_password: string }>(`/users/${routeId.value}/reset-password`, {})
125
+ resetting.value = false
126
+ if (res.ok) {
127
+ resetPassword.value = res.data.temporary_password
128
+ copied.value = false
129
+ } else {
130
+ notify.error(res.error.message)
131
+ }
132
+ }
133
+
134
+ // ── Delete ────────────────────────────────────────────────────────────────────
135
+
136
+ async function doDelete() {
137
+ showDeleteConfirm.value = false
138
+ if (!routeId.value) return
139
+ saving.value = true
140
+ const res = await api.del(`/users/${routeId.value}`)
141
+ saving.value = false
142
+ if (!res.ok) {
143
+ formError.value = (res as { ok: false; error: { message: string } }).error.message
144
+ return
145
+ }
146
+ usersStore.removeUser(routeId.value)
147
+ void router.push({ name: 'user-list' })
148
+ }
149
+
150
+ // ── Page title ────────────────────────────────────────────────────────────────
151
+
152
+ const pageTitle = computed(() => isEdit.value ? (email.value || 'Edit user') : 'New user')
153
+ </script>
154
+
155
+ <template>
156
+ <!-- Loading -->
157
+ <div v-if="loading" class="flex items-center justify-center py-20">
158
+ <span
159
+ class="inline-block h-6 w-6 animate-spin rounded-full border-2 border-indigo-600 border-t-transparent"
160
+ aria-hidden="true"
161
+ />
162
+ </div>
163
+
164
+ <div v-else class="mx-auto max-w-lg">
165
+ <!-- Header -->
166
+ <div class="mb-6 flex items-start justify-between gap-4">
167
+ <h1 class="text-[26px] font-bold tracking-tight text-ink">{{ pageTitle }}</h1>
168
+
169
+ <div class="flex shrink-0 items-center gap-2">
170
+ <!-- Reset password -->
171
+ <button
172
+ v-if="can('users:edit') && isEdit && !isOwnProfile"
173
+ type="button"
174
+ :disabled="resetting"
175
+ class="rounded-md px-3 py-2 text-[13px] font-semibold text-[#3D3D52] ring-1 ring-inset ring-gray-300 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
176
+ @click="doResetPassword"
177
+ >
178
+ {{ resetting ? 'Resetting…' : 'Reset password' }}
179
+ </button>
180
+
181
+ <!-- Delete -->
182
+ <button
183
+ v-if="can('users:delete') && isEdit && !isOwnProfile"
184
+ type="button"
185
+ :disabled="saving"
186
+ class="rounded-md px-3 py-2 text-sm font-medium text-red-600 ring-1 ring-inset ring-red-300 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-50"
187
+ @click="showDeleteConfirm = true"
188
+ >
189
+ Delete
190
+ </button>
191
+ </div>
192
+ </div>
193
+
194
+ <!-- Temporary password box (shown once after creation) -->
195
+ <div
196
+ v-if="temporaryPassword"
197
+ class="mb-5 rounded-md border border-amber-200 bg-amber-50 p-4"
198
+ role="alert"
199
+ >
200
+ <p class="mb-2 text-sm font-medium text-amber-800">
201
+ User created. Share this temporary password — it will not be shown again.
202
+ </p>
203
+ <div class="flex items-center gap-2">
204
+ <input
205
+ :value="temporaryPassword"
206
+ type="text"
207
+ readonly
208
+ class="flex-1 rounded-md border border-amber-200 bg-white px-3 py-1.5 font-mono text-sm"
209
+ />
210
+ <button
211
+ type="button"
212
+ class="shrink-0 rounded-md bg-amber-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-amber-700"
213
+ @click="copyPassword(temporaryPassword)"
214
+ >
215
+ {{ copied ? 'Copied!' : 'Copy' }}
216
+ </button>
217
+ </div>
218
+ </div>
219
+
220
+ <!-- Reset password box (shown after password reset) -->
221
+ <div
222
+ v-if="resetPassword"
223
+ class="mb-5 rounded-md border border-amber-200 bg-amber-50 p-4"
224
+ role="alert"
225
+ >
226
+ <p class="mb-2 text-sm font-medium text-amber-800">
227
+ Password reset. Share this temporary password — it will not be shown again.
228
+ </p>
229
+ <div class="flex items-center gap-2">
230
+ <input
231
+ :value="resetPassword"
232
+ type="text"
233
+ readonly
234
+ class="flex-1 rounded-md border border-amber-200 bg-white px-3 py-1.5 font-mono text-sm"
235
+ />
236
+ <button
237
+ type="button"
238
+ class="shrink-0 rounded-md bg-amber-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-amber-700"
239
+ @click="copyPassword(resetPassword)"
240
+ >
241
+ {{ copied ? 'Copied!' : 'Copy' }}
242
+ </button>
243
+ </div>
244
+ </div>
245
+
246
+ <!-- Form -->
247
+ <form class="space-y-5" novalidate @submit.prevent="onSubmit">
248
+ <!-- Email -->
249
+ <div>
250
+ <label for="user-email" class="block text-[13px] font-semibold text-[#3D3D52]">Email</label>
251
+ <input
252
+ id="user-email"
253
+ v-model="email"
254
+ type="email"
255
+ required
256
+ autocomplete="email"
257
+ :disabled="saving"
258
+ class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-200 disabled:cursor-not-allowed disabled:opacity-60"
259
+ />
260
+ </div>
261
+
262
+ <!-- Role picker — hidden for own profile to prevent self-demotion -->
263
+ <div v-if="!isOwnProfile">
264
+ <label for="user-role" class="block text-[13px] font-semibold text-[#3D3D52]">Role</label>
265
+ <select
266
+ id="user-role"
267
+ v-model="selectedRole"
268
+ :disabled="saving || rolesBelow().length === 0"
269
+ class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-200 disabled:cursor-not-allowed disabled:opacity-60"
270
+ >
271
+ <option value="" disabled>Select a role</option>
272
+ <option
273
+ v-for="r in rolesBelow()"
274
+ :key="r.name"
275
+ :value="r.name"
276
+ >
277
+ {{ r.label }}
278
+ </option>
279
+ </select>
280
+ <p v-if="rolesBelow().length === 0" class="mt-1 text-xs text-gray-400">
281
+ No assignable roles available.
282
+ </p>
283
+ </div>
284
+
285
+ <!-- Form error -->
286
+ <p v-if="formError" class="text-sm text-red-600" role="alert">{{ formError }}</p>
287
+
288
+ <!-- Submit -->
289
+ <button
290
+ v-if="isEdit ? can('users:edit') : can('users:create')"
291
+ type="submit"
292
+ :disabled="saving || !email.trim() || (!isOwnProfile && !selectedRole)"
293
+ class="w-full rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50"
294
+ >
295
+ {{ saving ? 'Saving…' : isEdit ? 'Save changes' : 'Create user' }}
296
+ </button>
297
+ </form>
298
+
299
+ <!-- Delete confirmation -->
300
+ <ConfirmDialog
301
+ v-if="showDeleteConfirm"
302
+ message="Are you sure you want to delete this user? This action cannot be undone."
303
+ :on-confirm="doDelete"
304
+ :on-cancel="() => (showDeleteConfirm = false)"
305
+ />
306
+ </div>
307
+ </template>
@@ -0,0 +1,127 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, ref, computed } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+ import { useApiClient } from '../../composables/useApiClient'
5
+ import { usePermission } from '../../composables/usePermission'
6
+ import { useUsersStore } from '../../stores/users'
7
+ import type { UserResponse } from '../../stores/users'
8
+
9
+ const router = useRouter()
10
+ const api = useApiClient()
11
+ const { can } = usePermission()
12
+ const usersStore = useUsersStore()
13
+
14
+ const search = ref('')
15
+
16
+ // Full list is already loaded eagerly (no pagination on this endpoint) — filter client-side.
17
+ const filteredUsers = computed(() => {
18
+ const term = search.value.trim().toLowerCase()
19
+ if (term === '') return usersStore.users
20
+ return usersStore.users.filter(u => u.email.toLowerCase().includes(term))
21
+ })
22
+
23
+ function formatDate(val: string | unknown): string {
24
+ if (typeof val !== 'string' || !val) return '—'
25
+ return new Date(val).toLocaleDateString(undefined, {
26
+ year: 'numeric', month: 'short', day: 'numeric',
27
+ })
28
+ }
29
+
30
+ onMounted(async () => {
31
+ if (usersStore.users.length > 0) return // already loaded
32
+ usersStore.loading = true
33
+ const res = await api.get<UserResponse[]>('/users')
34
+ usersStore.loading = false
35
+ if (res.ok) usersStore.setUsers(res.data)
36
+ })
37
+
38
+ function goToEdit(user: UserResponse) {
39
+ void router.push({ name: 'user-edit', params: { id: user.id } })
40
+ }
41
+
42
+ function goToCreate() {
43
+ void router.push({ name: 'user-new' })
44
+ }
45
+ </script>
46
+
47
+ <template>
48
+ <div>
49
+ <!-- Header -->
50
+ <div class="mb-5.5 flex flex-wrap items-center justify-between gap-4">
51
+ <h1 class="text-[26px] font-bold tracking-tight text-ink">Users</h1>
52
+ <button
53
+ v-if="can('users:create')"
54
+ type="button"
55
+ class="inline-flex items-center gap-1.75 rounded-[11px] bg-indigo-600 px-4 py-2.5 text-[13.5px] font-semibold text-white shadow-[0_3px_10px_rgba(91,87,232,0.3)] transition-all hover:bg-indigo-700 hover:shadow-[0_6px_18px_rgba(91,87,232,0.4)]"
56
+ @click="goToCreate"
57
+ >
58
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14" /><path d="M5 12h14" /></svg>
59
+ New user
60
+ </button>
61
+ </div>
62
+
63
+ <!-- Loading -->
64
+ <div v-if="usersStore.loading" class="space-y-2">
65
+ <div v-for="n in 5" :key="n" class="h-12 animate-pulse rounded-[10px] bg-gray-100" />
66
+ </div>
67
+
68
+ <!-- Search -->
69
+ <div
70
+ v-else-if="usersStore.users.length > 0"
71
+ class="mb-4 flex h-9.5 items-center gap-2 rounded-[10px] border border-card-border bg-[#FBFBFD] px-2.75 text-[13px] transition-colors focus-within:border-indigo-400"
72
+ >
73
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 text-faint">
74
+ <path d="M21 21l-4.3-4.3" /><path d="M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z" />
75
+ </svg>
76
+ <input
77
+ v-model="search"
78
+ type="text"
79
+ placeholder="Search users…"
80
+ class="w-full bg-transparent text-ink outline-none placeholder:text-faint"
81
+ />
82
+ </div>
83
+
84
+ <!-- Empty -->
85
+ <div
86
+ v-if="!usersStore.loading && usersStore.users.length === 0"
87
+ class="rounded-2xl border border-dashed border-gray-300 p-12 text-center text-sm text-muted"
88
+ >
89
+ No users found.
90
+ </div>
91
+
92
+ <!-- No search results -->
93
+ <div
94
+ v-else-if="!usersStore.loading && filteredUsers.length === 0"
95
+ class="rounded-2xl border border-dashed border-gray-300 p-12 text-center text-sm text-muted"
96
+ >
97
+ No results for “{{ search.trim() }}”.
98
+ </div>
99
+
100
+ <!-- Table -->
101
+ <div v-else-if="!usersStore.loading" class="overflow-hidden rounded-2xl border border-card-border bg-white shadow-[0_1px_2px_rgba(24,24,48,0.04),0_10px_28px_rgba(24,24,48,0.04)]">
102
+ <table class="w-full text-sm">
103
+ <thead class="text-[11.5px] font-bold uppercase tracking-[.06em] text-faint">
104
+ <tr class="border-b border-divider">
105
+ <th class="px-5.5 py-3.5 text-left">Email</th>
106
+ <th class="px-5.5 py-3.5 text-left">Role</th>
107
+ <th class="px-5.5 py-3.5 text-left">Created</th>
108
+ </tr>
109
+ </thead>
110
+ <tbody class="divide-y divide-[#F4F3F9]">
111
+ <tr
112
+ v-for="user in filteredUsers"
113
+ :key="user.id"
114
+ class="cursor-pointer transition-colors hover:bg-[#FAFAFE]"
115
+ @click="goToEdit(user)"
116
+ >
117
+ <td class="px-5.5 py-4 text-[14.5px] font-semibold text-ink">{{ user.email }}</td>
118
+ <td class="px-5.5 py-4">
119
+ <span class="rounded-[7px] bg-indigo-50 px-2.5 py-1 font-mono text-xs font-medium text-indigo-600">{{ user.role }}</span>
120
+ </td>
121
+ <td class="px-5.5 py-4 text-[13px] text-faint">{{ formatDate(user.created_at) }}</td>
122
+ </tr>
123
+ </tbody>
124
+ </table>
125
+ </div>
126
+ </div>
127
+ </template>
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
package/vite.config.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+
5
+ export default defineConfig({
6
+ plugins: [vue(), tailwindcss()],
7
+ define: {
8
+ __ADMIN_PREFIX__: JSON.stringify('/admin'),
9
+ __API_PREFIX__: JSON.stringify('/api'),
10
+ },
11
+ build: {
12
+ // No inline module-preload polyfill — keeps script-src 'self' working
13
+ // without an inline-script exception. Modern browsers preload modules
14
+ // natively via <link rel="modulepreload">.
15
+ modulePreload: { polyfill: false },
16
+ },
17
+ })
@@ -1 +0,0 @@
1
- import{A as e,B as t,E as n,L as r,O as i,P as a,W as o,a as s,c,ct as l,d as u,g as d,i as f,j as p,l as m,lt as h,m as g,s as _,t as v,u as y,ut as b,v as x,w as ee,x as S}from"./schema-B-RKX_ep.js";import{a as C,c as w,d as T,n as E,r as D,s as O,t as te,u as k}from"./index-BkazhAun.js";import{a as A,i as j,n as M,o as N,r as P,s as ne,t as re}from"./EnumSelect-BOrBkA76.js";import{t as F}from"./useNotification-DU6J5vms.js";import{t as ie}from"./content--2XYr_UQ.js";import{t as ae}from"./ConfirmDialog-B7nxdDge.js";import{t as oe}from"./useTabIndicator-CvEsGBJh.js";var se={class:`flex h-full max-h-144 w-full max-w-3xl flex-col overflow-hidden rounded-lg bg-white shadow-xl`},I={class:`flex items-center justify-between border-b border-gray-200 px-5 py-4`},L={key:0,class:`flex gap-1 border-b border-gray-200 px-5 pt-3`},ce=[`onClick`],le={key:1,class:`border-b border-gray-200 px-5 py-2 text-sm text-gray-500`},ue={class:`font-medium text-gray-700`},de={class:`flex-1 overflow-y-auto p-5`},fe={key:0,class:`flex items-center justify-center py-16`},pe={key:1,class:`flex flex-col items-center justify-center py-16 text-gray-400`},R={key:2,class:`grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5`},z=[`aria-pressed`,`aria-label`,`onClick`],B=[`src`,`alt`],V={key:1,class:`flex h-full w-full flex-col items-center justify-center gap-1 bg-gray-50 text-gray-400`,"aria-hidden":`true`},H={class:`text-3xl`},me={class:`max-w-full truncate px-1 text-xs`},U={key:2,class:`absolute right-1 top-1 flex h-5 w-5 items-center justify-center rounded-full bg-indigo-600 text-xs text-white`,"aria-hidden":`true`},W={key:2,class:`flex items-center justify-center gap-3 border-t border-gray-100 px-5 py-2`},G=[`disabled`],K={class:`text-xs text-gray-500`},he=[`disabled`],q={class:`flex justify-end gap-3 border-t border-gray-200 px-5 py-4`},J=[`disabled`],Y=d({__name:`MediaSelectModal`,props:{acceptedType:{}},emits:[`select`,`close`],setup(e,{emit:r}){let a=e,o=r,d=te(),h=[{label:`Images`,value:`image`},{label:`Videos`,value:`video`},{label:`PDFs`,value:`file`}],v=t(a.acceptedType??`all`),x=_(()=>!a.acceptedType);p(()=>a.acceptedType,e=>{e&&(v.value=e)});let S=t([]),C=t(!1),w=t(1),E=t(1),D=t(null);function O(e,t){let n=new URLSearchParams({page:String(t),per_page:`20`});return e!==`all`&&n.set(`type`,e),`/media?${n.toString()}`}async function k(){C.value=!0;let e=await d.get(O(v.value,w.value));C.value=!1,e.ok&&(S.value=e.data,E.value=e.meta?.total_pages??1)}p(v,()=>{w.value=1,D.value=null,k()}),p(w,()=>{k()}),ee(()=>{k()});function A(){D.value&&o(`select`,D.value)}return(t,r)=>(n(),m(s,{to:`body`},[c(`div`,{class:`fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4`,role:`dialog`,"aria-modal":`true`,"aria-label":`Select media`,onClick:r[5]||=T(e=>t.$emit(`close`),[`self`])},[c(`div`,se,[c(`div`,I,[r[6]||=c(`h2`,{class:`text-base font-semibold text-gray-900`},`Select Media`,-1),c(`button`,{type:`button`,class:`text-xl leading-none text-gray-400 hover:text-gray-600`,"aria-label":`Close`,onClick:r[0]||=e=>t.$emit(`close`)},` × `)]),x.value?(n(),u(`div`,L,[c(`button`,{type:`button`,class:l([`rounded-t-md px-3 py-1.5 text-sm font-medium transition-colors`,v.value===`all`?`border-b-2 border-indigo-600 text-indigo-600`:`text-gray-500 hover:text-gray-800`]),onClick:r[1]||=e=>v.value=`all`},` All `,2),(n(),u(f,null,i(h,e=>c(`button`,{key:e.value,type:`button`,class:l([`rounded-t-md px-3 py-1.5 text-sm font-medium transition-colors`,v.value===e.value?`border-b-2 border-indigo-600 text-indigo-600`:`text-gray-500 hover:text-gray-800`]),onClick:t=>v.value=e.value},b(e.label),11,ce)),64))])):(n(),u(`div`,le,[r[7]||=g(` Showing: `,-1),c(`span`,ue,b(h.find(t=>t.value===e.acceptedType)?.label??e.acceptedType),1)])),c(`div`,de,[C.value?(n(),u(`div`,fe,[...r[8]||=[c(`span`,{class:`inline-block h-6 w-6 animate-spin rounded-full border-2 border-indigo-600 border-t-transparent`,"aria-hidden":`true`},null,-1)]])):S.value.length===0?(n(),u(`div`,pe,[...r[9]||=[c(`span`,{class:`text-4xl`,"aria-hidden":`true`},`🖼`,-1),c(`p`,{class:`mt-2 text-sm`},`No media found`,-1)]])):(n(),u(`div`,R,[(n(!0),u(f,null,i(S.value,e=>(n(),u(`button`,{key:e.id,type:`button`,class:l([`group relative aspect-square overflow-hidden rounded-md border-2 transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1`,D.value?.id===e.id?`border-indigo-500`:`border-transparent hover:border-gray-300`]),"aria-pressed":D.value?.id===e.id,"aria-label":`Select ${e.alt??e.url.split(`/`).pop()}`,onClick:t=>D.value=e},[e.type===`image`?(n(),u(`img`,{key:0,src:e.url,alt:e.alt??``,class:`h-full w-full object-cover`,loading:`lazy`},null,8,B)):(n(),u(`div`,V,[c(`span`,H,b(e.type===`video`?`▶`:`📄`),1),c(`span`,me,b(e.url.split(`/`).pop()),1)])),D.value?.id===e.id?(n(),u(`div`,U,` ✓ `)):y(``,!0)],10,z))),128))]))]),E.value>1?(n(),u(`div`,W,[c(`button`,{type:`button`,disabled:w.value===1,class:`rounded px-2 py-1 text-sm text-gray-600 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40`,onClick:r[2]||=e=>w.value--},` ← Prev `,8,G),c(`span`,K,b(w.value)+` / `+b(E.value),1),c(`button`,{type:`button`,disabled:w.value===E.value,class:`rounded px-2 py-1 text-sm text-gray-600 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40`,onClick:r[3]||=e=>w.value++},` Next → `,8,he)])):y(``,!0),c(`div`,q,[c(`button`,{type:`button`,class:`rounded-md px-4 py-2 text-sm font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50`,onClick:r[4]||=e=>t.$emit(`close`)},` Cancel `),c(`button`,{type:`button`,disabled:!D.value,class:`rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50`,onClick:A},` Select `,8,J)])])])]))}}),X={class:`block text-[13px] font-semibold text-[#3D3D52]`},Z={key:0,class:`ml-0.5 text-red-500`,"aria-hidden":`true`},ge={key:0,class:`mt-1 flex flex-col gap-2 rounded-md border border-gray-200 p-3`},Q={class:`flex items-center gap-3`},_e=[`src`,`alt`],ve={key:1,class:`flex h-16 w-16 items-center justify-center rounded bg-gray-100 text-2xl text-gray-400`,"aria-hidden":`true`},ye={class:`min-w-0 flex-1`},be={class:`truncate text-sm font-medium text-gray-800`},xe={class:`text-xs text-gray-500`},Se={key:0},Ce=[`for`],we=[`id`],Te={key:1,class:`mt-1 rounded-md border border-gray-200 p-3`},Ee={class:`flex items-center gap-2`},De={class:`h-2 flex-1 overflow-hidden rounded-full bg-gray-100`},Oe={class:`w-8 shrink-0 text-right text-xs text-gray-500`},ke={key:2,class:`mt-1 rounded-md border border-gray-200 p-3`},Ae={class:`mb-2 truncate text-[13px] font-semibold text-[#3D3D52]`},je=[`for`],Me=[`id`],$={class:`mt-3 flex justify-end gap-2`},Ne=[`disabled`],Pe={key:3,class:`mt-1 flex gap-2`},Fe=[`accept`],Ie=[`disabled`],Le=[`disabled`],Re={key:4,class:`mt-1 text-sm text-red-600`,role:`alert`},ze={key:5,class:`mt-1 text-sm text-red-600`,role:`alert`},Be=d({__name:`MediaUpload`,props:{field:{},modelValue:{},error:{},disabled:{type:Boolean}},emits:[`update:modelValue`],setup(e,{emit:r}){let i=e,o=r,s=D(),l=_(()=>{let e=i.field.ui_component;return e.component===`file-upload`?e.accepted_mime_types.join(`,`):``}),d=_(()=>{let e=i.field.field_type;return e===`image`||e===`video`||e===`file`?e:`file`}),f=_(()=>d.value!==`image`),p=t(`idle`),v=t(0),x=t(``),S=t(null),C=t(``),w=t(null),T=t(!1),E=t(null);function O(e){return e>=1e6?`${(e/1e6).toFixed(1)} MB`:e>=1e3?`${(e/1e3).toFixed(0)} KB`:`${e} B`}function te(e){return e.split(`/`).pop()??e}function A(e,t){return new Promise((n,r)=>{let i=new FormData;i.append(`file`,e),t&&i.append(`alt`,t);let a=new XMLHttpRequest;a.withCredentials=!0,a.open(`POST`,`/admin/api/media/${d.value}`),a.upload.onprogress=e=>{e.lengthComputable&&(v.value=Math.round(e.loaded/e.total*100))},a.onload=()=>{try{let e=JSON.parse(a.responseText);e.ok&&e.data?n(e.data):r(Error(e.error?.message??`Upload failed`))}catch{r(Error(`Invalid server response`))}},a.onerror=()=>r(Error(`Network error`)),a.send(i)})}async function j(e,t){let n=new URLSearchParams({type:d.value,filename:e.name,mime_type:e.type}),r=await(await fetch(`/admin/api/media/presigned-url?${n.toString()}`,{credentials:`include`})).json();if(!r.ok||!r.data)throw Error(r.error?.message??`Failed to get upload URL`);let{upload_url:i,media_id:a,method:o,fields:s}=r.data;await new Promise((t,n)=>{let r=new XMLHttpRequest;if(r.upload.onprogress=e=>{e.lengthComputable&&(v.value=Math.round(e.loaded/e.total*100))},r.onload=()=>{r.status>=200&&r.status<300?t():n(Error(`Storage upload failed (${r.status})`))},r.onerror=()=>n(Error(`Network error during upload`)),o===`POST`&&s){let t=new FormData;for(let[e,n]of Object.entries(s))t.append(e,n);t.append(`file`,e),r.open(`POST`,i),r.send(t)}else r.open(`PUT`,i),r.send(e)});let c=await(await fetch(`/admin/api/media/confirm/${a}`,{method:`POST`,credentials:`include`,headers:{"Content-Type":`application/json`},body:JSON.stringify({alt:t})})).json();if(!c.ok||!c.data)throw Error(c.error?.message??`Upload confirmation failed`);return c.data}async function M(e){p.value=`uploading`,v.value=0,x.value=``;try{let t=C.value.trim(),n=s.presignedUploads?await j(e,t):await A(e,t);w.value=n,C.value=n.alt??``,o(`update:modelValue`,n.id)}catch(e){x.value=e instanceof Error?e.message:`Upload failed`}finally{p.value=`idle`,S.value=null}}async function N(){let e=w.value;if(!e)return;let t=new XMLHttpRequest;t.withCredentials=!0,t.open(`PATCH`,`/admin/api/media/${e.id}`),t.setRequestHeader(`Content-Type`,`application/json`),t.onload=()=>{try{let e=JSON.parse(t.responseText);e.ok&&e.data&&(w.value=e.data)}catch{}},t.send(JSON.stringify({alt:C.value.trim()}))}function P(e){let t=e.target.files?.[0];t&&(S.value=t,C.value=``,e.target.value=``,f.value?p.value=`alt-collect`:M(t))}function ne(){S.value&&(f.value&&!C.value.trim()||M(S.value))}function re(){S.value=null,C.value=``,p.value=`idle`,x.value=``}function F(){w.value=null,C.value=``,o(`update:modelValue`,null)}function ie(e){w.value=e,C.value=e.alt??``,T.value=!1,o(`update:modelValue`,e.id)}return ee(async()=>{if(typeof i.modelValue!=`string`||!i.modelValue)return;let e=await fetch(`/admin/api/media/${i.modelValue}`,{credentials:`include`});if(!e.ok)return;let t=await e.json();t.ok&&t.data&&(w.value=t.data,C.value=t.data.alt??``)}),(t,r)=>(n(),u(`div`,null,[c(`label`,X,[g(b(e.field.label)+` `,1),e.field.required?(n(),u(`span`,Z,`*`)):y(``,!0)]),w.value?(n(),u(`div`,ge,[c(`div`,Q,[w.value.type===`image`?(n(),u(`img`,{key:0,src:w.value.url,alt:w.value.alt??``,class:`h-16 w-16 rounded object-cover`},null,8,_e)):(n(),u(`div`,ve,b(w.value.type===`video`?`▶`:`📄`),1)),c(`div`,ye,[c(`p`,be,b(te(w.value.url)),1),c(`p`,xe,b(O(w.value.file_size)),1)]),e.disabled?y(``,!0):(n(),u(`button`,{key:2,type:`button`,class:`shrink-0 text-lg leading-none text-gray-400 hover:text-red-500`,"aria-label":`Remove selected media`,onClick:F},` × `))]),w.value.type===`image`&&!e.disabled?(n(),u(`div`,Se,[c(`label`,{for:`${e.field.name}-alt`,class:`block text-xs text-gray-500`},[...r[5]||=[g(` Alt text `,-1),c(`span`,{class:`text-gray-400`},`(optional)`,-1)]],8,Ce),a(c(`input`,{id:`${e.field.name}-alt`,"onUpdate:modelValue":r[0]||=e=>C.value=e,type:`text`,placeholder:`Describe this image`,class:`mt-0.5 block w-full rounded-md border border-gray-300 px-2 py-1 text-sm shadow-sm focus:border-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-200`,onBlur:N},null,40,we),[[k,C.value]])])):y(``,!0)])):p.value===`uploading`?(n(),u(`div`,Te,[r[6]||=c(`p`,{class:`mb-1.5 text-xs text-gray-500`},`Uploading…`,-1),c(`div`,Ee,[c(`div`,De,[c(`div`,{class:`h-2 rounded-full bg-indigo-600 transition-all duration-150`,style:h({width:`${v.value}%`})},null,4)]),c(`span`,Oe,b(v.value)+`% `,1)])])):p.value===`alt-collect`?(n(),u(`div`,ke,[c(`p`,Ae,b(S.value?.name),1),c(`label`,{for:`${e.field.name}-alt-pre`,class:`block text-xs text-gray-600`},[...r[7]||=[g(` Alt text `,-1),c(`span`,{class:`text-red-500`,"aria-hidden":`true`},`*`,-1)]],8,je),a(c(`input`,{id:`${e.field.name}-alt-pre`,"onUpdate:modelValue":r[1]||=e=>C.value=e,type:`text`,placeholder:`Describe this file`,class:`mt-0.5 block w-full rounded-md border border-gray-300 px-2 py-1 text-sm shadow-sm focus:border-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-200`},null,8,Me),[[k,C.value]]),c(`div`,$,[c(`button`,{type:`button`,class:`rounded-md px-3 py-1.5 text-sm text-gray-600 ring-1 ring-inset ring-gray-300 hover:bg-gray-50`,onClick:re},` Cancel `),c(`button`,{type:`button`,disabled:!C.value.trim(),class:`rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50`,onClick:ne},` Upload `,8,Ne)])])):(n(),u(`div`,Pe,[c(`input`,{ref_key:`fileInputRef`,ref:E,type:`file`,class:`hidden`,accept:l.value,onChange:P},null,40,Fe),c(`button`,{type:`button`,disabled:e.disabled,class:`rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50`,onClick:r[2]||=e=>E.value?.click()},` Upload `,8,Ie),c(`button`,{type:`button`,disabled:e.disabled,class:`rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50`,onClick:r[3]||=e=>T.value=!0},` Select from library `,8,Le)])),x.value?(n(),u(`p`,Re,b(x.value),1)):y(``,!0),e.error?(n(),u(`p`,ze,b(e.error),1)):y(``,!0),T.value?(n(),m(Y,{key:6,"accepted-type":d.value,onSelect:ie,onClose:r[4]||=e=>T.value=!1},null,8,[`accepted-type`])):y(``,!0)]))}}),Ve={class:`block text-[13px] font-semibold text-[#3D3D52]`},He={key:0,class:`ml-0.5 text-red-500`,"aria-hidden":`true`},Ue={key:0,class:`mt-1.5 flex flex-wrap gap-1.5`},We=[`disabled`,`aria-label`,`onClick`],Ge={key:1,class:`mt-1.5`},Ke={class:`inline-flex items-center gap-1 rounded-full bg-indigo-100 px-2.5 py-0.5 text-xs font-medium text-indigo-800`},qe=[`disabled`],Je={key:2,class:`mt-0.5 text-xs text-gray-500`},Ye={class:`relative mt-1`},Xe=[`id`,`value`,`disabled`,`placeholder`],Ze={key:0,role:`listbox`,class:`absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-sm shadow-lg ring-1 ring-black/5 focus:outline-none`},Qe={key:0,class:`flex items-center gap-2 px-3 py-2 text-gray-400`},$e={key:0,class:`px-3 py-2 text-gray-500`},et=[`onClick`],tt={key:3,class:`mt-1 text-sm text-red-600`,role:`alert`},nt=d({__name:`ReferenceSelect`,props:{field:{},modelValue:{},error:{},disabled:{type:Boolean}},emits:[`update:modelValue`],setup(e,{emit:r}){let a=e,o=r,s=te(),d=v(),p=_(()=>{let e=a.field.ui_component;return e.component===`typeahead-select`?e:null}),m=_(()=>p.value?.ref??``),h=_(()=>p.value?.rel??`one-to-one`),x=_(()=>a.field.validation.max_items??null),S=_(()=>h.value!==`one-to-one`),w=_(()=>!!d.contentTypes[m.value]),E=_(()=>(w.value?d.contentTypes[m.value]:d.taxonomyTypes[m.value])?.fields.find(e=>e.field_type===`text/plain`)?.name??`id`),D=_(()=>S.value?Array.isArray(a.modelValue)?a.modelValue.filter(e=>typeof e==`string`):[]:typeof a.modelValue==`string`?[a.modelValue]:[]),O=_(()=>x.value!==null&&D.value.length>=x.value),k=_(()=>D.value[0]??``),A=t({}),j=t(``),M=t(!1),N=t([]),P=t(!1);function ne(){return w.value?`/content/${m.value}`:`/taxonomy/${m.value}`}function re(e){let t=new URLSearchParams({[`filters[${E.value}][like]`]:e,per_page:`10`});return`${ne()}?${t.toString()}`}function F(e){return String(e[E.value]??e.id??``)}let ie=C(async e=>{if(e.length<2){N.value=[],P.value=!1;return}M.value=!0,P.value=!0;let t=await s.get(re(e));M.value=!1,t.ok&&(N.value=t.data)},300);function ae(e){j.value=e.target.value,ie(j.value)}function oe(){setTimeout(()=>{P.value=!1},150)}function se(){j.value.length>=2&&(P.value=!0)}function I(e){let t=String(e.id);if(A.value[t]=F(e),j.value=``,N.value=[],P.value=!1,S.value){let e=[...D.value];e.includes(t)||e.push(t),o(`update:modelValue`,e)}else o(`update:modelValue`,t)}function L(e){S.value?o(`update:modelValue`,D.value.filter(t=>t!==e)):o(`update:modelValue`,null)}return ee(async()=>{for(let e of D.value){if(A.value[e])continue;let t=await s.get(`${ne()}/${e}`);t.ok?A.value[e]=F(t.data):A.value[e]=e}}),(t,r)=>(n(),u(`div`,null,[c(`label`,Ve,[g(b(e.field.label)+` `,1),e.field.required?(n(),u(`span`,He,`*`)):y(``,!0)]),S.value&&D.value.length?(n(),u(`div`,Ue,[(n(!0),u(f,null,i(D.value,t=>(n(),u(`span`,{key:t,class:`inline-flex items-center gap-1 rounded-full bg-indigo-100 px-2.5 py-0.5 text-xs font-medium text-indigo-800`},[g(b(A.value[t]??t)+` `,1),c(`button`,{type:`button`,disabled:e.disabled,class:`ml-0.5 flex h-3.5 w-3.5 items-center justify-center rounded-full hover:bg-indigo-200 disabled:cursor-not-allowed`,"aria-label":`Remove ${A.value[t]??t}`,onClick:e=>L(t)},[...r[2]||=[c(`span`,{"aria-hidden":`true`},`×`,-1)]],8,We)]))),128))])):!S.value&&D.value.length?(n(),u(`div`,Ge,[c(`span`,Ke,[g(b(A.value[k.value]??k.value)+` `,1),c(`button`,{type:`button`,disabled:e.disabled,class:`ml-0.5 flex h-3.5 w-3.5 items-center justify-center rounded-full hover:bg-indigo-200 disabled:cursor-not-allowed`,"aria-label":`Clear selection`,onClick:r[0]||=e=>L(k.value)},[...r[3]||=[c(`span`,{"aria-hidden":`true`},`×`,-1)]],8,qe)])])):y(``,!0),x.value===null?y(``,!0):(n(),u(`p`,Je,b(D.value.length)+` / `+b(x.value)+` selected `,1)),c(`div`,Ye,[c(`input`,{id:e.field.name,type:`text`,value:j.value,disabled:e.disabled||O.value,placeholder:O.value?`Limit reached`:`Search ${e.field.label}…`,class:l([`block w-full rounded-[11px] border px-3.5 py-3 text-sm focus:outline-none focus:ring-[3px]`,e.error?`border-red-300 focus:border-red-400 focus:ring-red-200`:`border-[#E4E3EE] bg-[#FBFBFD] focus:border-indigo-400 focus:bg-white focus:ring-indigo-50`,(e.disabled||O.value)&&`cursor-not-allowed bg-gray-50 opacity-60`]),autocomplete:`off`,onInput:ae,onFocus:se,onFocusout:oe},null,42,Xe),P.value?(n(),u(`ul`,Ze,[M.value?(n(),u(`li`,Qe,[...r[4]||=[c(`span`,{class:`inline-block h-3.5 w-3.5 animate-spin rounded-full border-2 border-indigo-500 border-t-transparent`,"aria-hidden":`true`},null,-1),g(` Searching… `,-1)]])):(n(),u(f,{key:1},[N.value.length===0?(n(),u(`li`,$e,` No results found for "`+b(j.value)+`" `,1)):y(``,!0),(n(!0),u(f,null,i(N.value,e=>(n(),u(`li`,{key:String(e.id),role:`option`,class:`cursor-pointer px-3 py-2 hover:bg-indigo-50`,onMousedown:r[1]||=T(()=>{},[`prevent`]),onClick:t=>I(e)},b(F(e)),41,et))),128))],64))])):y(``,!0)]),e.error?(n(),u(`p`,tt,b(e.error),1)):y(``,!0)]))}}),rt={class:`block text-[13px] font-semibold text-[#3D3D52]`},it={key:0,class:`ml-0.5 text-red-500`,"aria-hidden":`true`},at={class:`mt-1 space-y-2`},ot=[`onDragstart`,`onDragover`,`onDrop`],st={class:`flex items-center gap-2 border-b border-gray-100 px-3 py-2`},ct={class:`text-xs font-medium text-gray-500`},lt=[`aria-label`,`onClick`],ut={class:`p-3`},dt={key:1,class:`mt-1 text-sm text-red-600`,role:`alert`},ft=d({__name:`ParagraphEmbed`,props:{field:{},modelValue:{},error:{},disabled:{type:Boolean},formComponent:{}},emits:[`update:modelValue`],setup(r,{emit:a}){let o=r,s=a,d=_(()=>Array.isArray(o.modelValue)?o.modelValue:[]),p=t(null),h=t(null);function v(){let e=[...d.value];e.push({order:e.length}),s(`update:modelValue`,e)}function x(e){let t=[...d.value];t.splice(e,1),s(`update:modelValue`,t.map((e,t)=>({...e,order:t})))}function ee(e,t){let n=[...d.value];n[e]={...t,order:e},s(`update:modelValue`,n)}function S(e){p.value=e}function C(e){h.value=e}function w(e){if(p.value===null||p.value===e){p.value=null,h.value=null;return}let t=[...d.value],[n]=t.splice(p.value,1);n!==void 0&&(t.splice(e,0,n),s(`update:modelValue`,t.map((e,t)=>({...e,order:t}))),p.value=null,h.value=null)}function E(){p.value=null,h.value=null}return(t,a)=>(n(),u(`div`,null,[c(`label`,rt,[g(b(r.field.label)+` `,1),r.field.required?(n(),u(`span`,it,`*`)):y(``,!0)]),c(`div`,at,[(n(!0),u(f,null,i(d.value,(t,i)=>(n(),u(`div`,{key:i,draggable:`true`,class:l([`rounded-md border bg-white transition-colors`,h.value===i&&p.value!==i?`border-indigo-400 ring-2 ring-indigo-200`:`border-gray-200`,r.disabled&&`opacity-60`]),onDragstart:e=>S(i),onDragover:T(e=>C(i),[`prevent`]),onDrop:T(e=>w(i),[`prevent`]),onDragend:E},[c(`div`,st,[a[0]||=c(`span`,{class:`cursor-grab select-none text-gray-400`,title:`Drag to reorder`,"aria-hidden":`true`},` ⋮⋮ `,-1),c(`span`,ct,` Item `+b(i+1),1),r.disabled?y(``,!0):(n(),u(`button`,{key:0,type:`button`,class:`ml-auto text-xs text-red-500 hover:text-red-700`,"aria-label":`Remove item ${i+1}`,onClick:e=>x(i)},` Remove `,8,lt))]),c(`div`,ut,[(n(),m(e(r.formComponent),{"model-value":t,disabled:r.disabled,"onUpdate:modelValue":e=>ee(i,e)},null,8,[`model-value`,`disabled`,`onUpdate:modelValue`]))])],42,ot))),128))]),r.disabled?y(``,!0):(n(),u(`button`,{key:0,type:`button`,class:`mt-2 inline-flex items-center gap-1.5 rounded-md border border-dashed border-gray-300 px-3 py-2 text-sm text-gray-600 hover:border-indigo-400 hover:text-indigo-600`,onClick:v},[a[1]||=c(`span`,{"aria-hidden":`true`},`+`,-1),g(` Add `+b(r.field.label),1)])),r.error?(n(),u(`p`,dt,b(r.error),1)):y(``,!0)]))}}),pt={key:0,class:`flex items-center justify-center py-20`},mt={key:1,class:`py-12 text-center text-sm text-gray-500`},ht={key:2},gt={class:`mb-[22px] flex flex-wrap items-start justify-between gap-4`},_t={class:`mb-1.5 text-[11px] font-bold uppercase tracking-[.06em] text-faint`},vt={class:`text-[25px] font-bold tracking-tight text-ink`},yt={class:`flex shrink-0 flex-wrap items-center gap-[9px]`},bt=[`disabled`],xt=[`disabled`],St=[`disabled`],Ct=[`disabled`],wt={key:0,class:`mb-[18px] rounded-2xl border border-card-border bg-white p-5 shadow-[0_1px_2px_rgba(24,24,48,0.04),0_8px_22px_rgba(24,24,48,0.04)]`},Tt={class:`mt-2 flex flex-wrap items-center gap-[10px]`},Et=[`disabled`],Dt={key:0,class:`mt-[9px] text-[12.5px] text-[#D9913C]`},Ot={key:1,class:`mt-1 text-sm text-red-600`,role:`alert`},kt=[`data-tab`,`onClick`],At={class:`space-y-5`},jt={key:2,class:`mt-4 text-sm text-red-600`,role:`alert`},Mt=d({__name:`ContentFormView`,setup(s){let C=O(),T=w(),D=te(),{can:se}=E(),{errors:I,validate:L,mergeServerErrors:ce,clearErrors:le,setError:ue}=ne(),de=F(),fe=v(),pe=ie(),R=_(()=>C.params.type),z=_(()=>C.params.id),B=_(()=>fe.contentTypes[R.value]),V=_(()=>B.value?.fields??[]),H=_(()=>B.value?.ui.tabs??[]),me=_(()=>B.value?.only_one===!0),U=_(()=>me.value?`singleton`:z.value?`edit`:`create`),W=t(null),G=t({}),K=t(!1),he=t(``),q=t(!1),J=t(!0),Y=t(!1),X=t(``),Z=t(!1),ge=t(!1),Q=t(null),_e=t(0),ve=_(()=>H.value[_e.value]?.name??``),ye=t(null),{left:be,width:xe}=oe(ye,ve),Se=_(()=>{if(H.value.length===0)return V.value;let e=H.value[_e.value];return e?e.fields.map(e=>V.value.find(t=>t.name===e)).filter(e=>e!==void 0):V.value}),Ce=_(()=>U.value===`edit`&&K.value&&G.value.slug!==he.value),we={"text/plain":r(N),"text/rich":r(A),integer:r(j),float:r(j),boolean:r(P),date:r(M),image:r(Be),video:r(Be),file:r(Be),enum:r(re),reference:r(nt),paragraph:r(ft)};function Te(e){return we[e.field_type]??we[`text/plain`]}let Ee=new Map;function De(e){if(Ee.has(e))return Ee.get(e);let t=r(d({name:`ParagraphForm_${e}`,props:{modelValue:{type:Object,default:()=>({})},disabled:{type:Boolean,default:!1}},emits:[`update:modelValue`],setup(t,{emit:n}){let r=v(),i=_(()=>r.paragraphTypes[e]);function a(e,r){n(`update:modelValue`,{...t.modelValue,[e]:r})}return()=>i.value?x(`div`,{class:`space-y-3`},i.value.fields.map(e=>x(we[e.field_type]??we[`text/plain`],{key:e.name,field:e,modelValue:(t.modelValue??{})[e.name],disabled:t.disabled,"onUpdate:modelValue":t=>a(e.name,t)}))):x(`div`,{class:`text-sm text-gray-400`},`Unknown paragraph type`)}}));return Ee.set(e,t),t}function Oe(e){return e.ui_component.component===`paragraph-embed`?{formComponent:De(e.ui_component.ref)}:{}}function ke(e){switch(e.field_type){case`text/plain`:case`text/rich`:return``;case`integer`:case`float`:return null;case`boolean`:return!1;case`date`:return null;case`image`:case`video`:case`file`:return null;case`enum`:return``;case`reference`:return(e.ui_component.component===`typeahead-select`?e.ui_component.rel:`one-to-one`)===`one-to-one`?null:[];case`paragraph`:return[];default:return null}}function Ae(e){let t={};me.value||(t.slug=e?.slug??``);for(let n of V.value)t[n.name]=e?.[n.name]??ke(n);G.value=t,K.value=e?.published===!0,e?.slug&&(he.value=String(e.slug))}function je(e,t){G.value[e]=t}async function Me(e){let t=await D.get(`/content/${R.value}/${e}`);t.ok?(W.value=t.data,Ae(t.data)):X.value=t.error.message}ee(async()=>{if(!B.value){J.value=!1;return}U.value===`edit`&&z.value||U.value===`singleton`&&z.value?await Me(z.value):Ae(),J.value=!1}),p(z,async(e,t)=>{e!==t&&(J.value=!0,q.value=!1,le(),e?await Me(e):(W.value=null,Ae()),J.value=!1)});async function $(e){Y.value=!0,X.value=``;let t={...G.value};e!==void 0&&(t.published=e);let n;if(n=U.value===`create`?await D.post(`/content/${R.value}`,t):U.value===`singleton`?await D.put(`/content/${R.value}`,t):await D.patch(`/content/${R.value}/${z.value}`,t),Y.value=!1,!n.ok){let e=n.error;e.code===`PUBLISH_VALIDATION_ERROR`&&e.details?ce(e.details):e.code===`SLUG_CONFLICT`?ue(`slug`,`This slug is already taken.`):X.value=e.message;return}W.value=n.data,K.value=n.data.published===!0,he.value=String(n.data.slug??``),q.value=!1,pe.invalidate(R.value),de.success(`Saved.`),U.value===`create`&&T.replace({name:`content-edit`,params:{type:R.value,id:String(n.data.id)}})}function Ne(){if(L(V.value,G.value)){if(Ce.value){Q.value=()=>$(),Z.value=!0;return}$()}}function Pe(){if(L(V.value,G.value,!0)){if(Ce.value){Q.value=()=>$(!0),Z.value=!0;return}$(!0)}}function Fe(){$(!1)}function Ie(){Z.value=!1,Q.value&&=(Q.value(),null)}function Le(){Z.value=!1,Q.value=null}async function Re(){ge.value=!1,Y.value=!0;let e=await D.del(`/content/${R.value}/${z.value}`);if(Y.value=!1,!e.ok){X.value=e.error.message;return}pe.invalidate(R.value),T.push({name:`content-list`,params:{type:R.value}})}let ze=_(()=>{if(U.value===`create`)return`New ${B.value?.label??``}`;if(U.value===`singleton`)return B.value?.label??``;let e=V.value.find(e=>e.field_type===`text/plain`);return(e?String(W.value?.[e.name]??``):``)||(B.value?.label??``)});return(t,r)=>J.value?(n(),u(`div`,pt,[...r[3]||=[c(`span`,{class:`inline-block h-6 w-6 animate-spin rounded-full border-2 border-indigo-600 border-t-transparent`,"aria-hidden":`true`},null,-1)]])):B.value?(n(),u(`div`,ht,[c(`div`,gt,[c(`div`,null,[c(`p`,_t,b(B.value.label),1),c(`h1`,vt,b(ze.value),1)]),c(`div`,yt,[o(se)(`content:delete`)&&U.value===`edit`?(n(),u(`button`,{key:0,type:`button`,disabled:Y.value,class:`inline-flex items-center gap-1.5 rounded-[10px] border border-[#F3D2D7] bg-white px-[15px] py-2.5 text-[13px] font-semibold text-[#E1495B] transition-colors hover:bg-[#FDF2F4] disabled:cursor-not-allowed disabled:opacity-50`,onClick:r[0]||=e=>ge.value=!0},` Delete `,8,bt)):y(``,!0),o(se)(`content:edit`)?(n(),u(f,{key:1},[K.value?(n(),u(`button`,{key:0,type:`button`,disabled:Y.value,class:`rounded-[10px] border border-[#E4E3EE] bg-white px-4 py-2.5 text-[13px] font-semibold text-[#3D3D52] transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-50`,onClick:Fe},` Unpublish `,8,xt)):(n(),u(`button`,{key:1,type:`button`,disabled:Y.value,class:`rounded-[10px] border border-[#E4E3EE] bg-white px-4 py-2.5 text-[13px] font-semibold text-[#3D3D52] transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-50`,onClick:Pe},` Publish `,8,St)),c(`button`,{type:`button`,disabled:Y.value,class:`inline-flex items-center gap-1.5 rounded-[10px] bg-indigo-600 px-4 py-2.5 text-[13px] font-semibold text-white shadow-[0_3px_10px_rgba(91,87,232,0.3)] transition-all hover:bg-indigo-700 hover:shadow-[0_6px_18px_rgba(91,87,232,0.4)] disabled:cursor-not-allowed disabled:opacity-50`,onClick:Ne},b(Y.value?`Saving…`:`Save draft`),9,Ct)],64)):y(``,!0)])]),me.value?y(``,!0):(n(),u(`div`,wt,[r[4]||=c(`label`,{for:`slug-field`,class:`block text-[13px] font-semibold text-[#3D3D52]`},[g(` Slug `),c(`span`,{class:`ml-0.5 text-red-500`,"aria-hidden":`true`},`*`)],-1),c(`div`,Tt,[a(c(`input`,{id:`slug-field`,"onUpdate:modelValue":r[1]||=e=>G.value.slug=e,type:`text`,disabled:Y.value||K.value&&!q.value,class:l([`block min-w-[180px] flex-1 rounded-[10px] border px-[13px] py-[11px] font-mono text-sm focus:outline-none focus:ring-2`,o(I).slug?`border-red-300 focus:border-red-400 focus:ring-red-200`:`border-[#E4E3EE] bg-surface focus:border-indigo-400 focus:ring-indigo-200`,(Y.value||K.value&&!q.value)&&`cursor-not-allowed bg-gray-100 opacity-70`]),placeholder:`url-slug`},null,10,Et),[[k,G.value.slug]]),K.value&&!q.value?(n(),u(`button`,{key:0,type:`button`,class:`shrink-0 rounded-[10px] border border-[#E4E3EE] bg-white px-4 py-2.5 text-sm font-semibold text-[#3D3D52] transition-colors hover:bg-surface`,onClick:r[2]||=e=>q.value=!0},` Edit slug `)):y(``,!0)]),K.value?(n(),u(`p`,Dt,` Changing this slug will break existing links. `)):y(``,!0),o(I).slug?(n(),u(`p`,Ot,b(o(I).slug),1)):y(``,!0)])),H.value.length>1?(n(),u(`div`,{key:1,ref_key:`tabBarRef`,ref:ye,class:`relative mb-[22px] flex gap-0 overflow-x-auto border-b border-card-border`},[(n(!0),u(f,null,i(H.value,(e,t)=>(n(),u(`button`,{key:e.name,"data-tab":e.name,type:`button`,class:l([`mr-6 whitespace-nowrap bg-none px-0.5 py-[11px] text-sm transition-colors`,_e.value===t?`font-semibold text-indigo-600`:`font-medium text-faint hover:text-[#8A8A9E]`]),onClick:e=>_e.value=t},b(e.label),11,kt))),128)),c(`div`,{class:`absolute -bottom-px h-0.5 rounded-full bg-indigo-600 transition-[left,width] duration-300`,style:h({left:`${o(be)}px`,width:`${o(xe)}px`})},null,4)],512)):y(``,!0),c(`div`,At,[(n(!0),u(f,null,i(Se.value,t=>(n(),m(e(Te(t)),S({key:t.name,field:t,"model-value":G.value[t.name],error:o(I)[t.name],disabled:Y.value},{ref_for:!0},Oe(t),{"onUpdate:modelValue":e=>je(t.name,e)}),null,16,[`field`,`model-value`,`error`,`disabled`,`onUpdate:modelValue`]))),128))]),X.value?(n(),u(`p`,jt,b(X.value),1)):y(``,!0),Z.value?(n(),m(ae,{key:3,message:`This item is published. Changing its slug will break existing links. Continue?`,"on-confirm":Ie,"on-cancel":Le})):y(``,!0),ge.value?(n(),m(ae,{key:4,message:`Are you sure you want to delete this item? This action cannot be undone.`,"on-confirm":Re,"on-cancel":()=>ge.value=!1},null,8,[`on-cancel`])):y(``,!0)])):(n(),u(`div`,mt,` Content type not found. `))}});export{Mt as default};
@@ -1 +0,0 @@
1
- import{A as e,B as t,E as n,O as r,W as i,c as a,d as o,g as s,i as ee,j as c,l,s as u,t as te,u as d,ut as f,w as ne}from"./schema-B-RKX_ep.js";import{c as re,n as ie,s as ae,t as oe}from"./index-BkazhAun.js";import{a as se,i as p,n as m,o as h,r as g,s as _,t as v}from"./EnumSelect-BOrBkA76.js";import{t as ce}from"./useNotification-DU6J5vms.js";import{t as y}from"./ConfirmDialog-B7nxdDge.js";import{t as le}from"./taxonomy-DC-6wyg3.js";var ue={key:0,class:`flex items-center justify-center py-20`},de={key:1,class:`py-12 text-center text-sm text-gray-500`},b={key:2},x={class:`mb-6 flex items-start justify-between gap-4`},S={class:`text-xs font-medium uppercase tracking-wide text-gray-400`},C={class:`text-[26px] font-bold tracking-tight text-ink`},w={class:`flex shrink-0 items-center gap-2`},T=[`disabled`],E=[`disabled`],D={class:`space-y-5`},O={key:0,class:`mt-4 text-sm text-red-600`,role:`alert`},k=s({__name:`TaxonomyFormView`,setup(s){let k=ae(),A=re(),j=oe(),{can:M}=ie(),{errors:N,validate:P,mergeServerErrors:F,clearErrors:I,setError:L}=_(),R=ce(),z=te(),B=le(),V=u(()=>k.params.type),H=u(()=>k.params.id),U=u(()=>!!H.value),W=u(()=>z.taxonomyTypes[V.value]),G=u(()=>W.value?.fields??[]),K=t(null),q=t({}),J=t(!0),Y=t(!1),X=t(``),Z=t(!1),fe={"text/plain":h,"text/rich":se,integer:p,float:p,boolean:g,date:m,enum:v};function pe(e){let t=e.field_type;return fe[t]??h}function me(e){switch(e.field_type){case`text/plain`:case`text/rich`:return``;case`integer`:case`float`:return null;case`boolean`:return!1;case`date`:return null;case`enum`:return``;default:return null}}function Q(e){let t={};for(let n of G.value)t[n.name]=e?.[n.name]??me(n);q.value=t}function he(e,t){q.value[e]=t}async function $(e){let t=await j.get(`/taxonomy/${V.value}/${e}`);t.ok?(K.value=t.data,Q(t.data)):X.value=t.error.message}ne(async()=>{if(!W.value){J.value=!1;return}U.value&&H.value?await $(H.value):Q(),J.value=!1}),c(H,async(e,t)=>{e!==t&&(J.value=!0,I(),e?await $(e):(K.value=null,Q()),J.value=!1)});async function ge(){if(!P(G.value,q.value))return;Y.value=!0,X.value=``;let e=U.value?await j.patch(`/taxonomy/${V.value}/${H.value}`,q.value):await j.post(`/taxonomy/${V.value}`,q.value);if(Y.value=!1,!e.ok){let t=e.error;t.code===`PUBLISH_VALIDATION_ERROR`&&t.details?F(t.details):t.code===`SLUG_CONFLICT`?L(`slug`,`This slug is already taken.`):X.value=t.message;return}K.value=e.data,B.invalidate(V.value),R.success(`Saved.`),U.value||A.replace({name:`taxonomy-edit`,params:{type:V.value,id:String(e.data.id)}})}async function _e(){Z.value=!1,Y.value=!0;let e=await j.del(`/taxonomy/${V.value}/${H.value}`);if(Y.value=!1,!e.ok){X.value=e.error.message;return}B.invalidate(V.value),A.push({name:`taxonomy-list`,params:{type:V.value}})}let ve=u(()=>{if(!U.value)return`New ${W.value?.label??``}`;let e=G.value.find(e=>e.field_type===`text/plain`);return e?String(K.value?.[e.name]??``):W.value?.label??``});return(t,s)=>J.value?(n(),o(`div`,ue,[...s[1]||=[a(`span`,{class:`inline-block h-6 w-6 animate-spin rounded-full border-2 border-indigo-600 border-t-transparent`,"aria-hidden":`true`},null,-1)]])):W.value?(n(),o(`div`,b,[a(`div`,x,[a(`div`,null,[a(`p`,S,f(W.value.label),1),a(`h1`,C,f(ve.value),1)]),a(`div`,w,[i(M)(`taxonomy:delete`)&&U.value?(n(),o(`button`,{key:0,type:`button`,disabled:Y.value,class:`rounded-md px-3 py-2 text-sm font-medium text-red-600 ring-1 ring-inset ring-red-300 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-50`,onClick:s[0]||=e=>Z.value=!0},` Delete `,8,T)):d(``,!0),(U.value?i(M)(`taxonomy:edit`):i(M)(`taxonomy:create`))?(n(),o(`button`,{key:1,type:`button`,disabled:Y.value,class:`rounded-[11px] bg-indigo-600 px-4 py-2.5 text-[13.5px] font-semibold text-white shadow-[0_3px_10px_rgba(91,87,232,0.3)] transition-all hover:bg-indigo-700 hover:shadow-[0_6px_18px_rgba(91,87,232,0.4)] disabled:cursor-not-allowed disabled:opacity-50`,onClick:ge},f(Y.value?`Saving…`:`Save`),9,E)):d(``,!0)])]),a(`div`,D,[(n(!0),o(ee,null,r(G.value,t=>(n(),l(e(pe(t)),{key:t.name,field:t,"model-value":q.value[t.name],error:i(N)[t.name],disabled:Y.value,"onUpdate:modelValue":e=>he(t.name,e)},null,8,[`field`,`model-value`,`error`,`disabled`,`onUpdate:modelValue`]))),128))]),X.value?(n(),o(`p`,O,f(X.value),1)):d(``,!0),Z.value?(n(),l(y,{key:1,message:`Are you sure you want to delete this term? This action cannot be undone.`,"on-confirm":_e,"on-cancel":()=>Z.value=!1},null,8,[`on-cancel`])):d(``,!0)])):(n(),o(`div`,de,` Taxonomy type not found. `))}});export{k as default};