@edgedev/create-edge-app 1.1.25 → 1.1.26

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 +1298 -0
  50. package/edge/components/cms/themeDefaultMenu.vue +548 -0
  51. package/edge/components/cms/themeEditor.vue +426 -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,1785 @@
1
+ <script setup>
2
+ import { AlertTriangle, ArrowDown, ArrowUp, Maximize2, Monitor, Smartphone, Tablet } from 'lucide-vue-next'
3
+ import { toTypedSchema } from '@vee-validate/zod'
4
+ import * as z from 'zod'
5
+ const props = defineProps({
6
+ site: {
7
+ type: String,
8
+ required: true,
9
+ },
10
+ page: {
11
+ type: String,
12
+ required: true,
13
+ },
14
+ isTemplateSite: {
15
+ type: Boolean,
16
+ default: false,
17
+ },
18
+ })
19
+
20
+ const emit = defineEmits(['head'])
21
+
22
+ const edgeFirebase = inject('edgeFirebase')
23
+
24
+ const state = reactive({
25
+ newDocs: {
26
+ pages: {
27
+ name: { bindings: { 'field-type': 'text', 'label': 'Name', 'helper': 'Name' }, cols: '12', value: '' },
28
+ content: { value: [] },
29
+ postContent: { value: [] },
30
+ structure: { value: [] },
31
+ postStructure: { value: [] },
32
+ },
33
+ },
34
+ editMode: false,
35
+ showUnpublishedChangesDialog: false,
36
+ workingDoc: {},
37
+ previewViewport: 'full',
38
+ newRowLayout: '6',
39
+ newPostRowLayout: '6',
40
+ rowSettings: {
41
+ open: false,
42
+ rowId: null,
43
+ rowRef: null,
44
+ isPost: false,
45
+ draft: {
46
+ width: 'full',
47
+ gap: '4',
48
+ verticalAlign: 'start',
49
+ background: 'transparent',
50
+ },
51
+ },
52
+ addRowPopoverOpen: {
53
+ listTop: false,
54
+ listEmpty: false,
55
+ listBottom: false,
56
+ listBetween: {},
57
+ postTop: false,
58
+ postEmpty: false,
59
+ postBottom: false,
60
+ postBetween: {},
61
+ },
62
+ })
63
+
64
+ const schemas = {
65
+ pages: toTypedSchema(z.object({
66
+ name: z.string({
67
+ required_error: 'Name is required',
68
+ }).min(1, { message: 'Name is required' }),
69
+ })),
70
+ }
71
+
72
+ const previewViewportOptions = [
73
+ { id: 'full', label: 'Wild Width', width: '100%', icon: Maximize2 },
74
+ { id: 'large', label: 'Large Screen', width: '1280px', icon: Monitor },
75
+ { id: 'medium', label: 'Medium', width: '992px', icon: Tablet },
76
+ { id: 'mobile', label: 'Mobile', width: '420px', icon: Smartphone },
77
+ ]
78
+
79
+ const selectedPreviewViewport = computed(() => previewViewportOptions.find(option => option.id === state.previewViewport) || previewViewportOptions[0])
80
+
81
+ const previewViewportStyle = computed(() => {
82
+ const selected = selectedPreviewViewport.value
83
+ if (!selected || selected.id === 'full')
84
+ return { maxWidth: '100%' }
85
+ return {
86
+ width: '100%',
87
+ maxWidth: selected.width,
88
+ marginLeft: 'auto',
89
+ marginRight: 'auto',
90
+ }
91
+ })
92
+
93
+ const setPreviewViewport = (viewportId) => {
94
+ state.previewViewport = viewportId
95
+ }
96
+
97
+ const previewViewportMode = computed(() => {
98
+ if (state.previewViewport === 'full')
99
+ return 'auto'
100
+ return state.previewViewport
101
+ })
102
+
103
+ const isMobilePreview = computed(() => previewViewportMode.value === 'mobile')
104
+
105
+ const GRID_CLASSES = {
106
+ 1: 'grid grid-cols-1 gap-4',
107
+ 2: 'grid grid-cols-1 sm:grid-cols-2 gap-4',
108
+ 3: 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4',
109
+ 4: 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4',
110
+ 5: 'grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-5 gap-4',
111
+ 6: 'grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-6 gap-4',
112
+ }
113
+
114
+ const ROW_WIDTH_OPTIONS = [
115
+ { name: 'full', title: 'Full width (100%)', class: 'w-full' },
116
+ { name: 'max-w-screen-2xl', title: 'Max width 2XL', class: 'w-full max-w-screen-2xl' },
117
+ { name: 'max-w-screen-xl', title: 'Max width XL', class: 'w-full max-w-screen-xl' },
118
+ { name: 'max-w-screen-lg', title: 'Max width LG', class: 'w-full max-w-screen-lg' },
119
+ { name: 'max-w-screen-md', title: 'Max width MD', class: 'w-full max-w-screen-md' },
120
+ { name: 'max-w-screen-sm', title: 'Max width SM', class: 'w-full max-w-screen-sm' },
121
+ ]
122
+
123
+ const ROW_GAP_OPTIONS = [
124
+ { name: '0', title: 'No gap' },
125
+ { name: '2', title: 'Small' },
126
+ { name: '4', title: 'Medium' },
127
+ { name: '6', title: 'Large' },
128
+ { name: '8', title: 'X-Large' },
129
+ ]
130
+
131
+ const ROW_MOBILE_STACK_OPTIONS = [
132
+ { name: 'normal', title: 'Left first' },
133
+ { name: 'reverse', title: 'Right first' },
134
+ ]
135
+
136
+ const ROW_VERTICAL_ALIGN_OPTIONS = [
137
+ { name: 'start', title: 'Top' },
138
+ { name: 'center', title: 'Middle' },
139
+ { name: 'end', title: 'Bottom' },
140
+ { name: 'stretch', title: 'Stretch' },
141
+ ]
142
+
143
+ const normalizeForCompare = (value) => {
144
+ if (Array.isArray(value))
145
+ return value.map(normalizeForCompare)
146
+ if (value && typeof value === 'object') {
147
+ return Object.keys(value).sort().reduce((acc, key) => {
148
+ acc[key] = normalizeForCompare(value[key])
149
+ return acc
150
+ }, {})
151
+ }
152
+ return value
153
+ }
154
+
155
+ const stableSerialize = value => JSON.stringify(normalizeForCompare(value))
156
+ const areEqualNormalized = (a, b) => stableSerialize(a) === stableSerialize(b)
157
+
158
+ const layoutLabel = (spans) => {
159
+ const key = spans.join('-')
160
+ const map = {
161
+ '6': 'Single column',
162
+ '1-5': 'Narrow left, wide right',
163
+ '2-4': 'Slim left, large right',
164
+ '3-3': 'Two equal columns',
165
+ '4-2': 'Large left, slim right',
166
+ '5-1': 'Wide left, narrow right',
167
+ }
168
+ return map[key] || spans.join(' / ')
169
+ }
170
+
171
+ const LAYOUT_OPTIONS = [
172
+ { spans: [6] },
173
+ { spans: [1, 5] },
174
+ { spans: [2, 4] },
175
+ { spans: [3, 3] },
176
+ { spans: [4, 2] },
177
+ { spans: [5, 1] },
178
+ ]
179
+ .map(option => ({
180
+ id: option.spans.join('-'),
181
+ spans: option.spans,
182
+ label: layoutLabel(option.spans),
183
+ }))
184
+
185
+ const LAYOUT_MAP = {}
186
+ for (const option of LAYOUT_OPTIONS)
187
+ LAYOUT_MAP[option.id] = option.spans
188
+
189
+ const rowWidthClass = (width) => {
190
+ const found = ROW_WIDTH_OPTIONS.find(option => option.name === width)
191
+ return found?.class || ROW_WIDTH_OPTIONS[0].class
192
+ }
193
+
194
+ const ensureBlocksArray = (workingDoc, key) => {
195
+ if (!Array.isArray(workingDoc[key]))
196
+ workingDoc[key] = []
197
+ for (const block of workingDoc[key]) {
198
+ if (!block.id)
199
+ block.id = edgeGlobal.generateShortId()
200
+ }
201
+ }
202
+
203
+ const createRow = (columns = 1) => {
204
+ const row = {
205
+ id: edgeGlobal.generateShortId(),
206
+ width: 'full',
207
+ gap: '4',
208
+ background: 'transparent',
209
+ verticalAlign: 'start',
210
+ mobileOrder: 'normal',
211
+ columns: Array.from({ length: Math.min(Math.max(Number(columns) || 1, 1), 6) }, () => ({
212
+ id: edgeGlobal.generateShortId(),
213
+ blocks: [],
214
+ span: null,
215
+ })),
216
+ }
217
+ refreshRowTailwindClasses(row)
218
+ return row
219
+ }
220
+
221
+ const ensureStructureDefaults = (workingDoc, isPost = false) => {
222
+ if (!workingDoc)
223
+ return
224
+
225
+ const contentKey = isPost ? 'postContent' : 'content'
226
+ const structureKey = isPost ? 'postStructure' : 'structure'
227
+ ensureBlocksArray(workingDoc, contentKey)
228
+
229
+ if (!Array.isArray(workingDoc[structureKey])) {
230
+ if (workingDoc[contentKey].length > 0) {
231
+ const row = createRow(1)
232
+ row.columns[0].blocks = workingDoc[contentKey].map(block => block.id)
233
+ workingDoc[structureKey] = [row]
234
+ }
235
+ else {
236
+ workingDoc[structureKey] = []
237
+ }
238
+ return
239
+ }
240
+
241
+ let mutated = false
242
+ for (const row of workingDoc[structureKey]) {
243
+ if (!Array.isArray(row.columns)) {
244
+ row.columns = createRow(1).columns
245
+ mutated = true
246
+ }
247
+
248
+ for (const col of row.columns) {
249
+ if (!col.id) {
250
+ col.id = edgeGlobal.generateShortId()
251
+ mutated = true
252
+ }
253
+ if (!Array.isArray(col.blocks)) {
254
+ col.blocks = []
255
+ mutated = true
256
+ }
257
+ if (col.span == null)
258
+ col.span = null
259
+ }
260
+
261
+ if (!row.width) {
262
+ row.width = 'full'
263
+ mutated = true
264
+ }
265
+ if (!row.gap) {
266
+ row.gap = '4'
267
+ mutated = true
268
+ }
269
+ if (!row.mobileOrder) {
270
+ row.mobileOrder = 'normal'
271
+ mutated = true
272
+ }
273
+ if (!row.verticalAlign) {
274
+ row.verticalAlign = 'start'
275
+ mutated = true
276
+ }
277
+ if (typeof row.background !== 'string' || row.background === '') {
278
+ row.background = 'transparent'
279
+ mutated = true
280
+ }
281
+ refreshRowTailwindClasses(row)
282
+ }
283
+
284
+ const contentIds = new Set((workingDoc[contentKey] || []).map(block => block.id))
285
+ for (const row of workingDoc[structureKey]) {
286
+ for (const col of row.columns) {
287
+ const filtered = col.blocks.filter(blockId => contentIds.has(blockId))
288
+ if (filtered.length !== col.blocks.length) {
289
+ col.blocks = filtered
290
+ mutated = true
291
+ }
292
+ }
293
+ }
294
+
295
+ // If nothing needed normalization, leave as-is to avoid reactive churn
296
+ if (!mutated)
297
+ return
298
+ }
299
+
300
+ const addRow = (workingDoc, layoutValue = '6', isPost = false) => {
301
+ ensureStructureDefaults(workingDoc, isPost)
302
+ const structureKey = isPost ? 'postStructure' : 'structure'
303
+ workingDoc[structureKey].push(createRowFromLayout(layoutValue))
304
+ }
305
+
306
+ const adjustRowColumns = (row, newCount) => {
307
+ const count = Math.min(Math.max(Number(newCount) || 1, 1), 6)
308
+ if (row.columns.length === count)
309
+ return
310
+
311
+ if (row.columns.length > count) {
312
+ const removed = row.columns.splice(count)
313
+ const target = row.columns[count - 1]
314
+ for (const col of removed) {
315
+ if (Array.isArray(col.blocks))
316
+ target.blocks.push(...col.blocks)
317
+ }
318
+ }
319
+ else {
320
+ const toAdd = count - row.columns.length
321
+ for (let i = 0; i < toAdd; i++)
322
+ row.columns.push({ id: edgeGlobal.generateShortId(), blocks: [] })
323
+ }
324
+ }
325
+
326
+ const blockIndex = (workingDoc, blockId, isPost = false) => {
327
+ if (!workingDoc)
328
+ return -1
329
+ const contentKey = isPost ? 'postContent' : 'content'
330
+ return (workingDoc[contentKey] || []).findIndex(block => block.id === blockId)
331
+ }
332
+
333
+ const removeBlockFromStructure = (workingDoc, blockId, isPost = false) => {
334
+ const structureKey = isPost ? 'postStructure' : 'structure'
335
+ for (const row of workingDoc[structureKey] || []) {
336
+ for (const col of row.columns || [])
337
+ col.blocks = col.blocks.filter(id => id !== blockId)
338
+ }
339
+ }
340
+
341
+ const cleanupOrphanBlocks = (workingDoc, isPost = false) => {
342
+ const contentKey = isPost ? 'postContent' : 'content'
343
+ const structureKey = isPost ? 'postStructure' : 'structure'
344
+ const used = new Set()
345
+ for (const row of workingDoc[structureKey] || []) {
346
+ for (const col of row.columns || []) {
347
+ for (const blockId of col.blocks || [])
348
+ used.add(blockId)
349
+ }
350
+ }
351
+ workingDoc[contentKey] = (workingDoc[contentKey] || []).filter(block => used.has(block.id))
352
+ }
353
+
354
+ const addBlockToColumn = (rowIndex, colIndex, insertIndex, block, slotProps, isPost = false) => {
355
+ const workingDoc = slotProps.workingDoc
356
+ ensureStructureDefaults(workingDoc, isPost)
357
+ const contentKey = isPost ? 'postContent' : 'content'
358
+ const structureKey = isPost ? 'postStructure' : 'structure'
359
+ const row = workingDoc[structureKey]?.[rowIndex]
360
+ if (!row?.columns?.[colIndex])
361
+ return
362
+
363
+ const preparedBlock = edgeGlobal.dupObject(block)
364
+ preparedBlock.id = edgeGlobal.generateShortId()
365
+ workingDoc[contentKey].push(preparedBlock)
366
+ row.columns[colIndex].blocks.splice(insertIndex, 0, preparedBlock.id)
367
+ }
368
+
369
+ const blockKey = blockId => blockId
370
+
371
+ const deleteBlock = (blockId, slotProps, post = false) => {
372
+ console.log('Deleting block with ID:', blockId)
373
+ if (post) {
374
+ const index = slotProps.workingDoc.postContent.findIndex(block => block.id === blockId)
375
+ if (index !== -1) {
376
+ slotProps.workingDoc.postContent.splice(index, 1)
377
+ }
378
+ removeBlockFromStructure(slotProps.workingDoc, blockId, true)
379
+ return
380
+ }
381
+ const index = slotProps.workingDoc.content.findIndex(block => block.id === blockId)
382
+ if (index !== -1) {
383
+ slotProps.workingDoc.content.splice(index, 1)
384
+ }
385
+ removeBlockFromStructure(slotProps.workingDoc, blockId, false)
386
+ }
387
+
388
+ const blockPick = (block, index, slotProps, post = false) => {
389
+ const generatedId = edgeGlobal.generateShortId()
390
+ block.id = generatedId
391
+ if (index === 0 || index) {
392
+ if (post) {
393
+ slotProps.workingDoc.postContent.splice(index, 0, block)
394
+ }
395
+ else {
396
+ slotProps.workingDoc.content.splice(index, 0, block)
397
+ }
398
+ }
399
+ }
400
+
401
+ onMounted(() => {
402
+ if (props.page === 'new') {
403
+ state.editMode = true
404
+ }
405
+ })
406
+
407
+ const editorDocUpdates = (workingDoc) => {
408
+ ensureStructureDefaults(workingDoc, false)
409
+ if (workingDoc?.post || (Array.isArray(workingDoc?.postContent) && workingDoc.postContent.length > 0) || Array.isArray(workingDoc?.postStructure))
410
+ ensureStructureDefaults(workingDoc, true)
411
+ const blockIds = (workingDoc.content || []).map(block => block.blockId).filter(id => id)
412
+ const postBlockIds = workingDoc.postContent ? workingDoc.postContent.map(block => block.blockId).filter(id => id) : []
413
+ blockIds.push(...postBlockIds)
414
+ const uniqueBlockIds = [...new Set(blockIds)]
415
+ state.workingDoc.blockIds = uniqueBlockIds
416
+ }
417
+
418
+ const pageName = computed(() => {
419
+ return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`]?.[props.page]?.name || ''
420
+ })
421
+
422
+ const themes = computed(() => {
423
+ return Object.values(edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`] || {})
424
+ })
425
+
426
+ watch([themes, () => props.isTemplateSite], ([newThemes, isTemplate]) => {
427
+ if (!isTemplate)
428
+ return
429
+ const hasSelection = newThemes.some(themeDoc => themeDoc.docId === edgeGlobal.edgeState.blockEditorTheme)
430
+ if ((!edgeGlobal.edgeState.blockEditorTheme || !hasSelection) && newThemes.length > 0)
431
+ edgeGlobal.edgeState.blockEditorTheme = newThemes[0].docId
432
+ }, { immediate: true, deep: true })
433
+
434
+ const selectedThemeId = computed(() => {
435
+ if (props.isTemplateSite) {
436
+ return edgeGlobal.edgeState.blockEditorTheme || ''
437
+ }
438
+ return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`]?.[props.site]?.theme || ''
439
+ })
440
+
441
+ const theme = computed(() => {
442
+ const themeId = selectedThemeId.value
443
+ if (!themeId)
444
+ return null
445
+ const themeContents = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[themeId]?.theme || null
446
+ if (!themeContents)
447
+ return null
448
+ try {
449
+ return typeof themeContents === 'string' ? JSON.parse(themeContents) : themeContents
450
+ }
451
+ catch (e) {
452
+ return null
453
+ }
454
+ })
455
+
456
+ const themeColorMap = computed(() => {
457
+ const map = {}
458
+ const colors = theme.value?.extend?.colors
459
+ if (!colors || typeof colors !== 'object')
460
+ return map
461
+
462
+ for (const [key, val] of Object.entries(colors)) {
463
+ if (typeof val === 'string' && val !== '')
464
+ map[key] = val
465
+ }
466
+ return map
467
+ })
468
+
469
+ const themeColorOptions = computed(() => {
470
+ const colors = themeColorMap.value
471
+ const options = Object.keys(colors || {}).map(color => ({ name: color, title: color.charAt(0).toUpperCase() + color.slice(1) }))
472
+ return [{ name: 'transparent', title: 'Transparent' }, ...options]
473
+ })
474
+
475
+ const backgroundClass = (bgKey) => {
476
+ if (!bgKey)
477
+ return ''
478
+ if (bgKey === 'transparent')
479
+ return 'bg-transparent'
480
+ return `bg-${bgKey}`
481
+ }
482
+
483
+ const rowBackgroundStyle = (bgKey) => {
484
+ if (!bgKey)
485
+ return {}
486
+ if (bgKey === 'transparent')
487
+ return { backgroundColor: 'transparent' }
488
+ let color = themeColorMap.value?.[bgKey]
489
+ if (!color)
490
+ return {}
491
+ if (/^[0-9A-Fa-f]{6}$/.test(color))
492
+ color = `#${color}`
493
+ return { backgroundColor: color }
494
+ }
495
+
496
+ const layoutSpansFromString = (value, fallback = [6]) => {
497
+ if (Array.isArray(value))
498
+ return value
499
+ if (value && LAYOUT_MAP[String(value)])
500
+ return LAYOUT_MAP[String(value)]
501
+ const str = String(value || '').trim()
502
+ if (!str)
503
+ return fallback
504
+ if (!str.includes('-')) {
505
+ const count = Number(str)
506
+ if (Number.isFinite(count) && count > 0) {
507
+ const base = Math.floor(6 / count)
508
+ const remainder = 6 - (base * count)
509
+ const spans = Array.from({ length: count }, (_, idx) => base + (idx < remainder ? 1 : 0))
510
+ return spans
511
+ }
512
+ return fallback
513
+ }
514
+ const spans = str.split('-').map(s => Number(s)).filter(n => Number.isFinite(n) && n > 0)
515
+ const total = spans.reduce((a, b) => a + b, 0)
516
+ if (total !== 6 || spans.length === 0)
517
+ return fallback
518
+ return spans
519
+ }
520
+
521
+ const rowUsesSpans = row => (row?.columns || []).some(col => Number.isFinite(col?.span))
522
+
523
+ const rowGridClass = (row) => {
524
+ const base = isMobilePreview.value
525
+ ? 'grid grid-cols-1'
526
+ : (rowUsesSpans(row) ? 'grid grid-cols-1 sm:grid-cols-6' : (GRID_CLASSES[row.columns?.length] || GRID_CLASSES[1]))
527
+ return [base, rowGapClass(row)].filter(Boolean).join(' ')
528
+ }
529
+
530
+ const rowGridClassForData = (row) => {
531
+ const base = rowUsesSpans(row) ? 'grid grid-cols-1 sm:grid-cols-6' : (GRID_CLASSES[row.columns?.length] || GRID_CLASSES[1])
532
+ return [base, rowGapClass(row)].filter(Boolean).join(' ')
533
+ }
534
+
535
+ const rowVerticalAlignClass = (row) => {
536
+ const map = {
537
+ start: 'items-start',
538
+ center: 'items-center',
539
+ end: 'items-end',
540
+ stretch: 'items-stretch',
541
+ }
542
+ return map[row?.verticalAlign] || map.start
543
+ }
544
+
545
+ const rowGapClass = (row) => {
546
+ const gap = Number(row?.gap)
547
+ const allowed = new Set([0, 2, 4, 6, 8])
548
+ const safeGap = allowed.has(gap) ? gap : 4
549
+ if (safeGap === 0)
550
+ return 'gap-0'
551
+ return ['gap-0', `sm:gap-${safeGap}`].join(' ')
552
+ }
553
+
554
+ const rowGridStyle = (row) => {
555
+ if (isMobilePreview.value)
556
+ return {}
557
+ if (!rowUsesSpans(row))
558
+ return {}
559
+ return { gridTemplateColumns: 'repeat(6, minmax(0, 1fr))' }
560
+ }
561
+
562
+ const columnSpanStyle = (col) => {
563
+ if (isMobilePreview.value)
564
+ return {}
565
+ if (!Number.isFinite(col?.span))
566
+ return {}
567
+ const span = Math.min(Math.max(col.span, 1), 6)
568
+ return { gridColumn: `span ${span} / span ${span}` }
569
+ }
570
+
571
+ const columnSpanClass = (col) => {
572
+ if (!Number.isFinite(col?.span))
573
+ return ''
574
+ const span = Math.min(Math.max(col.span, 1), 6)
575
+ return `col-span-${span}`
576
+ }
577
+
578
+ const columnMobileOrderClass = (row, idx) => {
579
+ const len = row?.columns?.length || 0
580
+ if (!len)
581
+ return ''
582
+ const order = row?.mobileOrder === 'reverse' ? (len - idx) : (idx + 1)
583
+ return [`order-${order}`, 'sm:order-none'].join(' ')
584
+ }
585
+
586
+ const columnMobileOrderStyle = (row, idx) => {
587
+ if (!isMobilePreview.value)
588
+ return {}
589
+ const len = row?.columns?.length || 0
590
+ if (!len)
591
+ return {}
592
+ const order = row?.mobileOrder === 'reverse' ? (len - idx) : (idx + 1)
593
+ return { order, gridRowStart: order }
594
+ }
595
+
596
+ const computeRowTailwindClasses = (row) => {
597
+ const classes = [
598
+ rowWidthClass(row?.width),
599
+ backgroundClass(row?.background),
600
+ rowGridClassForData(row),
601
+ rowVerticalAlignClass(row),
602
+ rowGapClass(row),
603
+ ]
604
+ return classes.filter(Boolean).join(' ').trim()
605
+ }
606
+
607
+ const computeColumnTailwindClasses = (row, idx) => {
608
+ const classes = [
609
+ columnSpanClass(row?.columns?.[idx]),
610
+ columnMobileOrderClass(row, idx),
611
+ ]
612
+ return classes.filter(Boolean).join(' ').trim()
613
+ }
614
+
615
+ const refreshRowTailwindClasses = (row) => {
616
+ if (!row)
617
+ return
618
+ row.tailwindClasses = computeRowTailwindClasses(row)
619
+ if (Array.isArray(row.columns)) {
620
+ row.columns.forEach((col, idx) => {
621
+ col.tailwindClasses = computeColumnTailwindClasses(row, idx)
622
+ })
623
+ }
624
+ }
625
+
626
+ const activeRowSettingsRow = computed(() => {
627
+ if (state.rowSettings.rowRef)
628
+ return state.rowSettings.rowRef
629
+ const key = state.rowSettings.isPost ? 'postStructure' : 'structure'
630
+ const rows = state.workingDoc?.[key] || []
631
+ return rows.find(row => row.id === state.rowSettings.rowId) || null
632
+ })
633
+
634
+ const resetRowSettingsDraft = (row) => {
635
+ state.rowSettings.draft = {
636
+ width: row?.width || 'full',
637
+ gap: row?.gap || '4',
638
+ verticalAlign: row?.verticalAlign || 'start',
639
+ background: row?.background || 'transparent',
640
+ mobileOrder: row?.mobileOrder || 'normal',
641
+ }
642
+ }
643
+
644
+ const openRowSettings = (row, isPost = false) => {
645
+ state.rowSettings.rowId = row?.id || null
646
+ state.rowSettings.rowRef = row || null
647
+ state.rowSettings.isPost = isPost
648
+ resetRowSettingsDraft(row)
649
+ state.rowSettings.open = true
650
+ }
651
+
652
+ const saveRowSettings = () => {
653
+ const row = activeRowSettingsRow.value
654
+ if (!row) {
655
+ state.rowSettings.open = false
656
+ return
657
+ }
658
+ const draft = state.rowSettings.draft || {}
659
+ row.width = draft.width || 'full'
660
+ row.gap = draft.gap || '4'
661
+ row.verticalAlign = draft.verticalAlign || 'start'
662
+ row.background = draft.background || 'transparent'
663
+ row.mobileOrder = draft.mobileOrder || 'normal'
664
+ refreshRowTailwindClasses(row)
665
+ state.rowSettings.open = false
666
+ }
667
+
668
+ const closeAddRowPopover = (isPost = false, position = 'top', rowId = null) => {
669
+ const pop = state.addRowPopoverOpen
670
+ if (position === 'top') {
671
+ if (isPost)
672
+ pop.postTop = false
673
+ else
674
+ pop.listTop = false
675
+ return
676
+ }
677
+ if (position === 'empty') {
678
+ if (isPost)
679
+ pop.postEmpty = false
680
+ else
681
+ pop.listEmpty = false
682
+ return
683
+ }
684
+ if (position === 'bottom') {
685
+ if (isPost)
686
+ pop.postBottom = false
687
+ else
688
+ pop.listBottom = false
689
+ return
690
+ }
691
+ if (position === 'between' && rowId) {
692
+ const target = isPost ? pop.postBetween : pop.listBetween
693
+ target[rowId] = false
694
+ }
695
+ }
696
+
697
+ const addRowAndClose = (workingDoc, layoutValue, insertIndex, isPost = false, position = 'top', rowId = null) => {
698
+ addRowAt(workingDoc, layoutValue, insertIndex, isPost)
699
+ closeAddRowPopover(isPost, position, rowId)
700
+ }
701
+
702
+ const moveRow = (workingDoc, index, delta, isPost = false) => {
703
+ if (!workingDoc)
704
+ return
705
+ const key = isPost ? 'postStructure' : 'structure'
706
+ const rows = workingDoc[key]
707
+ if (!Array.isArray(rows))
708
+ return
709
+ const targetIndex = index + delta
710
+ if (targetIndex < 0 || targetIndex >= rows.length)
711
+ return
712
+ const [row] = rows.splice(index, 1)
713
+ rows.splice(targetIndex, 0, row)
714
+ }
715
+
716
+ const isLayoutSelected = (layoutId, isPost = false) => {
717
+ return (isPost ? state.newPostRowLayout : state.newRowLayout) === layoutId
718
+ }
719
+
720
+ const selectLayout = (spans, isPost = false) => {
721
+ const id = spans.join('-')
722
+ if (isPost)
723
+ state.newPostRowLayout = id
724
+ else
725
+ state.newRowLayout = id
726
+ }
727
+
728
+ const buildColumnsFromSpans = (spans) => {
729
+ return spans.map(span => ({
730
+ id: edgeGlobal.generateShortId(),
731
+ blocks: [],
732
+ span,
733
+ }))
734
+ }
735
+
736
+ const createRowFromLayout = (spans) => {
737
+ const safeSpans = layoutSpansFromString(spans, [6])
738
+ const row = {
739
+ id: edgeGlobal.generateShortId(),
740
+ width: 'full',
741
+ gap: '4',
742
+ background: 'transparent',
743
+ verticalAlign: 'start',
744
+ mobileOrder: 'normal',
745
+ columns: buildColumnsFromSpans(safeSpans),
746
+ }
747
+ refreshRowTailwindClasses(row)
748
+ return row
749
+ }
750
+
751
+ const addRowAt = (workingDoc, layoutValue = '6', insertIndex = 0, isPost = false) => {
752
+ ensureStructureDefaults(workingDoc, isPost)
753
+ const structureKey = isPost ? 'postStructure' : 'structure'
754
+ const count = workingDoc[structureKey]?.length || 0
755
+ const safeIndex = Math.min(Math.max(insertIndex, 0), count)
756
+ workingDoc[structureKey].splice(safeIndex, 0, createRowFromLayout(layoutValue))
757
+ }
758
+
759
+ const headObject = computed(() => {
760
+ const themeId = selectedThemeId.value
761
+ if (!themeId)
762
+ return {}
763
+ try {
764
+ return JSON.parse(edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[themeId]?.headJSON || '{}')
765
+ }
766
+ catch (e) {
767
+ return {}
768
+ }
769
+ })
770
+
771
+ watch(headObject, (newHeadElements) => {
772
+ emit('head', newHeadElements)
773
+ }, { immediate: true, deep: true })
774
+
775
+ const isPublishedPageDiff = (pageId) => {
776
+ const publishedPage = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`]?.[pageId]
777
+ const draftPage = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`]?.[pageId]
778
+ if (!publishedPage && draftPage) {
779
+ return true
780
+ }
781
+ if (publishedPage && !draftPage) {
782
+ return true
783
+ }
784
+ if (publishedPage && draftPage) {
785
+ return !areEqualNormalized(
786
+ {
787
+ content: publishedPage.content,
788
+ postContent: publishedPage.postContent,
789
+ structure: publishedPage.structure,
790
+ postStructure: publishedPage.postStructure,
791
+ metaTitle: publishedPage.metaTitle,
792
+ metaDescription: publishedPage.metaDescription,
793
+ structuredData: publishedPage.structuredData,
794
+ },
795
+ {
796
+ content: draftPage.content,
797
+ postContent: draftPage.postContent,
798
+ structure: draftPage.structure,
799
+ postStructure: draftPage.postStructure,
800
+ metaTitle: draftPage.metaTitle,
801
+ metaDescription: draftPage.metaDescription,
802
+ structuredData: draftPage.structuredData,
803
+ },
804
+ )
805
+ }
806
+ return false
807
+ }
808
+
809
+ const lastPublishedTime = (pageId) => {
810
+ const timestamp = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`]?.[pageId]?.last_updated
811
+ if (!timestamp)
812
+ return 'Never'
813
+ const date = new Date(timestamp)
814
+ return date.toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })
815
+ }
816
+
817
+ const publishedPage = computed(() => {
818
+ return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`]?.[props.page] || null
819
+ })
820
+
821
+ const currentPage = computed(() => {
822
+ return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`]?.[props.page] || null
823
+ })
824
+
825
+ watch (currentPage, (newPage) => {
826
+ state.workingDoc.last_updated = newPage?.last_updated
827
+ state.workingDoc.metaTitle = newPage?.metaTitle
828
+ state.workingDoc.metaDescription = newPage?.metaDescription
829
+ state.workingDoc.structuredData = newPage?.structuredData
830
+ }, { immediate: true, deep: true })
831
+
832
+ const stringifyLimited = (value, limit = 600) => {
833
+ if (value == null)
834
+ return '—'
835
+ try {
836
+ const stringVal = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
837
+ return stringVal.length > limit ? `${stringVal.slice(0, limit)}...` : stringVal
838
+ }
839
+ catch {
840
+ return '—'
841
+ }
842
+ }
843
+
844
+ const summarizeBlocks = (blocks) => {
845
+ if (!Array.isArray(blocks) || blocks.length === 0)
846
+ return 'No blocks'
847
+ const count = blocks.length
848
+ const names = blocks
849
+ .map(block => block?.type || block?.component || block?.layout || block?.name)
850
+ .filter(Boolean)
851
+ const sample = Array.from(new Set(names)).slice(0, 3).join(', ')
852
+ const suffix = names.length > 3 ? ', ...' : ''
853
+ return `${count} block${count === 1 ? '' : 's'}${sample ? ` (${sample}${suffix})` : ''}`
854
+ }
855
+
856
+ const summarizeStructure = (rows) => {
857
+ if (!Array.isArray(rows) || rows.length === 0)
858
+ return 'No rows'
859
+ const count = rows.length
860
+ const columnCounts = rows
861
+ .map(row => row?.columns?.length)
862
+ .filter(val => typeof val === 'number')
863
+ const sample = columnCounts.slice(0, 3).join(', ')
864
+ const suffix = columnCounts.length > 3 ? ', ...' : ''
865
+ const layout = sample ? ` (cols: ${sample}${suffix})` : ''
866
+ return `${count} row${count === 1 ? '' : 's'}${layout}`
867
+ }
868
+
869
+ const summarizeChangeValue = (value, detailed = false) => {
870
+ if (value == null || value === '')
871
+ return '—'
872
+ if (Array.isArray(value)) {
873
+ return detailed ? stringifyLimited(value) : summarizeBlocks(value)
874
+ }
875
+ if (typeof value === 'object') {
876
+ return stringifyLimited(value, detailed ? 900 : 180)
877
+ }
878
+ const stringVal = String(value).trim()
879
+ return stringVal.length > (detailed ? 320 : 180) ? `${stringVal.slice(0, detailed ? 317 : 177)}...` : stringVal
880
+ }
881
+
882
+ const describeBlock = (block) => {
883
+ if (!block)
884
+ return 'Block'
885
+ const type = block.component || block.type || block.layout || 'Block'
886
+ const title = block.title || block.heading || block.label || block.name || ''
887
+ const summary = block.text || block.content || block.body || ''
888
+ const parts = [type]
889
+ if (title)
890
+ parts.push(`“${String(title)}”`)
891
+ if (summary && String(summary).length < 80)
892
+ parts.push(String(summary))
893
+ return parts.filter(Boolean).join(' - ')
894
+ }
895
+
896
+ const diffBlockFields = (publishedBlock, draftBlock) => {
897
+ const keys = new Set([
898
+ ...Object.keys(publishedBlock || {}),
899
+ ...Object.keys(draftBlock || {}),
900
+ ])
901
+ const changes = []
902
+ for (const key of keys) {
903
+ if (key === 'id' || key === 'blockId')
904
+ continue
905
+ const prevVal = publishedBlock?.[key]
906
+ const nextVal = draftBlock?.[key]
907
+ if (!areEqualNormalized(prevVal, nextVal)) {
908
+ changes.push(`${key}: ${summarizeChangeValue(prevVal, true)} → ${summarizeChangeValue(nextVal, true)}`)
909
+ }
910
+ }
911
+ return changes
912
+ }
913
+
914
+ const buildBlockChangeDetails = (publishedBlocks = [], draftBlocks = []) => {
915
+ const details = []
916
+ const publishedMap = new Map()
917
+ const draftMap = new Map()
918
+
919
+ publishedBlocks.forEach((block, index) => {
920
+ const key = block?.id || block?.blockId || `pub-${index}`
921
+ publishedMap.set(key, block)
922
+ })
923
+ draftBlocks.forEach((block, index) => {
924
+ const key = block?.id || block?.blockId || `draft-${index}`
925
+ draftMap.set(key, block)
926
+ })
927
+
928
+ for (const [key, draftBlock] of draftMap.entries()) {
929
+ if (!publishedMap.has(key)) {
930
+ details.push(`Added ${describeBlock(draftBlock)}`)
931
+ continue
932
+ }
933
+ const publishedBlock = publishedMap.get(key)
934
+ if (!areEqualNormalized(publishedBlock, draftBlock)) {
935
+ const fieldChanges = diffBlockFields(publishedBlock, draftBlock)
936
+ if (fieldChanges.length)
937
+ details.push(`Updated ${describeBlock(draftBlock)} (${fieldChanges.join('; ')})`)
938
+ else
939
+ details.push(`Updated ${describeBlock(draftBlock)}`)
940
+ }
941
+ }
942
+
943
+ for (const [key, publishedBlock] of publishedMap.entries()) {
944
+ if (!draftMap.has(key)) {
945
+ details.push(`Removed ${describeBlock(publishedBlock)}`)
946
+ }
947
+ }
948
+
949
+ return details
950
+ }
951
+
952
+ const unpublishedChangeDetails = computed(() => {
953
+ const changes = []
954
+ const draft = currentPage.value
955
+ const published = publishedPage.value
956
+
957
+ if (!draft && !published)
958
+ return changes
959
+
960
+ const compareField = (key, label, formatter = v => summarizeChangeValue(v, false), options = {}) => {
961
+ const publishedVal = published?.[key]
962
+ const draftVal = draft?.[key]
963
+ if (areEqualNormalized(publishedVal, draftVal))
964
+ return
965
+ const change = {
966
+ key,
967
+ label,
968
+ published: formatter(publishedVal),
969
+ draft: formatter(draftVal),
970
+ }
971
+ if (options.details)
972
+ change.details = options.details(publishedVal, draftVal)
973
+ changes.push(change)
974
+ }
975
+
976
+ if (!published && draft) {
977
+ changes.push({
978
+ key: 'unpublished',
979
+ label: 'Not yet published',
980
+ published: 'No published version',
981
+ draft: 'Draft ready to publish',
982
+ })
983
+ }
984
+ if (published && !draft) {
985
+ changes.push({
986
+ key: 'draft-missing',
987
+ label: 'Draft missing',
988
+ published: 'Published version exists',
989
+ draft: 'No draft available',
990
+ })
991
+ }
992
+
993
+ compareField('content', 'Index content', summarizeBlocks, { details: (pubVal, draftVal) => buildBlockChangeDetails(pubVal, draftVal) })
994
+ compareField('postContent', 'Post content', summarizeBlocks, { details: (pubVal, draftVal) => buildBlockChangeDetails(pubVal, draftVal) })
995
+ compareField('structure', 'Index structure', summarizeStructure)
996
+ compareField('postStructure', 'Post structure', summarizeStructure)
997
+ compareField('metaTitle', 'Meta title', val => summarizeChangeValue(val, true))
998
+ compareField('metaDescription', 'Meta description', val => summarizeChangeValue(val, true))
999
+ compareField('structuredData', 'Structured data', val => summarizeChangeValue(val, true))
1000
+
1001
+ return changes
1002
+ })
1003
+
1004
+ const hasUnsavedChanges = (changes) => {
1005
+ console.log('Unsaved changes:', changes)
1006
+ if (changes === true) {
1007
+ edgeGlobal.edgeState.cmsPageWithUnsavedChanges = props.page
1008
+ }
1009
+ else {
1010
+ edgeGlobal.edgeState.cmsPageWithUnsavedChanges = null
1011
+ }
1012
+ }
1013
+ </script>
1014
+
1015
+ <template>
1016
+ <edge-editor
1017
+ :collection="`sites/${site}/pages`"
1018
+ :doc-id="page"
1019
+ :schema="schemas.pages"
1020
+ :new-doc-schema="state.newDocs.pages"
1021
+ class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none pt-0 px-0"
1022
+ :show-footer="false"
1023
+ :save-redirect-override="`/app/dashboard/sites/${site}`"
1024
+ :no-close-after-save="true"
1025
+ :working-doc-overrides="state.workingDoc"
1026
+ @working-doc="editorDocUpdates"
1027
+ @unsaved-changes="hasUnsavedChanges"
1028
+ >
1029
+ <template #header="slotProps">
1030
+ <div class="relative flex items-center bg-secondary p-2 justify-between sticky top-0 z-50 bg-primary rounded h-[50px]">
1031
+ <span class="text-lg font-semibold whitespace-nowrap pr-1">{{ pageName }}</span>
1032
+
1033
+ <div class="flex w-full items-center">
1034
+ <div class="w-full border-t border-gray-300 dark:border-white/15" aria-hidden="true" />
1035
+ <div v-if="!props.isTemplateSite" class="px-4 text-gray-600 dark:text-gray-300 whitespace-nowrap text-center flex flex-col items-center gap-1">
1036
+ <template v-if="isPublishedPageDiff(page)">
1037
+ <edge-shad-button
1038
+ variant="outline"
1039
+ class="bg-yellow-100 text-yellow-800 border-yellow-300 hover:bg-yellow-100 hover:text-yellow-900 text-xs h-[32px] gap-1"
1040
+ @click="state.showUnpublishedChangesDialog = true"
1041
+ >
1042
+ <AlertTriangle class="w-4 h-4" />
1043
+ Unpublished Changes
1044
+ </edge-shad-button>
1045
+ </template>
1046
+ <template v-else>
1047
+ <edge-chip class="bg-green-100 text-green-800">
1048
+ <div class="w-full">
1049
+ Published
1050
+ </div>
1051
+ </edge-chip>
1052
+ </template>
1053
+ <span class="text-[10px] leading-tight">Last Published: {{ lastPublishedTime(page) }}</span>
1054
+ </div>
1055
+ <div v-else class="px-4 w-full max-w-xs">
1056
+ <edge-shad-select
1057
+ v-model="edgeGlobal.edgeState.blockEditorTheme"
1058
+ name="theme"
1059
+ :items="themes.map(t => ({ title: t.name, name: t.docId }))"
1060
+ placeholder="Select Theme"
1061
+ class="w-full text-xs h-[32px]"
1062
+ />
1063
+ </div>
1064
+ <div class="w-full border-t border-border" aria-hidden="true" />
1065
+
1066
+ <div class="flex items-center gap-1 pr-3">
1067
+ <span class="text-[11px] uppercase tracking-wide text-muted-foreground">Viewport</span>
1068
+ <edge-shad-button
1069
+ v-for="option in previewViewportOptions"
1070
+ :key="option.id"
1071
+ type="button"
1072
+ variant="ghost"
1073
+ size="icon"
1074
+ class="h-[26px] w-[26px] text-xs gap-1 border transition-colors"
1075
+ :class="state.previewViewport === option.id ? 'bg-primary text-primary-foreground border-primary shadow-sm' : 'bg-muted text-foreground border-border hover:bg-muted/80'"
1076
+ @click="setPreviewViewport(option.id)"
1077
+ >
1078
+ <component :is="option.icon" class="w-3.5 h-3.5" />
1079
+ </edge-shad-button>
1080
+ </div>
1081
+
1082
+ <edge-shad-button variant="text" class="hover:text-primary/50 text-xs h-[26px] text-primary" @click="state.editMode = !state.editMode">
1083
+ <template v-if="state.editMode">
1084
+ <Eye class="w-4 h-4" />
1085
+ Preview Mode
1086
+ </template>
1087
+ <template v-else>
1088
+ <Pencil class="w-4 h-4" />
1089
+ Edit Mode
1090
+ </template>
1091
+ </edge-shad-button>
1092
+ <edge-shad-button
1093
+ v-if="!slotProps.unsavedChanges"
1094
+ variant="text"
1095
+ class="hover:text-red-700/50 text-xs h-[26px] text-red-700"
1096
+ @click="slotProps.onCancel"
1097
+ >
1098
+ <X class="w-4 h-4" />
1099
+ Close
1100
+ </edge-shad-button>
1101
+ <edge-shad-button
1102
+ v-else
1103
+ variant="text"
1104
+ class="hover:text-red-700/50 text-xs h-[26px] text-red-700"
1105
+ @click="slotProps.onCancel"
1106
+ >
1107
+ <X class="w-4 h-4" />
1108
+ Cancel
1109
+ </edge-shad-button>
1110
+ <edge-shad-button
1111
+ v-if="state.editMode || slotProps.unsavedChanges"
1112
+ variant="text"
1113
+ type="submit"
1114
+ class="bg-secondary hover:text-primary/50 text-xs h-[26px] text-primary"
1115
+ :disabled="slotProps.submitting"
1116
+ >
1117
+ <Loader2 v-if="slotProps.submitting" class="w-4 h-4 animate-spin" />
1118
+ <Save v-else class="w-4 h-4" />
1119
+ <span>Save</span>
1120
+ </edge-shad-button>
1121
+ </div>
1122
+ </div>
1123
+ </template>
1124
+ <template #main="slotProps">
1125
+ <Tabs class="w-full" default-value="list">
1126
+ <TabsList v-if="slotProps.workingDoc?.post" class="w-full mt-3 bg-primary rounded-sm">
1127
+ <TabsTrigger value="list">
1128
+ Index Page
1129
+ </TabsTrigger>
1130
+ <TabsTrigger value="post">
1131
+ Detail Page
1132
+ </TabsTrigger>
1133
+ </TabsList>
1134
+ <TabsContent value="list">
1135
+ <Separator class="my-4" />
1136
+ <div
1137
+ :key="selectedThemeId"
1138
+ class="w-full mx-auto bg-card border border-border rounded-lg shadow-sm md:shadow-md p-0 space-y-6"
1139
+ :class="{ 'transition-all duration-300': !state.editMode }"
1140
+ :style="previewViewportStyle"
1141
+ >
1142
+ <edge-button-divider v-if="state.editMode" class="my-2">
1143
+ <Popover v-model:open="state.addRowPopoverOpen.listTop">
1144
+ <PopoverTrigger as-child>
1145
+ <edge-shad-button class="bg-secondary text-primary hover:bg-primary/10 hover:text-primary text-xs h-[32px]">
1146
+ Add Row
1147
+ </edge-shad-button>
1148
+ </PopoverTrigger>
1149
+ <PopoverContent class="w-[360px]">
1150
+ <div class="grid grid-cols-2 gap-2">
1151
+ <button
1152
+ v-for="option in LAYOUT_OPTIONS"
1153
+ :key="option.id"
1154
+ type="button"
1155
+ class="border rounded-md p-2 transition bg-white hover:border-primary text-left w-full"
1156
+ :class="isLayoutSelected(option.id, false) ? 'border-primary ring-1 ring-primary/40' : 'border-gray-200'"
1157
+ @click="selectLayout(option.spans, false); addRowAndClose(slotProps.workingDoc, option.id, 0, false, 'top')"
1158
+ >
1159
+ <div class="text-[11px] font-medium mb-1">
1160
+ {{ option.label }}
1161
+ </div>
1162
+ <div class="w-full h-8 grid gap-[2px]" style="grid-template-columns: repeat(6, minmax(0, 1fr));">
1163
+ <div
1164
+ v-for="(span, idx) in option.spans"
1165
+ :key="idx"
1166
+ class="bg-gray-200 rounded-sm"
1167
+ :style="{ gridColumn: `span ${span} / span ${span}` }"
1168
+ />
1169
+ </div>
1170
+ </button>
1171
+ </div>
1172
+ </PopoverContent>
1173
+ </Popover>
1174
+ </edge-button-divider>
1175
+ <div
1176
+ v-if="(!slotProps.workingDoc?.structure || slotProps.workingDoc.structure.length === 0)"
1177
+ class="flex items-center justify-between border border-dashed border-gray-300 rounded-md px-4 py-3 bg-gray-50"
1178
+ >
1179
+ <div class="text-sm text-gray-700">
1180
+ No rows yet. Add your first row to start building.
1181
+ </div>
1182
+ <Popover v-if="state.editMode" v-model:open="state.addRowPopoverOpen.listEmpty">
1183
+ <PopoverTrigger as-child>
1184
+ <edge-shad-button class="bg-secondary text-primary hover:bg-primary/10 hover:text-primary text-xs h-[32px]">
1185
+ Add Row
1186
+ </edge-shad-button>
1187
+ </PopoverTrigger>
1188
+ <PopoverContent class="w-[360px]">
1189
+ <div class="grid grid-cols-2 gap-2">
1190
+ <button
1191
+ v-for="option in LAYOUT_OPTIONS"
1192
+ :key="option.id"
1193
+ type="button"
1194
+ class="border rounded-md p-2 transition bg-white hover:border-primary text-left w-full"
1195
+ :class="isLayoutSelected(option.id, false) ? 'border-primary ring-1 ring-primary/40' : 'border-gray-200'"
1196
+ @click="selectLayout(option.spans, false); addRowAndClose(slotProps.workingDoc, option.id, 0, false, 'empty')"
1197
+ >
1198
+ <div class="text-[11px] font-medium mb-1">
1199
+ {{ option.label }}
1200
+ </div>
1201
+ <div class="w-full h-8 grid gap-[2px]" style="grid-template-columns: repeat(6, minmax(0, 1fr));">
1202
+ <div
1203
+ v-for="(span, idx) in option.spans"
1204
+ :key="idx"
1205
+ class="bg-gray-200 rounded-sm"
1206
+ :style="{ gridColumn: `span ${span} / span ${span}` }"
1207
+ />
1208
+ </div>
1209
+ </button>
1210
+ </div>
1211
+ </PopoverContent>
1212
+ </Popover>
1213
+ </div>
1214
+ <draggable
1215
+ v-if="slotProps.workingDoc?.structure && slotProps.workingDoc.structure.length > 0"
1216
+ v-model="slotProps.workingDoc.structure"
1217
+ item-key="id"
1218
+ :disabled="true"
1219
+ >
1220
+ <template #item="{ element: row, index: rowIndex }">
1221
+ <div class="space-y-2">
1222
+ <div v-if="state.editMode" class="flex px-4 flex-wrap items-center gap-2 justify-between">
1223
+ <div class="flex flex-wrap items-center gap-2">
1224
+ <edge-shad-button
1225
+ variant="outline"
1226
+ size="icon"
1227
+ class="h-8 w-8"
1228
+ :disabled="rowIndex === 0"
1229
+ @click="moveRow(slotProps.workingDoc, rowIndex, -1, false)"
1230
+ >
1231
+ <ArrowUp class="h-4 w-4" />
1232
+ </edge-shad-button>
1233
+ <edge-shad-button
1234
+ variant="outline"
1235
+ size="icon"
1236
+ class="h-8 w-8"
1237
+ :disabled="rowIndex === (slotProps.workingDoc?.structure?.length || 0) - 1"
1238
+ @click="moveRow(slotProps.workingDoc, rowIndex, 1, false)"
1239
+ >
1240
+ <ArrowDown class="h-4 w-4" />
1241
+ </edge-shad-button>
1242
+ <edge-shad-button variant="outline" size="icon" class="h-8 w-8" @click="openRowSettings(row, false)">
1243
+ <Settings class="h-4 w-4" />
1244
+ </edge-shad-button>
1245
+ </div>
1246
+ <edge-shad-button variant="destructive" size="icon" class="text-white" @click="slotProps.workingDoc.structure.splice(rowIndex, 1); cleanupOrphanBlocks(slotProps.workingDoc, false)">
1247
+ <Trash class="h-4 w-4" />
1248
+ </edge-shad-button>
1249
+ </div>
1250
+ <div
1251
+ class="mx-auto"
1252
+ :class="[rowWidthClass(row.width), backgroundClass(row.background), state.editMode ? 'shadow-sm border border-gray-200/70 p-4' : 'shadow-none border-0 p-0']"
1253
+ :style="rowBackgroundStyle(row.background)"
1254
+ >
1255
+ <div :class="[rowGridClass(row), rowVerticalAlignClass(row)]" :style="rowGridStyle(row)">
1256
+ <div
1257
+ v-for="(column, colIndex) in row.columns"
1258
+ :key="column.id || colIndex"
1259
+ class="space-y-2"
1260
+ :class="[state.editMode ? 'rounded-md bg-white/80 p-3 border border-dashed border-gray-200' : '', columnMobileOrderClass(row, colIndex)]"
1261
+ :style="{ ...columnSpanStyle(column), ...columnMobileOrderStyle(row, colIndex) }"
1262
+ >
1263
+ <edge-button-divider v-if="state.editMode" class="my-1">
1264
+ <edge-cms-block-picker :site-id="props.site" :theme="theme" @pick="(block) => addBlockToColumn(rowIndex, colIndex, 0, block, slotProps, false)" />
1265
+ </edge-button-divider>
1266
+ <draggable
1267
+ v-model="column.blocks"
1268
+ :group="{ name: 'page-blocks', pull: true, put: true }"
1269
+ :item-key="blockKey"
1270
+ handle=".block-drag-handle"
1271
+ ghost-class="block-ghost"
1272
+ chosen-class="block-dragging"
1273
+ drag-class="block-dragging"
1274
+ >
1275
+ <template #item="{ element: blockId, index: blockPosition }">
1276
+ <div class="space-y-2">
1277
+ <div :key="blockId" class="relative group">
1278
+ <edge-cms-block
1279
+ v-if="blockIndex(slotProps.workingDoc, blockId, false) !== -1"
1280
+ v-model="slotProps.workingDoc.content[blockIndex(slotProps.workingDoc, blockId, false)]"
1281
+ :site-id="props.site"
1282
+ :edit-mode="state.editMode"
1283
+ :viewport-mode="previewViewportMode"
1284
+ :block-id="blockId"
1285
+ :theme="theme"
1286
+ @delete="(block) => deleteBlock(block, slotProps)"
1287
+ />
1288
+ <div
1289
+ v-if="state.editMode"
1290
+ class="block-drag-handle pointer-events-none absolute inset-x-0 top-2 flex justify-center opacity-0 transition group-hover:opacity-100 z-30"
1291
+ >
1292
+ <div class="pointer-events-auto inline-flex items-center justify-center rounded-full bg-white/90 shadow px-2 py-1 text-gray-700 cursor-grab">
1293
+ <Grip class="w-4 h-4" />
1294
+ </div>
1295
+ </div>
1296
+ </div>
1297
+ <div v-if="state.editMode && column.blocks.length > blockPosition + 1" class="w-full">
1298
+ <edge-button-divider class="my-2">
1299
+ <edge-cms-block-picker :site-id="props.site" :theme="theme" @pick="(block) => addBlockToColumn(rowIndex, colIndex, blockPosition + 1, block, slotProps, false)" />
1300
+ </edge-button-divider>
1301
+ </div>
1302
+ </div>
1303
+ </template>
1304
+ </draggable>
1305
+ <edge-button-divider v-if="state.editMode && column.blocks.length > 0" class="my-1">
1306
+ <edge-cms-block-picker :site-id="props.site" :theme="theme" @pick="(block) => addBlockToColumn(rowIndex, colIndex, column.blocks.length, block, slotProps, false)" />
1307
+ </edge-button-divider>
1308
+ </div>
1309
+ </div>
1310
+ </div>
1311
+ <edge-button-divider
1312
+ v-if="state.editMode && rowIndex < (slotProps.workingDoc?.structure?.length || 0) - 1"
1313
+ class="my-2"
1314
+ >
1315
+ <Popover v-model:open="state.addRowPopoverOpen.listBetween[row.id]">
1316
+ <PopoverTrigger as-child>
1317
+ <edge-shad-button class="bg-secondary text-primary hover:bg-primary/10 hover:text-primary text-xs h-[32px]">
1318
+ Add Row
1319
+ </edge-shad-button>
1320
+ </PopoverTrigger>
1321
+ <PopoverContent class="w-[360px]">
1322
+ <div class="grid grid-cols-2 gap-2">
1323
+ <button
1324
+ v-for="option in LAYOUT_OPTIONS"
1325
+ :key="option.id"
1326
+ type="button"
1327
+ class="border rounded-md p-2 transition bg-white hover:border-primary text-left w-full"
1328
+ :class="isLayoutSelected(option.id, false) ? 'border-primary ring-1 ring-primary/40' : 'border-gray-200'"
1329
+ @click="selectLayout(option.spans, false); addRowAndClose(slotProps.workingDoc, option.id, rowIndex + 1, false, 'between', row.id)"
1330
+ >
1331
+ <div class="text-[11px] font-medium mb-1">
1332
+ {{ option.label }}
1333
+ </div>
1334
+ <div class="w-full h-8 grid gap-[2px]" style="grid-template-columns: repeat(6, minmax(0, 1fr));">
1335
+ <div
1336
+ v-for="(span, idx) in option.spans"
1337
+ :key="idx"
1338
+ class="bg-gray-200 rounded-sm"
1339
+ :style="{ gridColumn: `span ${span} / span ${span}` }"
1340
+ />
1341
+ </div>
1342
+ </button>
1343
+ </div>
1344
+ </PopoverContent>
1345
+ </Popover>
1346
+ </edge-button-divider>
1347
+ </div>
1348
+ </template>
1349
+ </draggable>
1350
+ <edge-button-divider v-if="state.editMode && slotProps.workingDoc?.structure && slotProps.workingDoc.structure.length > 0" class="my-2">
1351
+ <Popover v-model:open="state.addRowPopoverOpen.listBottom">
1352
+ <PopoverTrigger as-child>
1353
+ <edge-shad-button class="bg-secondary text-primary hover:bg-primary/10 hover:text-primary text-xs h-[32px]">
1354
+ Add Row
1355
+ </edge-shad-button>
1356
+ </PopoverTrigger>
1357
+ <PopoverContent class="w-[360px]">
1358
+ <div class="grid grid-cols-2 gap-2">
1359
+ <button
1360
+ v-for="option in LAYOUT_OPTIONS"
1361
+ :key="option.id"
1362
+ type="button"
1363
+ class="border rounded-md p-2 transition bg-white hover:border-primary text-left w-full"
1364
+ :class="isLayoutSelected(option.id, false) ? 'border-primary ring-1 ring-primary/40' : 'border-gray-200'"
1365
+ @click="selectLayout(option.spans, false); addRowAndClose(slotProps.workingDoc, option.id, slotProps.workingDoc.structure.length, false, 'bottom')"
1366
+ >
1367
+ <div class="text-[11px] font-medium mb-1">
1368
+ {{ option.label }}
1369
+ </div>
1370
+ <div class="w-full h-8 grid gap-[2px]" style="grid-template-columns: repeat(6, minmax(0, 1fr));">
1371
+ <div
1372
+ v-for="(span, idx) in option.spans"
1373
+ :key="idx"
1374
+ class="bg-gray-200 rounded-sm"
1375
+ :style="{ gridColumn: `span ${span} / span ${span}` }"
1376
+ />
1377
+ </div>
1378
+ </button>
1379
+ </div>
1380
+ </PopoverContent>
1381
+ </Popover>
1382
+ </edge-button-divider>
1383
+ </div>
1384
+ </TabsContent>
1385
+ <TabsContent value="post">
1386
+ <Separator class="my-4" />
1387
+ <div
1388
+ :key="`${selectedThemeId}-post`"
1389
+ class="w-full mx-auto bg-card border border-border rounded-lg shadow-sm md:shadow-md p-4 space-y-6"
1390
+ :class="{ 'transition-all duration-300': !state.editMode }"
1391
+ :style="previewViewportStyle"
1392
+ >
1393
+ <edge-button-divider v-if="state.editMode" class="my-2">
1394
+ <Popover v-model:open="state.addRowPopoverOpen.postTop">
1395
+ <PopoverTrigger as-child>
1396
+ <edge-shad-button class="bg-secondary hover:text-primary/50 text-xs h-[32px] text-primary">
1397
+ Add Row
1398
+ </edge-shad-button>
1399
+ </PopoverTrigger>
1400
+ <PopoverContent class="w-[360px]">
1401
+ <div class="grid grid-cols-2 gap-2">
1402
+ <button
1403
+ v-for="option in LAYOUT_OPTIONS"
1404
+ :key="option.id"
1405
+ type="button"
1406
+ class="border rounded-md p-2 transition bg-white hover:border-primary text-left w-full"
1407
+ :class="isLayoutSelected(option.id, true) ? 'border-primary ring-1 ring-primary/40' : 'border-gray-200'"
1408
+ @click="selectLayout(option.spans, true); addRowAndClose(slotProps.workingDoc, option.id, 0, true, 'top')"
1409
+ >
1410
+ <div class="text-[11px] font-medium mb-1">
1411
+ {{ option.label }}
1412
+ </div>
1413
+ <div class="w-full h-8 grid gap-[2px]" style="grid-template-columns: repeat(6, minmax(0, 1fr));">
1414
+ <div
1415
+ v-for="(span, idx) in option.spans"
1416
+ :key="idx"
1417
+ class="bg-gray-200 rounded-sm"
1418
+ :style="{ gridColumn: `span ${span} / span ${span}` }"
1419
+ />
1420
+ </div>
1421
+ </button>
1422
+ </div>
1423
+ </PopoverContent>
1424
+ </Popover>
1425
+ </edge-button-divider>
1426
+ <div
1427
+ v-if="(!slotProps.workingDoc?.postStructure || slotProps.workingDoc.postStructure.length === 0)"
1428
+ class="flex items-center justify-between border border-dashed border-gray-300 rounded-md px-4 py-3 bg-gray-50"
1429
+ >
1430
+ <div class="text-sm text-gray-700">
1431
+ No rows yet. Add your first row to start building.
1432
+ </div>
1433
+ <Popover v-if="state.editMode" v-model:open="state.addRowPopoverOpen.postEmpty">
1434
+ <PopoverTrigger as-child>
1435
+ <edge-shad-button class="bg-secondary hover:text-primary/50 text-xs h-[32px] text-primary">
1436
+ Add Row
1437
+ </edge-shad-button>
1438
+ </PopoverTrigger>
1439
+ <PopoverContent class="w-[360px]">
1440
+ <div class="grid grid-cols-2 gap-2">
1441
+ <button
1442
+ v-for="option in LAYOUT_OPTIONS"
1443
+ :key="option.id"
1444
+ type="button"
1445
+ class="border rounded-md p-2 transition bg-white hover:border-primary text-left w-full"
1446
+ :class="isLayoutSelected(option.id, true) ? 'border-primary ring-1 ring-primary/40' : 'border-gray-200'"
1447
+ @click="selectLayout(option.spans, true); addRowAndClose(slotProps.workingDoc, option.id, 0, true, 'empty')"
1448
+ >
1449
+ <div class="text-[11px] font-medium mb-1">
1450
+ {{ option.label }}
1451
+ </div>
1452
+ <div class="w-full h-8 grid gap-[2px]" style="grid-template-columns: repeat(6, minmax(0, 1fr));">
1453
+ <div
1454
+ v-for="(span, idx) in option.spans"
1455
+ :key="idx"
1456
+ class="bg-gray-200 rounded-sm"
1457
+ :style="{ gridColumn: `span ${span} / span ${span}` }"
1458
+ />
1459
+ </div>
1460
+ </button>
1461
+ </div>
1462
+ </PopoverContent>
1463
+ </Popover>
1464
+ </div>
1465
+ <draggable
1466
+ v-if="slotProps.workingDoc?.postStructure && slotProps.workingDoc.postStructure.length > 0"
1467
+ v-model="slotProps.workingDoc.postStructure"
1468
+ item-key="id"
1469
+ :disabled="true"
1470
+ >
1471
+ <template #item="{ element: row, index: rowIndex }">
1472
+ <div class="space-y-2">
1473
+ <div v-if="state.editMode" class="flex flex-wrap items-center gap-2 justify-between">
1474
+ <div class="flex flex-wrap items-center gap-2">
1475
+ <edge-shad-button
1476
+ variant="outline"
1477
+ size="icon"
1478
+ class="h-8 w-8"
1479
+ :disabled="rowIndex === 0"
1480
+ @click="moveRow(slotProps.workingDoc, rowIndex, -1, true)"
1481
+ >
1482
+ <ArrowUp class="h-4 w-4" />
1483
+ </edge-shad-button>
1484
+ <edge-shad-button
1485
+ variant="outline"
1486
+ size="icon"
1487
+ class="h-8 w-8"
1488
+ :disabled="rowIndex === (slotProps.workingDoc?.postStructure?.length || 0) - 1"
1489
+ @click="moveRow(slotProps.workingDoc, rowIndex, 1, true)"
1490
+ >
1491
+ <ArrowDown class="h-4 w-4" />
1492
+ </edge-shad-button>
1493
+ <edge-shad-button variant="outline" size="icon" class="h-8 w-8" @click="openRowSettings(row, true)">
1494
+ <Settings class="h-4 w-4" />
1495
+ </edge-shad-button>
1496
+ </div>
1497
+ <edge-shad-button variant="destructive" size="icon" class="text-white" @click="slotProps.workingDoc.postStructure.splice(rowIndex, 1); cleanupOrphanBlocks(slotProps.workingDoc, true)">
1498
+ <Trash class="h-4 w-4" />
1499
+ </edge-shad-button>
1500
+ </div>
1501
+ <div
1502
+ class="mx-auto"
1503
+ :class="[rowWidthClass(row.width), backgroundClass(row.background), state.editMode ? 'shadow-sm border border-gray-200/70 p-4' : 'shadow-none border-0 p-0']"
1504
+ :style="rowBackgroundStyle(row.background)"
1505
+ >
1506
+ <div :class="[rowGridClass(row), rowVerticalAlignClass(row)]" :style="rowGridStyle(row)">
1507
+ <div
1508
+ v-for="(column, colIndex) in row.columns"
1509
+ :key="column.id || colIndex"
1510
+ class="space-y-2"
1511
+ :class="[state.editMode ? 'rounded-md bg-white/80 p-3 border border-dashed border-gray-200' : '', columnMobileOrderClass(row, colIndex)]"
1512
+ :style="{ ...columnSpanStyle(column), ...columnMobileOrderStyle(row, colIndex) }"
1513
+ >
1514
+ <edge-button-divider v-if="state.editMode" class="my-1">
1515
+ <edge-cms-block-picker :site-id="props.site" :theme="theme" @pick="(block) => addBlockToColumn(rowIndex, colIndex, 0, block, slotProps, true)" />
1516
+ </edge-button-divider>
1517
+ <draggable
1518
+ v-model="column.blocks"
1519
+ :group="{ name: 'post-blocks', pull: true, put: true }"
1520
+ :item-key="blockKey"
1521
+ handle=".block-drag-handle"
1522
+ ghost-class="block-ghost"
1523
+ chosen-class="block-dragging"
1524
+ drag-class="block-dragging"
1525
+ >
1526
+ <template #item="{ element: blockId, index: blockPosition }">
1527
+ <div class="space-y-2">
1528
+ <div :key="blockId" class="relative group">
1529
+ <edge-cms-block
1530
+ v-if="blockIndex(slotProps.workingDoc, blockId, true) !== -1"
1531
+ v-model="slotProps.workingDoc.postContent[blockIndex(slotProps.workingDoc, blockId, true)]"
1532
+ :edit-mode="state.editMode"
1533
+ :viewport-mode="previewViewportMode"
1534
+ :block-id="blockId"
1535
+ :theme="theme"
1536
+ :site-id="props.site"
1537
+ @delete="(block) => deleteBlock(block, slotProps, true)"
1538
+ />
1539
+ <div
1540
+ v-if="state.editMode"
1541
+ class="block-drag-handle pointer-events-none absolute inset-x-0 top-2 flex justify-center opacity-0 transition group-hover:opacity-100 z-30"
1542
+ >
1543
+ <div class="pointer-events-auto inline-flex items-center justify-center rounded-full bg-white/90 shadow px-2 py-1 text-gray-700 cursor-grab">
1544
+ <Grip class="w-4 h-4" />
1545
+ </div>
1546
+ </div>
1547
+ </div>
1548
+ <div v-if="state.editMode && column.blocks.length > blockPosition + 1" class="w-full">
1549
+ <edge-button-divider class="my-2">
1550
+ <edge-cms-block-picker :site-id="props.site" :theme="theme" @pick="(block) => addBlockToColumn(rowIndex, colIndex, blockPosition + 1, block, slotProps, true)" />
1551
+ </edge-button-divider>
1552
+ </div>
1553
+ </div>
1554
+ </template>
1555
+ </draggable>
1556
+ <edge-button-divider v-if="state.editMode && column.blocks.length > 0" class="my-1">
1557
+ <edge-cms-block-picker :site-id="props.site" :theme="theme" @pick="(block) => addBlockToColumn(rowIndex, colIndex, column.blocks.length, block, slotProps, true)" />
1558
+ </edge-button-divider>
1559
+ </div>
1560
+ </div>
1561
+ </div>
1562
+ <edge-button-divider
1563
+ v-if="state.editMode && rowIndex < (slotProps.workingDoc?.postStructure?.length || 0) - 1"
1564
+ class="my-2"
1565
+ >
1566
+ <Popover v-model:open="state.addRowPopoverOpen.postBetween[row.id]">
1567
+ <PopoverTrigger as-child>
1568
+ <edge-shad-button class="bg-secondary hover:text-primary/50 text-xs h-[32px] text-primary">
1569
+ Add Row
1570
+ </edge-shad-button>
1571
+ </PopoverTrigger>
1572
+ <PopoverContent class="w-[360px]">
1573
+ <div class="grid grid-cols-2 gap-2">
1574
+ <button
1575
+ v-for="option in LAYOUT_OPTIONS"
1576
+ :key="option.id"
1577
+ type="button"
1578
+ class="border rounded-md p-2 transition bg-white hover:border-primary text-left w-full"
1579
+ :class="isLayoutSelected(option.id, true) ? 'border-primary ring-1 ring-primary/40' : 'border-gray-200'"
1580
+ @click="selectLayout(option.spans, true); addRowAndClose(slotProps.workingDoc, option.id, rowIndex + 1, true, 'between', row.id)"
1581
+ >
1582
+ <div class="text-[11px] font-medium mb-1">
1583
+ {{ option.label }}
1584
+ </div>
1585
+ <div class="w-full h-8 grid gap-[2px]" style="grid-template-columns: repeat(6, minmax(0, 1fr));">
1586
+ <div
1587
+ v-for="(span, idx) in option.spans"
1588
+ :key="idx"
1589
+ class="bg-gray-200 rounded-sm"
1590
+ :style="{ gridColumn: `span ${span} / span ${span}` }"
1591
+ />
1592
+ </div>
1593
+ </button>
1594
+ </div>
1595
+ </PopoverContent>
1596
+ </Popover>
1597
+ </edge-button-divider>
1598
+ </div>
1599
+ </template>
1600
+ </draggable>
1601
+ <edge-button-divider v-if="state.editMode && slotProps.workingDoc?.postStructure && slotProps.workingDoc.postStructure.length > 0" class="my-2">
1602
+ <Popover v-model:open="state.addRowPopoverOpen.postBottom">
1603
+ <PopoverTrigger as-child>
1604
+ <edge-shad-button class="bg-secondary text-primary hover:bg-primary/10 hover:text-primary text-xs h-[32px]">
1605
+ Add Row
1606
+ </edge-shad-button>
1607
+ </PopoverTrigger>
1608
+ <PopoverContent class="w-[360px]">
1609
+ <div class="grid grid-cols-2 gap-2">
1610
+ <button
1611
+ v-for="option in LAYOUT_OPTIONS"
1612
+ :key="option.id"
1613
+ type="button"
1614
+ class="border rounded-md p-2 transition bg-white hover:border-primary text-left w-full"
1615
+ :class="isLayoutSelected(option.id, true) ? 'border-primary ring-1 ring-primary/40' : 'border-gray-200'"
1616
+ @click="selectLayout(option.spans, true); addRowAndClose(slotProps.workingDoc, option.id, slotProps.workingDoc.postStructure.length, true, 'bottom')"
1617
+ >
1618
+ <div class="text-[11px] font-medium mb-1">
1619
+ {{ option.label }}
1620
+ </div>
1621
+ <div class="w-full h-8 grid gap-[2px]" style="grid-template-columns: repeat(6, minmax(0, 1fr));">
1622
+ <div
1623
+ v-for="(span, idx) in option.spans"
1624
+ :key="idx"
1625
+ class="bg-gray-200 rounded-sm"
1626
+ :style="{ gridColumn: `span ${span} / span ${span}` }"
1627
+ />
1628
+ </div>
1629
+ </button>
1630
+ </div>
1631
+ </PopoverContent>
1632
+ </Popover>
1633
+ </edge-button-divider>
1634
+ </div>
1635
+ </TabsContent>
1636
+ </Tabs>
1637
+ <Sheet v-model:open="state.rowSettings.open">
1638
+ <SheetContent side="right" class="w-full sm:max-w-md flex flex-col h-full">
1639
+ <SheetHeader>
1640
+ <SheetTitle>Row Settings</SheetTitle>
1641
+ <SheetDescription>Adjust layout and spacing for this row.</SheetDescription>
1642
+ </SheetHeader>
1643
+ <div v-if="activeRowSettingsRow" class="mt-6 space-y-5 flex-1 overflow-y-auto">
1644
+ <div class="space-y-2">
1645
+ <edge-shad-select
1646
+ v-model="state.rowSettings.draft.width"
1647
+ label="Width"
1648
+ name="row-width-setting"
1649
+ :items="ROW_WIDTH_OPTIONS"
1650
+ class="w-full"
1651
+ placeholder="Row width"
1652
+ />
1653
+ </div>
1654
+ <div class="space-y-2">
1655
+ <edge-shad-select
1656
+ v-model="state.rowSettings.draft.gap"
1657
+ label="Gap"
1658
+ name="row-gap-setting"
1659
+ :items="ROW_GAP_OPTIONS"
1660
+ class="w-full"
1661
+ placeholder="Row gap"
1662
+ />
1663
+ </div>
1664
+ <div class="space-y-2">
1665
+ <edge-shad-select
1666
+ v-model="state.rowSettings.draft.verticalAlign"
1667
+ label="Vertical Alignment"
1668
+ name="row-vertical-align-setting"
1669
+ :items="ROW_VERTICAL_ALIGN_OPTIONS"
1670
+ class="w-full"
1671
+ placeholder="Vertical align"
1672
+ />
1673
+ </div>
1674
+ <div class="space-y-2">
1675
+ <edge-shad-select
1676
+ v-model="state.rowSettings.draft.mobileOrder"
1677
+ label="Mobile Stack Order"
1678
+ name="row-mobile-order-setting"
1679
+ :items="ROW_MOBILE_STACK_OPTIONS"
1680
+ class="w-full"
1681
+ placeholder="Mobile stack order"
1682
+ />
1683
+ </div>
1684
+ <div v-if="themeColorOptions.length" class="space-y-2">
1685
+ <edge-shad-select
1686
+ v-model="state.rowSettings.draft.background"
1687
+ label="Background"
1688
+ name="row-background-setting"
1689
+ :items="themeColorOptions"
1690
+ class="w-full"
1691
+ placeholder="Background"
1692
+ />
1693
+ </div>
1694
+ </div>
1695
+ <SheetFooter class="pt-2 flex justify-between mt-auto">
1696
+ <edge-shad-button variant="destructive" class="text-white" @click="state.rowSettings.open = false">
1697
+ Cancel
1698
+ </edge-shad-button>
1699
+ <edge-shad-button class=" bg-slate-800 hover:bg-slate-400 w-full" @click="saveRowSettings">
1700
+ Save changes
1701
+ </edge-shad-button>
1702
+ </SheetFooter>
1703
+ </SheetContent>
1704
+ </Sheet>
1705
+ </template>
1706
+ </edge-editor>
1707
+ <edge-shad-dialog v-model="state.showUnpublishedChangesDialog">
1708
+ <DialogContent class="max-w-2xl">
1709
+ <DialogHeader>
1710
+ <DialogTitle class="text-left">
1711
+ Unpublished Changes
1712
+ </DialogTitle>
1713
+ <DialogDescription class="text-left">
1714
+ Review what changed since the last publish. Last Published: {{ lastPublishedTime(page) }}
1715
+ </DialogDescription>
1716
+ </DialogHeader>
1717
+ <div v-if="unpublishedChangeDetails.length" class="space-y-3 mt-2">
1718
+ <div
1719
+ v-for="change in unpublishedChangeDetails"
1720
+ :key="change.key"
1721
+ class="rounded-md border border-gray-200 dark:border-white/10 bg-secondary p-3 text-left"
1722
+ >
1723
+ <div class="text-sm font-semibold text-primary mb-2">
1724
+ {{ change.label }}
1725
+ </div>
1726
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
1727
+ <div class="rounded border border-gray-200 dark:border-white/15 bg-white/80 dark:bg-gray-800 p-2">
1728
+ <div class="text-[11px] uppercase tracking-wide text-gray-500 mb-1">
1729
+ Published
1730
+ </div>
1731
+ <div class="whitespace-pre-wrap break-words text-gray-900 dark:text-gray-100">
1732
+ {{ change.published }}
1733
+ </div>
1734
+ </div>
1735
+ <div class="rounded border border-gray-200 dark:border-white/15 bg-white/80 dark:bg-gray-800 p-2">
1736
+ <div class="text-[11px] uppercase tracking-wide text-gray-500 mb-1">
1737
+ Draft
1738
+ </div>
1739
+ <div class="whitespace-pre-wrap break-words text-gray-900 dark:text-gray-100">
1740
+ {{ change.draft }}
1741
+ </div>
1742
+ </div>
1743
+ </div>
1744
+ <div v-if="change.details?.length" class="mt-2 text-sm text-gray-700 dark:text-gray-300">
1745
+ <ul class="list-disc pl-5 space-y-1">
1746
+ <li v-for="(detail, detailIndex) in change.details" :key="`${change.key}-${detailIndex}`">
1747
+ {{ detail }}
1748
+ </li>
1749
+ </ul>
1750
+ </div>
1751
+ </div>
1752
+ </div>
1753
+ <div v-else class="text-sm text-gray-600 dark:text-gray-300 text-left">
1754
+ No unpublished differences detected.
1755
+ </div>
1756
+ <DialogFooter class="pt-4">
1757
+ <edge-shad-button class="w-full" variant="outline" @click="state.showUnpublishedChangesDialog = false">
1758
+ Close
1759
+ </edge-shad-button>
1760
+ </DialogFooter>
1761
+ </DialogContent>
1762
+ </edge-shad-dialog>
1763
+ </template>
1764
+
1765
+ <style scoped>
1766
+ .block-ghost {
1767
+ opacity: 0.35;
1768
+ pointer-events: none;
1769
+ filter: grayscale(0.4);
1770
+ }
1771
+
1772
+ .block-dragging,
1773
+ .block-dragging * {
1774
+ user-select: none !important;
1775
+ cursor: grabbing !important;
1776
+ }
1777
+
1778
+ .block-drag-handle {
1779
+ cursor: grab;
1780
+ }
1781
+
1782
+ .block-drag-handle:active {
1783
+ cursor: grabbing;
1784
+ }
1785
+ </style>