@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.
- package/README.md +55 -20
- package/{agent.md → agents.md} +2 -0
- package/bin/cli.js +6 -6
- package/edge/components/auth/login.vue +384 -0
- package/edge/components/auth/register.vue +396 -0
- package/edge/components/auth.vue +108 -0
- package/edge/components/autoFileUpload.vue +215 -0
- package/edge/components/billing.vue +8 -0
- package/edge/components/buttonDivider.vue +14 -0
- package/edge/components/chip.vue +34 -0
- package/edge/components/clipboardButton.vue +42 -0
- package/edge/components/cms/block.vue +529 -0
- package/edge/components/cms/blockApi.vue +212 -0
- package/edge/components/cms/blockEditor.vue +725 -0
- package/edge/components/cms/blockInput.vue +66 -0
- package/edge/components/cms/blockPicker.vue +486 -0
- package/edge/components/cms/blockRender.vue +78 -0
- package/edge/components/cms/blockSheetContent.vue +28 -0
- package/edge/components/cms/codeEditor.vue +466 -0
- package/edge/components/cms/fontUpload.vue +327 -0
- package/edge/components/cms/htmlContent.vue +807 -0
- package/edge/components/cms/init_blocks/api_with_subarrays.html +17 -0
- package/edge/components/cms/init_blocks/array_with_collection.html +7 -0
- package/edge/components/cms/init_blocks/array_with_objects.html +7 -0
- package/edge/components/cms/init_blocks/carousel.html +103 -0
- package/edge/components/cms/init_blocks/contact_us.html +69 -0
- package/edge/components/cms/init_blocks/content_with_left_image.html +27 -0
- package/edge/components/cms/init_blocks/footer.html +24 -0
- package/edge/components/cms/init_blocks/header_divider.html +7 -0
- package/edge/components/cms/init_blocks/hero.html +35 -0
- package/edge/components/cms/init_blocks/hero_carousel.html +52 -0
- package/edge/components/cms/init_blocks/newsletter.html +117 -0
- package/edge/components/cms/init_blocks/post_content.html +7 -0
- package/edge/components/cms/init_blocks/post_title_header.html +21 -0
- package/edge/components/cms/init_blocks/posts_list.html +20 -0
- package/edge/components/cms/init_blocks/properties_showcase.html +100 -0
- package/edge/components/cms/init_blocks/property_carousel.html +59 -0
- package/edge/components/cms/init_blocks/property_detail.html +112 -0
- package/edge/components/cms/init_blocks/property_detail_header.html +34 -0
- package/edge/components/cms/init_blocks/property_results.html +137 -0
- package/edge/components/cms/init_blocks/property_search.html +75 -0
- package/edge/components/cms/init_blocks/simple_array.html +7 -0
- package/edge/components/cms/mediaCard.vue +116 -0
- package/edge/components/cms/mediaManager.vue +386 -0
- package/edge/components/cms/menu.vue +1103 -0
- package/edge/components/cms/optionsSelect.vue +107 -0
- package/edge/components/cms/page.vue +1785 -0
- package/edge/components/cms/posts.vue +1083 -0
- package/edge/components/cms/site.vue +1298 -0
- package/edge/components/cms/themeDefaultMenu.vue +548 -0
- package/edge/components/cms/themeEditor.vue +426 -0
- package/edge/components/dashboard.vue +776 -0
- package/edge/components/editor.vue +671 -0
- package/edge/components/fileTree.vue +72 -0
- package/edge/components/files.vue +89 -0
- package/edge/components/formSubtypes/myOrgs.vue +214 -0
- package/edge/components/formSubtypes/users.vue +336 -0
- package/edge/components/functionChips.vue +57 -0
- package/edge/components/gError.vue +98 -0
- package/edge/components/gHelper.vue +67 -0
- package/edge/components/gInput.vue +1331 -0
- package/edge/components/loggingIn.vue +41 -0
- package/edge/components/menu.vue +137 -0
- package/edge/components/menuContent.vue +132 -0
- package/edge/components/myAccount.vue +317 -0
- package/edge/components/myOrganizations.vue +75 -0
- package/edge/components/myProfile.vue +122 -0
- package/edge/components/orgSwitcher.vue +25 -0
- package/edge/components/organizationMembers.vue +522 -0
- package/edge/components/organizationSettings.vue +271 -0
- package/edge/components/shad/breadcrumbs.vue +35 -0
- package/edge/components/shad/button.vue +43 -0
- package/edge/components/shad/checkbox.vue +73 -0
- package/edge/components/shad/combobox.vue +238 -0
- package/edge/components/shad/datepicker.vue +184 -0
- package/edge/components/shad/dialog.vue +32 -0
- package/edge/components/shad/dropdownMenu.vue +54 -0
- package/edge/components/shad/dropdownMenuItem.vue +21 -0
- package/edge/components/shad/form.vue +59 -0
- package/edge/components/shad/html.vue +877 -0
- package/edge/components/shad/input.vue +139 -0
- package/edge/components/shad/number.vue +109 -0
- package/edge/components/shad/select.vue +151 -0
- package/edge/components/shad/selectTags.vue +278 -0
- package/edge/components/shad/switch.vue +67 -0
- package/edge/components/shad/tags.vue +137 -0
- package/edge/components/shad/textarea.vue +102 -0
- package/edge/components/shad/typeMoney.vue +167 -0
- package/edge/components/sideBar.vue +288 -0
- package/edge/components/sideBarContent.vue +268 -0
- package/edge/components/sidebarProvider.vue +33 -0
- package/edge/components/tooltip.vue +16 -0
- package/edge/components/userMenu.vue +148 -0
- package/edge/components/v/alert.vue +59 -0
- package/edge/components/v/alertTitle.vue +18 -0
- package/edge/components/v/card.vue +53 -0
- package/edge/components/v/cardActions.vue +18 -0
- package/edge/components/v/cardText.vue +18 -0
- package/edge/components/v/cardTitle.vue +20 -0
- package/edge/components/v/col.vue +56 -0
- package/edge/components/v/list.vue +46 -0
- package/edge/components/v/listItem.vue +26 -0
- package/edge/components/v/listItemTitle.vue +18 -0
- package/edge/components/v/row.vue +42 -0
- package/edge/components/v/toolbar.vue +24 -0
- package/edge/composables/global.ts +519 -0
- package/edge-pull.sh +2 -0
- package/edge-push.sh +1 -0
- package/edge-status.sh +14 -0
- package/package.json +1 -1
- 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>
|