@edgedev/create-edge-app 1.1.25 → 1.1.27

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 (111) hide show
  1. package/README.md +55 -20
  2. package/{agent.md → agents.md} +2 -0
  3. package/bin/cli.js +6 -6
  4. package/edge/components/auth/login.vue +384 -0
  5. package/edge/components/auth/register.vue +396 -0
  6. package/edge/components/auth.vue +108 -0
  7. package/edge/components/autoFileUpload.vue +215 -0
  8. package/edge/components/billing.vue +8 -0
  9. package/edge/components/buttonDivider.vue +14 -0
  10. package/edge/components/chip.vue +34 -0
  11. package/edge/components/clipboardButton.vue +42 -0
  12. package/edge/components/cms/block.vue +529 -0
  13. package/edge/components/cms/blockApi.vue +212 -0
  14. package/edge/components/cms/blockEditor.vue +725 -0
  15. package/edge/components/cms/blockInput.vue +66 -0
  16. package/edge/components/cms/blockPicker.vue +486 -0
  17. package/edge/components/cms/blockRender.vue +78 -0
  18. package/edge/components/cms/blockSheetContent.vue +28 -0
  19. package/edge/components/cms/codeEditor.vue +466 -0
  20. package/edge/components/cms/fontUpload.vue +327 -0
  21. package/edge/components/cms/htmlContent.vue +807 -0
  22. package/edge/components/cms/init_blocks/api_with_subarrays.html +17 -0
  23. package/edge/components/cms/init_blocks/array_with_collection.html +7 -0
  24. package/edge/components/cms/init_blocks/array_with_objects.html +7 -0
  25. package/edge/components/cms/init_blocks/carousel.html +103 -0
  26. package/edge/components/cms/init_blocks/contact_us.html +69 -0
  27. package/edge/components/cms/init_blocks/content_with_left_image.html +27 -0
  28. package/edge/components/cms/init_blocks/footer.html +24 -0
  29. package/edge/components/cms/init_blocks/header_divider.html +7 -0
  30. package/edge/components/cms/init_blocks/hero.html +35 -0
  31. package/edge/components/cms/init_blocks/hero_carousel.html +52 -0
  32. package/edge/components/cms/init_blocks/newsletter.html +117 -0
  33. package/edge/components/cms/init_blocks/post_content.html +7 -0
  34. package/edge/components/cms/init_blocks/post_title_header.html +21 -0
  35. package/edge/components/cms/init_blocks/posts_list.html +20 -0
  36. package/edge/components/cms/init_blocks/properties_showcase.html +100 -0
  37. package/edge/components/cms/init_blocks/property_carousel.html +59 -0
  38. package/edge/components/cms/init_blocks/property_detail.html +112 -0
  39. package/edge/components/cms/init_blocks/property_detail_header.html +34 -0
  40. package/edge/components/cms/init_blocks/property_results.html +137 -0
  41. package/edge/components/cms/init_blocks/property_search.html +75 -0
  42. package/edge/components/cms/init_blocks/simple_array.html +7 -0
  43. package/edge/components/cms/mediaCard.vue +116 -0
  44. package/edge/components/cms/mediaManager.vue +386 -0
  45. package/edge/components/cms/menu.vue +1103 -0
  46. package/edge/components/cms/optionsSelect.vue +107 -0
  47. package/edge/components/cms/page.vue +1785 -0
  48. package/edge/components/cms/posts.vue +1083 -0
  49. package/edge/components/cms/site.vue +1475 -0
  50. package/edge/components/cms/themeDefaultMenu.vue +548 -0
  51. package/edge/components/cms/themeEditor.vue +429 -0
  52. package/edge/components/dashboard.vue +776 -0
  53. package/edge/components/editor.vue +671 -0
  54. package/edge/components/fileTree.vue +72 -0
  55. package/edge/components/files.vue +89 -0
  56. package/edge/components/formSubtypes/myOrgs.vue +214 -0
  57. package/edge/components/formSubtypes/users.vue +336 -0
  58. package/edge/components/functionChips.vue +57 -0
  59. package/edge/components/gError.vue +98 -0
  60. package/edge/components/gHelper.vue +67 -0
  61. package/edge/components/gInput.vue +1331 -0
  62. package/edge/components/loggingIn.vue +41 -0
  63. package/edge/components/menu.vue +137 -0
  64. package/edge/components/menuContent.vue +132 -0
  65. package/edge/components/myAccount.vue +317 -0
  66. package/edge/components/myOrganizations.vue +75 -0
  67. package/edge/components/myProfile.vue +122 -0
  68. package/edge/components/orgSwitcher.vue +25 -0
  69. package/edge/components/organizationMembers.vue +522 -0
  70. package/edge/components/organizationSettings.vue +271 -0
  71. package/edge/components/shad/breadcrumbs.vue +35 -0
  72. package/edge/components/shad/button.vue +43 -0
  73. package/edge/components/shad/checkbox.vue +73 -0
  74. package/edge/components/shad/combobox.vue +238 -0
  75. package/edge/components/shad/datepicker.vue +184 -0
  76. package/edge/components/shad/dialog.vue +32 -0
  77. package/edge/components/shad/dropdownMenu.vue +54 -0
  78. package/edge/components/shad/dropdownMenuItem.vue +21 -0
  79. package/edge/components/shad/form.vue +59 -0
  80. package/edge/components/shad/html.vue +877 -0
  81. package/edge/components/shad/input.vue +139 -0
  82. package/edge/components/shad/number.vue +109 -0
  83. package/edge/components/shad/select.vue +151 -0
  84. package/edge/components/shad/selectTags.vue +278 -0
  85. package/edge/components/shad/switch.vue +67 -0
  86. package/edge/components/shad/tags.vue +137 -0
  87. package/edge/components/shad/textarea.vue +102 -0
  88. package/edge/components/shad/typeMoney.vue +167 -0
  89. package/edge/components/sideBar.vue +288 -0
  90. package/edge/components/sideBarContent.vue +268 -0
  91. package/edge/components/sidebarProvider.vue +33 -0
  92. package/edge/components/tooltip.vue +16 -0
  93. package/edge/components/userMenu.vue +148 -0
  94. package/edge/components/v/alert.vue +59 -0
  95. package/edge/components/v/alertTitle.vue +18 -0
  96. package/edge/components/v/card.vue +53 -0
  97. package/edge/components/v/cardActions.vue +18 -0
  98. package/edge/components/v/cardText.vue +18 -0
  99. package/edge/components/v/cardTitle.vue +20 -0
  100. package/edge/components/v/col.vue +56 -0
  101. package/edge/components/v/list.vue +46 -0
  102. package/edge/components/v/listItem.vue +26 -0
  103. package/edge/components/v/listItemTitle.vue +18 -0
  104. package/edge/components/v/row.vue +42 -0
  105. package/edge/components/v/toolbar.vue +24 -0
  106. package/edge/composables/global.ts +519 -0
  107. package/edge-pull.sh +2 -0
  108. package/edge-push.sh +1 -0
  109. package/edge-status.sh +14 -0
  110. package/package.json +1 -1
  111. package/edge-components-install.sh +0 -1
@@ -0,0 +1,1083 @@
1
+ <script setup lang="js">
2
+ import { computed, inject, onBeforeMount, reactive, ref, watch } from 'vue'
3
+ import { toTypedSchema } from '@vee-validate/zod'
4
+ import * as z from 'zod'
5
+ import { File, FileCheck, FilePen, FileWarning, Image, ImagePlus, Loader2, MoreHorizontal, Plus, Save, Trash2, X } from 'lucide-vue-next'
6
+
7
+ const props = defineProps({
8
+ site: {
9
+ type: String,
10
+ required: true,
11
+ },
12
+ mode: {
13
+ type: String,
14
+ default: 'sidebar',
15
+ },
16
+ selectedPostId: {
17
+ type: String,
18
+ default: '',
19
+ },
20
+ listVariant: {
21
+ type: String,
22
+ default: 'sidebar',
23
+ },
24
+ })
25
+
26
+ const emit = defineEmits(['updating', 'update:selectedPostId'])
27
+
28
+ const edgeFirebase = inject('edgeFirebase')
29
+
30
+ const collection = computed(() => `sites/${props.site}/posts`)
31
+ const collectionKey = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/${collection.value}`)
32
+
33
+ const publishedCollection = computed(() => `sites/${props.site}/published_posts`)
34
+ const publishedCollectionKey = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/${publishedCollection.value}`)
35
+
36
+ const schemas = {
37
+ posts: toTypedSchema(z.object({
38
+ name: z.string({
39
+ required_error: 'Name is required',
40
+ }).min(1, { message: 'Name is required' }),
41
+ title: z.string({
42
+ required_error: 'Title is required',
43
+ }).min(1, { message: 'Title is required' }),
44
+ tags: z.array(z.string()).optional(),
45
+ blurb: z.string({
46
+ required_error: 'Content blurb is required',
47
+ }).min(1, { message: 'Content blurb is required' }).max(500, { message: 'Content blurb must be at most 500 characters' }),
48
+ content: z.string({
49
+ required_error: 'Content is required',
50
+ }).min(1, { message: 'Content is required' }),
51
+ featuredImages: z.array(z.string()).optional(),
52
+ })),
53
+ }
54
+
55
+ const renameSchema = toTypedSchema(z.object({
56
+ name: z.string({
57
+ required_error: 'Name is required',
58
+ }).min(1, { message: 'Name is required' }),
59
+ }))
60
+
61
+ const isPublishedPostDiff = (postId) => {
62
+ const publishedPost = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published_posts`]?.[postId]
63
+ const draftPost = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/posts`]?.[postId]
64
+ if (!publishedPost && draftPost) {
65
+ return true
66
+ }
67
+ if (publishedPost && !draftPost) {
68
+ return true
69
+ }
70
+ if (publishedPost && draftPost) {
71
+ return JSON.stringify({ name: publishedPost.name, content: publishedPost.content, blurb: publishedPost.blurb, tags: publishedPost.tags, title: publishedPost.title, featuredImages: publishedPost.featuredImages }) !== JSON.stringify({ name: draftPost.name, content: draftPost.content, blurb: draftPost.blurb, tags: draftPost.tags, title: draftPost.title, featuredImages: draftPost.featuredImages })
72
+ }
73
+ return false
74
+ }
75
+
76
+ const lastPublishedTime = (postId) => {
77
+ const timestamp = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`]?.[postId]?.last_updated
78
+ if (!timestamp)
79
+ return 'Never'
80
+ const date = new Date(timestamp)
81
+ return date.toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })
82
+ }
83
+
84
+ const state = reactive({
85
+ sheetOpen: false,
86
+ activePostId: '',
87
+ deleteDialog: false,
88
+ postToDelete: null,
89
+ editorDoc: null,
90
+ internalSlugUpdate: false,
91
+ slugManuallyEdited: false,
92
+ lastAutoSlug: '',
93
+ renameDialog: false,
94
+ renamePost: null,
95
+ renameValue: '',
96
+ renameSubmitting: false,
97
+ renameInternalUpdate: false,
98
+ contentImageDialog: false,
99
+ newDocs: {
100
+ posts: {
101
+ name: {
102
+ value: '',
103
+ cols: '12',
104
+ bindings: {
105
+ 'field-type': 'text',
106
+ 'label': 'Name',
107
+ },
108
+ },
109
+ title: {
110
+ value: '',
111
+ cols: '12',
112
+ bindings: {
113
+ 'field-type': 'text',
114
+ 'label': 'Title',
115
+ },
116
+ },
117
+ tags: {
118
+ value: [],
119
+ cols: '12',
120
+ bindings: {
121
+ 'field-type': 'tags',
122
+ 'value-as': 'array',
123
+ 'label': 'Tags',
124
+ 'placeholder': 'Add a tag',
125
+ },
126
+ },
127
+ blurb: {
128
+ value: '',
129
+ cols: '12',
130
+ bindings: {
131
+ 'field-type': 'textarea',
132
+ 'label': 'Content Blurb / Preview',
133
+ 'rows': '8',
134
+ },
135
+ },
136
+ content: {
137
+ value: '',
138
+ cols: '12',
139
+ bindings: {
140
+ 'field-type': 'textarea',
141
+ 'label': 'Content',
142
+ 'rows': '8',
143
+ },
144
+ },
145
+ featuredImage: {
146
+ value: '',
147
+ cols: '12',
148
+ bindings: {
149
+ 'field-type': 'tags',
150
+ 'value-as': 'array',
151
+ 'label': 'Featured Images',
152
+ 'description': 'Enter image URLs or storage paths',
153
+ },
154
+ },
155
+ },
156
+ },
157
+ })
158
+
159
+ const contentEditor = ref(null)
160
+
161
+ onBeforeMount(async () => {
162
+ if (!edgeFirebase.data?.[collectionKey.value]) {
163
+ await edgeFirebase.startSnapshot(collectionKey.value)
164
+ }
165
+ if (!edgeFirebase.data?.[publishedCollectionKey.value]) {
166
+ await edgeFirebase.startSnapshot(publishedCollectionKey.value)
167
+ }
168
+ })
169
+
170
+ const posts = computed(() => edgeFirebase.data?.[collectionKey.value] || {})
171
+ const postsList = computed(() =>
172
+ Object.entries(posts.value)
173
+ .map(([id, data]) => ({ id, ...data }))
174
+ .sort((a, b) => (b.doc_created_at ?? 0) - (a.doc_created_at ?? 0)),
175
+ )
176
+ const hasPosts = computed(() => postsList.value.length > 0)
177
+ const isCreating = computed(() => state.activePostId === 'new')
178
+ const isFullList = computed(() => props.mode === 'list' && props.listVariant === 'full')
179
+
180
+ const getPostSlug = post => (post?.name && (typeof post.name === 'string' ? post.name.trim() : ''))
181
+
182
+ const slugify = (value) => {
183
+ if (!value)
184
+ return ''
185
+ return String(value)
186
+ .toLowerCase()
187
+ .replace(/[^a-z0-9]+/g, '-')
188
+ .replace(/(^-|-$)+/g, '')
189
+ }
190
+
191
+ const ensureUniqueSlug = (input, excludeId = '') => {
192
+ let base = slugify(input)
193
+ if (!base)
194
+ base = 'post'
195
+ const existing = new Set(
196
+ postsList.value
197
+ .filter(post => post.id !== excludeId)
198
+ .map(post => getPostSlug(post))
199
+ .filter(Boolean),
200
+ )
201
+
202
+ let candidate = base
203
+ let suffix = 1
204
+ while (existing.has(candidate)) {
205
+ candidate = `${base}-${suffix}`
206
+ suffix += 1
207
+ }
208
+ return candidate
209
+ }
210
+
211
+ const activePost = computed(() => {
212
+ if (!state.activePostId || state.activePostId === 'new')
213
+ return null
214
+ return posts.value?.[state.activePostId] || null
215
+ })
216
+
217
+ const editorOpen = computed(() => {
218
+ if (props.mode === 'editor')
219
+ return Boolean(props.selectedPostId)
220
+ return state.sheetOpen
221
+ })
222
+
223
+ const sheetTitle = computed(() => {
224
+ if (!editorOpen.value)
225
+ return ''
226
+ if (isCreating.value)
227
+ return 'New Post'
228
+ return activePost.value?.name || getPostSlug(activePost.value) || 'Edit Post'
229
+ })
230
+
231
+ const currentDocId = () => (state.activePostId && (state.activePostId !== 'new' ? state.activePostId : ''))
232
+
233
+ watch(
234
+ () => state.editorDoc?.title,
235
+ (newTitle) => {
236
+ if (!state.editorDoc)
237
+ return
238
+ if (state.slugManuallyEdited && state.editorDoc.name)
239
+ return
240
+ if (!newTitle) {
241
+ state.lastAutoSlug = ''
242
+ return
243
+ }
244
+ const unique = ensureUniqueSlug(newTitle, currentDocId())
245
+ state.internalSlugUpdate = true
246
+ state.lastAutoSlug = unique
247
+ state.editorDoc.name = unique
248
+ state.internalSlugUpdate = false
249
+ },
250
+ )
251
+
252
+ watch(
253
+ () => state.editorDoc?.name,
254
+ (newName) => {
255
+ if (!state.editorDoc)
256
+ return
257
+ if (state.internalSlugUpdate) {
258
+ state.internalSlugUpdate = false
259
+ return
260
+ }
261
+ const sanitized = slugify(newName)
262
+ if (!sanitized) {
263
+ state.editorDoc.name = ''
264
+ state.slugManuallyEdited = false
265
+ state.lastAutoSlug = ''
266
+ return
267
+ }
268
+ const unique = ensureUniqueSlug(sanitized, currentDocId())
269
+ if (unique !== newName) {
270
+ state.internalSlugUpdate = true
271
+ state.editorDoc.name = unique
272
+ return
273
+ }
274
+ state.editorDoc.name = unique
275
+ if (unique !== state.lastAutoSlug)
276
+ state.slugManuallyEdited = true
277
+ },
278
+ )
279
+
280
+ watch(
281
+ () => state.renameValue,
282
+ (newVal) => {
283
+ if (!state.renameDialog)
284
+ return
285
+ if (state.renameInternalUpdate) {
286
+ state.renameInternalUpdate = false
287
+ return
288
+ }
289
+ const sanitized = slugify(newVal)
290
+ if (sanitized === newVal)
291
+ return
292
+ state.renameInternalUpdate = true
293
+ state.renameValue = sanitized
294
+ },
295
+ )
296
+
297
+ const formatTimestamp = (input) => {
298
+ if (!input)
299
+ return 'Not yet saved'
300
+ try {
301
+ return new Date(input).toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })
302
+ }
303
+ catch {
304
+ return 'Not yet saved'
305
+ }
306
+ }
307
+
308
+ const postKey = post => post?.docId || post?.id || ''
309
+ const tagPreview = (tags = [], limit = 3) => {
310
+ const list = Array.isArray(tags) ? tags.filter(Boolean) : []
311
+ return {
312
+ visible: list.slice(0, limit),
313
+ remaining: Math.max(list.length - limit, 0),
314
+ }
315
+ }
316
+ const postFeaturedImage = (post) => {
317
+ if (post?.featuredImage)
318
+ return post.featuredImage
319
+ if (Array.isArray(post?.featuredImages) && post.featuredImages[0])
320
+ return post.featuredImages[0]
321
+ return ''
322
+ }
323
+
324
+ const previewContent = (content) => {
325
+ if (typeof content !== 'string')
326
+ return ''
327
+ const normalized = content.trim()
328
+ if (!normalized)
329
+ return ''
330
+ return normalized.length > 140 ? `${normalized.slice(0, 140)}…` : normalized
331
+ }
332
+
333
+ const resetEditorTracking = () => {
334
+ state.editorDoc = null
335
+ state.slugManuallyEdited = false
336
+ state.internalSlugUpdate = false
337
+ state.lastAutoSlug = ''
338
+ }
339
+
340
+ const openNewPost = () => {
341
+ if (props.mode === 'list') {
342
+ emit('update:selectedPostId', 'new')
343
+ return
344
+ }
345
+ state.activePostId = 'new'
346
+ resetEditorTracking()
347
+ state.sheetOpen = true
348
+ }
349
+
350
+ const editPost = (postId) => {
351
+ if (props.mode === 'list') {
352
+ emit('update:selectedPostId', postId)
353
+ return
354
+ }
355
+ state.activePostId = postId
356
+ state.slugManuallyEdited = true
357
+ state.internalSlugUpdate = false
358
+ state.lastAutoSlug = getPostSlug(posts.value?.[postId]) || ''
359
+ state.sheetOpen = true
360
+ }
361
+
362
+ const closeSheet = () => {
363
+ state.sheetOpen = false
364
+ state.activePostId = ''
365
+ resetEditorTracking()
366
+ if (props.mode === 'editor')
367
+ emit('update:selectedPostId', '')
368
+ }
369
+
370
+ const handlePostSaved = () => {
371
+ console.log('Post saved')
372
+ }
373
+
374
+ const openContentImageDialog = () => {
375
+ state.contentImageDialog = true
376
+ }
377
+
378
+ const handleContentImageSelect = (url) => {
379
+ if (url && contentEditor.value?.insertImage) {
380
+ contentEditor.value.insertImage(url)
381
+ }
382
+ state.contentImageDialog = false
383
+ }
384
+
385
+ const onWorkingDocUpdate = (doc) => {
386
+ state.editorDoc = doc
387
+ if (!state.slugManuallyEdited && doc?.name)
388
+ state.lastAutoSlug = doc.name
389
+ }
390
+
391
+ watch(
392
+ () => props.selectedPostId,
393
+ (next) => {
394
+ if (props.mode !== 'editor')
395
+ return
396
+ if (!next) {
397
+ closeSheet()
398
+ return
399
+ }
400
+ if (next === 'new') {
401
+ state.activePostId = 'new'
402
+ resetEditorTracking()
403
+ state.sheetOpen = true
404
+ return
405
+ }
406
+ state.activePostId = next
407
+ state.slugManuallyEdited = true
408
+ state.internalSlugUpdate = false
409
+ state.lastAutoSlug = getPostSlug(posts.value?.[next]) || ''
410
+ state.sheetOpen = true
411
+ },
412
+ { immediate: true },
413
+ )
414
+
415
+ const openRenameDialog = (post) => {
416
+ const slug = getPostSlug(post)
417
+ const fallback = slug || ensureUniqueSlug(post?.title || post?.name || 'post', post?.id)
418
+ state.renamePost = {
419
+ id: post.id,
420
+ title: post.title || '',
421
+ currentSlug: slug,
422
+ }
423
+ state.renameSubmitting = false
424
+ state.renameInternalUpdate = true
425
+ state.renameValue = fallback
426
+ state.renameInternalUpdate = false
427
+ state.renameDialog = true
428
+ }
429
+
430
+ const closeRenameDialog = () => {
431
+ state.renameDialog = false
432
+ state.renamePost = null
433
+ state.renameValue = ''
434
+ state.renameSubmitting = false
435
+ }
436
+
437
+ const renamePostAction = async () => {
438
+ if (!state.renamePost?.id)
439
+ return closeRenameDialog()
440
+
441
+ state.renameSubmitting = true
442
+
443
+ let desired = slugify(state.renameValue || state.renamePost.currentSlug || state.renamePost.title || 'post')
444
+ if (!desired)
445
+ desired = 'post'
446
+
447
+ const unique = ensureUniqueSlug(desired, state.renamePost.id)
448
+
449
+ if (unique === state.renamePost.currentSlug) {
450
+ state.renameSubmitting = false
451
+ closeRenameDialog()
452
+ return
453
+ }
454
+
455
+ try {
456
+ await edgeFirebase.changeDoc(collectionKey.value, state.renamePost.id, { name: unique })
457
+ state.renameValue = unique
458
+ closeRenameDialog()
459
+ }
460
+ catch (error) {
461
+ console.error('Failed to rename post:', error)
462
+ state.renameSubmitting = false
463
+ }
464
+ }
465
+
466
+ const showDeleteDialog = (post) => {
467
+ state.postToDelete = {
468
+ id: post.id,
469
+ name: post.title || getPostSlug(post) || 'Untitled Post',
470
+ }
471
+ state.deleteDialog = true
472
+ }
473
+
474
+ const deletePost = async () => {
475
+ const target = state.postToDelete
476
+ if (!target?.id) {
477
+ state.deleteDialog = false
478
+ return
479
+ }
480
+
481
+ const postId = target.id
482
+ try {
483
+ await edgeFirebase.removeDoc(collectionKey.value, postId)
484
+ await edgeFirebase.removeDoc(publishedCollectionKey.value, postId)
485
+ if (state.activePostId === postId)
486
+ closeSheet()
487
+ }
488
+ catch (error) {
489
+ console.error('Failed to delete post:', error)
490
+ }
491
+ finally {
492
+ state.deleteDialog = false
493
+ state.postToDelete = null
494
+ }
495
+ }
496
+
497
+ const addTag = async (tag) => {
498
+ console.log('Tag to add:', tag)
499
+ }
500
+
501
+ const getTagsFromPosts = computed(() => {
502
+ const tagMap = new Map()
503
+ postsList.value.forEach((post) => {
504
+ if (Array.isArray(post.tags)) {
505
+ post.tags.forEach((tag) => {
506
+ if (tag && typeof tag === 'string' && !tagMap.has(tag)) {
507
+ tagMap.set(tag, { name: tag, title: tag })
508
+ }
509
+ })
510
+ }
511
+ })
512
+ return Array.from(tagMap.values()).sort((a, b) => a.title.localeCompare(b.title))
513
+ })
514
+
515
+ const publishPost = async (postId) => {
516
+ emit('updating', true)
517
+ if (!postId)
518
+ return
519
+ const post = posts.value?.[postId]
520
+ if (!post)
521
+ return
522
+ try {
523
+ await edgeFirebase.storeDoc(publishedCollectionKey.value, post)
524
+ }
525
+ catch (error) {
526
+ console.error('Failed to publish post:', error)
527
+ }
528
+ emit('updating', false)
529
+ }
530
+
531
+ const unPublishPost = async (postId) => {
532
+ if (!postId)
533
+ return
534
+ try {
535
+ await edgeFirebase.removeDoc(publishedCollectionKey.value, postId)
536
+ }
537
+
538
+ catch (error) {
539
+ console.error('Failed to unpublish post:', error)
540
+ }
541
+ }
542
+ </script>
543
+
544
+ <template>
545
+ <div v-if="props.mode !== 'editor'" class="space-y-4">
546
+ <edge-shad-button
547
+ variant="outline"
548
+ :class="isFullList ? 'h-8 px-3' : 'w-full mt-2 py-0 h-[28px]'"
549
+ @click="openNewPost"
550
+ >
551
+ <Plus class="h-4 w-4" />
552
+ New Post
553
+ </edge-shad-button>
554
+
555
+ <div v-if="isFullList" class="rounded-lg border bg-card overflow-hidden">
556
+ <div class="flex items-center justify-between px-4 py-3 border-b bg-muted/40">
557
+ <div class="text-sm font-semibold">
558
+ Posts
559
+ </div>
560
+ <div class="text-xs text-muted-foreground">
561
+ {{ postsList.length }} total
562
+ </div>
563
+ </div>
564
+ <div v-if="hasPosts" class="divide-y">
565
+ <div
566
+ v-for="post in postsList"
567
+ :key="post.id"
568
+ class="px-4 py-3 hover:bg-muted/40 cursor-pointer"
569
+ @click="editPost(post.id)"
570
+ >
571
+ <div class="flex items-start gap-4">
572
+ <div class="h-16 w-20 rounded-md border bg-muted/40 overflow-hidden flex items-center justify-center shrink-0">
573
+ <img
574
+ v-if="postFeaturedImage(post)"
575
+ :src="postFeaturedImage(post)"
576
+ alt=""
577
+ class="h-full w-full object-cover"
578
+ >
579
+ <Image v-else class="h-6 w-6 text-muted-foreground/60" />
580
+ </div>
581
+ <div class="flex-1 min-w-0">
582
+ <div class="flex items-start justify-between gap-3">
583
+ <div class="min-w-0 space-y-1">
584
+ <div class="text-sm font-medium text-foreground truncate">
585
+ {{ post.title || post.name || 'Untitled Post' }}
586
+ </div>
587
+ <div class="text-xs text-muted-foreground line-clamp-2">
588
+ {{ previewContent(post.blurb || post.content) || 'No content yet.' }}
589
+ </div>
590
+ </div>
591
+ <DropdownMenu>
592
+ <DropdownMenuTrigger as-child>
593
+ <edge-shad-button variant="ghost" size="icon" class="h-8 w-8" @click.stop>
594
+ <MoreHorizontal class="h-4 w-4" />
595
+ </edge-shad-button>
596
+ </DropdownMenuTrigger>
597
+ <DropdownMenuContent side="right" align="start">
598
+ <DropdownMenuItem @click="openRenameDialog(post)">
599
+ <FilePen class="h-4 w-4" />
600
+ Rename
601
+ </DropdownMenuItem>
602
+ <DropdownMenuItem v-if="isPublishedPostDiff(postKey(post))" @click="publishPost(postKey(post))">
603
+ <FileCheck class="h-4 w-4" />
604
+ Publish
605
+ </DropdownMenuItem>
606
+ <DropdownMenuItem v-else @click="unPublishPost(postKey(post))">
607
+ <FileWarning class="h-4 w-4" />
608
+ Unpublish
609
+ </DropdownMenuItem>
610
+ <DropdownMenuItem class="text-destructive" @click="showDeleteDialog(post)">
611
+ <Trash2 class="h-4 w-4" />
612
+ Delete
613
+ </DropdownMenuItem>
614
+ </DropdownMenuContent>
615
+ </DropdownMenu>
616
+ </div>
617
+ <div class="mt-2 flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
618
+ <div class="flex items-center gap-1">
619
+ <FileWarning v-if="isPublishedPostDiff(postKey(post))" class="h-3.5 w-3.5 text-yellow-600" />
620
+ <FileCheck v-else class="h-3.5 w-3.5 text-green-700" />
621
+ <span>{{ isPublishedPostDiff(postKey(post)) ? 'Draft' : 'Published' }}</span>
622
+ </div>
623
+ <span>{{ formatTimestamp(post.last_updated || post.doc_created_at) }}</span>
624
+ <div v-if="Array.isArray(post.tags) && post.tags.length" class="flex flex-wrap items-center gap-1">
625
+ <span
626
+ v-for="tag in tagPreview(post.tags).visible"
627
+ :key="tag"
628
+ class="rounded-full bg-secondary px-2 py-0.5 text-[10px] text-secondary-foreground"
629
+ >
630
+ {{ tag }}
631
+ </span>
632
+ <span
633
+ v-if="tagPreview(post.tags).remaining"
634
+ class="rounded-full bg-muted px-2 py-0.5 text-[10px] text-muted-foreground"
635
+ >
636
+ +{{ tagPreview(post.tags).remaining }}
637
+ </span>
638
+ </div>
639
+ <span v-if="Array.isArray(post.featuredImages) && post.featuredImages.length">
640
+ {{ post.featuredImages.length }} featured image{{ post.featuredImages.length > 1 ? 's' : '' }}
641
+ </span>
642
+ </div>
643
+ </div>
644
+ </div>
645
+ </div>
646
+ </div>
647
+ <div
648
+ v-else
649
+ class="flex flex-col items-center justify-center gap-3 px-6 py-12 text-center"
650
+ >
651
+ <File class="h-8 w-8 text-muted-foreground/60" />
652
+ <div class="space-y-1">
653
+ <h3 class="text-base font-medium">
654
+ No posts yet
655
+ </h3>
656
+ <p class="text-sm text-muted-foreground">
657
+ Create your first post to start publishing content.
658
+ </p>
659
+ </div>
660
+ <edge-shad-button variant="outline" class="gap-2" @click="openNewPost">
661
+ <Plus class="h-4 w-4" />
662
+ New Post
663
+ </edge-shad-button>
664
+ </div>
665
+ </div>
666
+
667
+ <div v-else>
668
+ <div v-if="hasPosts" class="space-y-2 hidden">
669
+ <SidebarMenuItem v-for="post in postsList" :key="post.id">
670
+ <SidebarMenuButton class="!px-0 hover:!bg-transparent" @click="editPost(post.id)">
671
+ <div class="h-8 w-8 rounded-md border bg-muted/40 overflow-hidden flex items-center justify-center shrink-0">
672
+ <img
673
+ v-if="postFeaturedImage(post)"
674
+ :src="postFeaturedImage(post)"
675
+ alt=""
676
+ class="h-full w-full object-cover"
677
+ >
678
+ <Image v-else class="h-4 w-4 text-muted-foreground/60" />
679
+ </div>
680
+ <FileWarning v-if="isPublishedPostDiff(postKey(post))" class="!text-yellow-600 ml-2" />
681
+ <FileCheck v-else class="text-xs !text-green-700 font-normal ml-2" />
682
+ <div class="ml-2 flex flex-col text-left">
683
+ <span class="text-sm font-medium">{{ post.name || 'Untitled Post' }}</span>
684
+ </div>
685
+ </SidebarMenuButton>
686
+ <SidebarGroupAction class="absolute right-2 top-0 hover:!bg-transparent">
687
+ <DropdownMenu>
688
+ <DropdownMenuTrigger as-child>
689
+ <SidebarMenuAction>
690
+ <MoreHorizontal />
691
+ </SidebarMenuAction>
692
+ </DropdownMenuTrigger>
693
+ <DropdownMenuContent side="right" align="start">
694
+ <DropdownMenuItem @click="openRenameDialog(post)">
695
+ <FilePen class="h-4 w-4" />
696
+ Rename
697
+ </DropdownMenuItem>
698
+ <DropdownMenuItem v-if="isPublishedPostDiff(postKey(post))" @click="publishPost(postKey(post))">
699
+ <FileCheck class="h-4 w-4" />
700
+ Publish
701
+ </DropdownMenuItem>
702
+ <DropdownMenuItem v-else @click="unPublishPost(postKey(post))">
703
+ <FileWarning class="h-4 w-4" />
704
+ Unpublish
705
+ </DropdownMenuItem>
706
+
707
+ <DropdownMenuItem class="text-destructive" @click="showDeleteDialog(post)">
708
+ <Trash2 class="h-4 w-4" />
709
+ Delete
710
+ </DropdownMenuItem>
711
+ </DropdownMenuContent>
712
+ </DropdownMenu>
713
+ </SidebarGroupAction>
714
+ <div class="w-full pl-7 pb-2 text-xs text-muted-foreground cursor-pointer" @click="editPost(post.id)">
715
+ <div>{{ formatTimestamp(post.last_updated || post.doc_created_at) }}</div>
716
+ <div v-if="Array.isArray(post.tags) && post.tags.length" class="mt-1 flex flex-wrap gap-1">
717
+ <span
718
+ v-for="tag in tagPreview(post.tags).visible"
719
+ :key="tag"
720
+ class="rounded-full bg-secondary px-2 py-0.5 text-[10px] text-secondary-foreground"
721
+ >
722
+ {{ tag }}
723
+ </span>
724
+ <span
725
+ v-if="tagPreview(post.tags).remaining"
726
+ class="rounded-full bg-muted px-2 py-0.5 text-[10px] text-muted-foreground"
727
+ >
728
+ +{{ tagPreview(post.tags).remaining }}
729
+ </span>
730
+ </div>
731
+ <div v-if="Array.isArray(post.featuredImages) && post.featuredImages.length" class="mt-1 text-[11px]">
732
+ {{ post.featuredImages.length }} featured image{{ post.featuredImages.length > 1 ? 's' : '' }}
733
+ </div>
734
+ </div>
735
+ <Separator class="my-2" />
736
+ </SidebarMenuItem>
737
+ </div>
738
+
739
+ <div
740
+ v-else
741
+ class="flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-muted-foreground/40 px-6 py-10 text-center"
742
+ >
743
+ <File class="h-8 w-8 text-muted-foreground/60" />
744
+ <div class="space-y-1">
745
+ <h3 class="text-base font-medium">
746
+ No posts yet
747
+ </h3>
748
+ <p class="text-sm text-muted-foreground">
749
+ Create your first post to start publishing content.
750
+ </p>
751
+ </div>
752
+ <edge-shad-button variant="outline" class="gap-2" @click="openNewPost">
753
+ <Plus class="h-4 w-4" />
754
+ New Post
755
+ </edge-shad-button>
756
+ </div>
757
+ </div>
758
+ </div>
759
+
760
+ <edge-shad-dialog v-model="state.deleteDialog">
761
+ <DialogContent class="pt-10">
762
+ <DialogHeader>
763
+ <DialogTitle class="text-left">
764
+ Delete "{{ state.postToDelete?.name || 'this post' }}"?
765
+ </DialogTitle>
766
+ <DialogDescription>
767
+ This action cannot be undone.
768
+ </DialogDescription>
769
+ </DialogHeader>
770
+ <DialogFooter class="flex justify-between pt-2">
771
+ <edge-shad-button variant="outline" @click="state.deleteDialog = false">
772
+ Cancel
773
+ </edge-shad-button>
774
+ <edge-shad-button variant="destructive" class="w-full" @click="deletePost">
775
+ Delete
776
+ </edge-shad-button>
777
+ </DialogFooter>
778
+ </DialogContent>
779
+ </edge-shad-dialog>
780
+
781
+ <edge-shad-dialog v-model="state.renameDialog">
782
+ <DialogContent class="pt-10">
783
+ <edge-shad-form :schema="renameSchema" @submit="renamePostAction">
784
+ <DialogHeader>
785
+ <DialogTitle class="text-left">
786
+ Rename "{{ state.renamePost?.title || state.renamePost?.currentSlug || 'Post' }}"
787
+ </DialogTitle>
788
+ <DialogDescription>
789
+ Update the slug used in URLs. Existing links will change after renaming.
790
+ </DialogDescription>
791
+ </DialogHeader>
792
+ <edge-shad-input v-model="state.renameValue" name="name" label="Name" />
793
+ <DialogFooter class="flex justify-between pt-2">
794
+ <edge-shad-button variant="outline" @click="closeRenameDialog">
795
+ Cancel
796
+ </edge-shad-button>
797
+ <edge-shad-button
798
+ type="submit"
799
+ class="w-full bg-slate-800 text-white hover:bg-slate-400"
800
+ :disabled="state.renameSubmitting"
801
+ >
802
+ <Loader2 v-if="state.renameSubmitting" class="h-4 w-4 animate-spin" />
803
+ <span v-else>Rename</span>
804
+ </edge-shad-button>
805
+ </DialogFooter>
806
+ </edge-shad-form>
807
+ </DialogContent>
808
+ </edge-shad-dialog>
809
+
810
+ <template v-if="props.mode === 'editor'">
811
+ <div v-if="editorOpen" class="h-full flex flex-col bg-background px-0">
812
+ <edge-editor
813
+ v-if="editorOpen"
814
+ :collection="collection"
815
+ :doc-id="state.activePostId"
816
+ :schema="schemas.posts"
817
+ :new-doc-schema="state.newDocs.posts"
818
+ class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none pt-0 px-0"
819
+ card-content-class="px-0"
820
+ :show-header="true"
821
+ :no-close-after-save="true"
822
+ :save-function-override="handlePostSaved"
823
+ @working-doc="onWorkingDocUpdate"
824
+ >
825
+ <template #header="slotProps">
826
+ <div class="relative flex items-center bg-secondary p-2 justify-between sticky top-0 z-50 bg-primary rounded h-[50px]">
827
+ <span class="text-lg font-semibold whitespace-nowrap pr-1">{{ sheetTitle }}</span>
828
+ <div class="flex w-full items-center">
829
+ <div class="w-full border-t border-gray-300 dark:border-white/15" aria-hidden="true" />
830
+ <div class="flex items-center gap-1 pr-3">
831
+ <edge-shad-button
832
+ v-if="!slotProps.unsavedChanges"
833
+ variant="text"
834
+ class="hover:text-red-700/50 text-xs h-[26px] text-red-700"
835
+ @click="closeSheet"
836
+ >
837
+ <X class="w-4 h-4" />
838
+ Close
839
+ </edge-shad-button>
840
+ <edge-shad-button
841
+ v-else
842
+ variant="text"
843
+ class="hover:text-red-700/50 text-xs h-[26px] text-red-700"
844
+ @click="closeSheet"
845
+ >
846
+ <X class="w-4 h-4" />
847
+ Cancel
848
+ </edge-shad-button>
849
+ <edge-shad-button
850
+ v-if="isCreating || slotProps.unsavedChanges"
851
+ variant="text"
852
+ type="submit"
853
+ class="bg-secondary hover:text-primary/50 text-xs h-[26px] text-primary"
854
+ :disabled="slotProps.submitting"
855
+ >
856
+ <Loader2 v-if="slotProps.submitting" class="w-4 h-4 animate-spin" />
857
+ <Save v-else class="w-4 h-4" />
858
+ <span>Save</span>
859
+ </edge-shad-button>
860
+ </div>
861
+ </div>
862
+ </div>
863
+ </template>
864
+ <template #main="slotProps">
865
+ <div class="p-6 h-[calc(100vh-122px)] overflow-y-auto">
866
+ <div class="grid gap-6 lg:grid-cols-[320px_minmax(0,1fr)]">
867
+ <div class="space-y-6">
868
+ <div class="rounded-xl border bg-card p-4 space-y-4 shadow-sm">
869
+ <div class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
870
+ Post Details
871
+ </div>
872
+ <edge-shad-input
873
+ v-model="slotProps.workingDoc.name"
874
+ name="name"
875
+ label="Name (slug used in URL)"
876
+ />
877
+ <edge-shad-input
878
+ v-model="slotProps.workingDoc.title"
879
+ name="title"
880
+ label="Title"
881
+ :disabled="slotProps.submitting"
882
+ />
883
+ <edge-shad-select-tags
884
+ v-model="slotProps.workingDoc.tags"
885
+ name="tags"
886
+ label="Tags"
887
+ placeholder="Add a tag"
888
+ :disabled="slotProps.submitting"
889
+ :items="getTagsFromPosts"
890
+ :allow-additions="true"
891
+ @add="addTag"
892
+ />
893
+ </div>
894
+ <div class="rounded-xl border bg-card p-4 space-y-4 shadow-sm">
895
+ <div class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
896
+ Featured Image
897
+ </div>
898
+ <div class="relative bg-muted/50 py-2 h-48 rounded-lg flex items-center justify-center hover:opacity-80 transition-opacity cursor-pointer">
899
+ <div class="bg-black/80 absolute left-0 top-0 w-full h-full opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center z-10 cursor-pointer rounded-lg">
900
+ <Dialog v-model:open="state.imageOpen">
901
+ <DialogTrigger as-child>
902
+ <edge-shad-button variant="outline" class="bg-white text-black hover:bg-gray-200">
903
+ <ImagePlus class="h-5 w-5" />
904
+ Select Image
905
+ </edge-shad-button>
906
+ </DialogTrigger>
907
+ <DialogContent class="w-full max-w-[1200px] max-h-[80vh] overflow-y-auto">
908
+ <DialogHeader>
909
+ <DialogTitle>Select Image</DialogTitle>
910
+ <DialogDescription />
911
+ </DialogHeader>
912
+ <edge-cms-media-manager
913
+ :site="props.site"
914
+ :select-mode="true"
915
+ @select="(url) => { slotProps.workingDoc.featuredImage = url; state.imageOpen = false; }"
916
+ />
917
+ </DialogContent>
918
+ </Dialog>
919
+ </div>
920
+ <img v-if="slotProps.workingDoc.featuredImage" :src="slotProps.workingDoc.featuredImage" class="mb-2 max-h-40 mx-auto object-contain">
921
+ <span v-else class="text-sm text-muted-foreground italic">No featured image selected</span>
922
+ </div>
923
+ </div>
924
+ </div>
925
+ <div class="space-y-6">
926
+ <div class="rounded-xl border bg-card p-4 space-y-4 shadow-sm">
927
+ <div class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
928
+ Content
929
+ </div>
930
+ <edge-shad-textarea
931
+ v-model="slotProps.workingDoc.blurb"
932
+ name="blurb"
933
+ label="Content Blurb / Preview"
934
+ :disabled="slotProps.submitting"
935
+ rows="6"
936
+ />
937
+ <edge-shad-html
938
+ ref="contentEditor"
939
+ v-model="slotProps.workingDoc.content"
940
+ height-class="h-[calc(100vh-490px)]"
941
+ :enabled-toggles="['bold', 'italic', 'strike', 'bulletlist', 'orderedlist', 'underline', 'image']"
942
+ name="content"
943
+ label="Content"
944
+ :disabled="slotProps.submitting"
945
+ @request-image="openContentImageDialog"
946
+ />
947
+ <Dialog v-model:open="state.contentImageDialog">
948
+ <DialogContent class="w-full max-w-[1200px] max-h-[80vh] overflow-y-auto">
949
+ <DialogHeader>
950
+ <DialogTitle>Select Image</DialogTitle>
951
+ <DialogDescription />
952
+ </DialogHeader>
953
+ <edge-cms-media-manager
954
+ :site="props.site"
955
+ :select-mode="true"
956
+ @select="handleContentImageSelect"
957
+ />
958
+ </DialogContent>
959
+ </Dialog>
960
+ </div>
961
+ </div>
962
+ </div>
963
+ </div>
964
+ </template>
965
+ <template #footer>
966
+ <div />
967
+ </template>
968
+ </edge-editor>
969
+ </div>
970
+ </template>
971
+ <Sheet v-else v-model:open="state.sheetOpen">
972
+ <SheetContent side="left" class="w-full md:w-1/2 max-w-none sm:max-w-none max-w-2xl">
973
+ <SheetHeader>
974
+ <SheetTitle>{{ sheetTitle }}</SheetTitle>
975
+ </SheetHeader>
976
+ <edge-editor
977
+ v-if="editorOpen"
978
+ :collection="collection"
979
+ :doc-id="state.activePostId"
980
+ :schema="schemas.posts"
981
+ :new-doc-schema="state.newDocs.posts"
982
+ class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none pt-0"
983
+ card-content-class="px-0"
984
+ :show-header="false"
985
+ :no-close-after-save="true"
986
+ :save-function-override="handlePostSaved"
987
+ @working-doc="onWorkingDocUpdate"
988
+ >
989
+ <template #main="slotProps">
990
+ <div class="p-6 space-y-4 h-[calc(100vh-122px)] overflow-y-auto">
991
+ <edge-shad-input
992
+ v-model="slotProps.workingDoc.name"
993
+ name="name"
994
+ label="Name"
995
+ />
996
+ <edge-shad-input
997
+ v-model="slotProps.workingDoc.title"
998
+ name="title"
999
+ label="Title"
1000
+ :disabled="slotProps.submitting"
1001
+ />
1002
+ <div class="relative bg-muted py-2 h-48 rounded-md flex items-center justify-center hover:opacity-80 transition-opacity cursor-pointer">
1003
+ <div class="bg-black/80 absolute left-0 top-0 w-full h-full opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center z-10 cursor-pointer">
1004
+ <Dialog v-model:open="state.imageOpen">
1005
+ <DialogTrigger as-child>
1006
+ <edge-shad-button variant="outline" class="bg-white text-black hover:bg-gray-200">
1007
+ <ImagePlus class="h-5 w-5" />
1008
+ Select Image
1009
+ </edge-shad-button>
1010
+ </DialogTrigger>
1011
+ <DialogContent class="w-full max-w-[1200px] max-h-[80vh] overflow-y-auto">
1012
+ <DialogHeader>
1013
+ <DialogTitle>Select Image</DialogTitle>
1014
+ <DialogDescription />
1015
+ </DialogHeader>
1016
+ <edge-cms-media-manager
1017
+ :site="props.site"
1018
+ :select-mode="true"
1019
+ @select="(url) => { slotProps.workingDoc.featuredImage = url; state.imageOpen = false; }"
1020
+ />
1021
+ </DialogContent>
1022
+ </Dialog>
1023
+ </div>
1024
+ <img v-if="slotProps.workingDoc.featuredImage" :src="slotProps.workingDoc.featuredImage" class="mb-2 max-h-40 mx-auto object-contain">
1025
+ <span v-else class="text-sm text-muted-foreground italic">No featured image selected, click to select</span>
1026
+ </div>
1027
+ <edge-shad-select-tags
1028
+ v-model="slotProps.workingDoc.tags"
1029
+ name="tags"
1030
+ label="Tags"
1031
+ placeholder="Add a tag"
1032
+ :disabled="slotProps.submitting"
1033
+ :items="getTagsFromPosts"
1034
+ :allow-additions="true"
1035
+ @add="addTag"
1036
+ />
1037
+ <edge-shad-textarea
1038
+ v-model="slotProps.workingDoc.blurb"
1039
+ name="blurb"
1040
+ label="Content Blurb / Preview"
1041
+ :disabled="slotProps.submitting"
1042
+ rows="8"
1043
+ />
1044
+ <edge-shad-html
1045
+ ref="contentEditor"
1046
+ v-model="slotProps.workingDoc.content"
1047
+ :enabled-toggles="['bold', 'italic', 'strike', 'bulletlist', 'orderedlist', 'underline', 'image']"
1048
+ name="content"
1049
+ label="Content"
1050
+ :disabled="slotProps.submitting"
1051
+ @request-image="openContentImageDialog"
1052
+ />
1053
+ <Dialog v-model:open="state.contentImageDialog">
1054
+ <DialogContent class="w-full max-w-[1200px] max-h-[80vh] overflow-y-auto">
1055
+ <DialogHeader>
1056
+ <DialogTitle>Select Image</DialogTitle>
1057
+ <DialogDescription />
1058
+ </DialogHeader>
1059
+ <edge-cms-media-manager
1060
+ :site="props.site"
1061
+ :select-mode="true"
1062
+ @select="handleContentImageSelect"
1063
+ />
1064
+ </DialogContent>
1065
+ </Dialog>
1066
+ </div>
1067
+ <SheetFooter class="pt-2 flex justify-between">
1068
+ <edge-shad-button variant="destructive" class="text-white" @click="state.sheetOpen = false">
1069
+ Cancel
1070
+ </edge-shad-button>
1071
+ <edge-shad-button :disabled="slotProps.submitting" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
1072
+ <Loader2 v-if="slotProps.submitting" class=" h-4 w-4 animate-spin" />
1073
+ Save
1074
+ </edge-shad-button>
1075
+ </SheetFooter>
1076
+ </template>
1077
+ <template #footer>
1078
+ <div />
1079
+ </template>
1080
+ </edge-editor>
1081
+ </SheetContent>
1082
+ </Sheet>
1083
+ </template>