@edgedev/create-edge-app 1.1.23 → 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 (116) hide show
  1. package/.env +1 -0
  2. package/.env.dev +1 -0
  3. package/README.md +55 -20
  4. package/{agent.md → agents.md} +2 -0
  5. package/bin/cli.js +6 -6
  6. package/edge/components/auth/login.vue +384 -0
  7. package/edge/components/auth/register.vue +396 -0
  8. package/edge/components/auth.vue +108 -0
  9. package/edge/components/autoFileUpload.vue +215 -0
  10. package/edge/components/billing.vue +8 -0
  11. package/edge/components/buttonDivider.vue +14 -0
  12. package/edge/components/chip.vue +34 -0
  13. package/edge/components/clipboardButton.vue +42 -0
  14. package/edge/components/cms/block.vue +529 -0
  15. package/edge/components/cms/blockApi.vue +212 -0
  16. package/edge/components/cms/blockEditor.vue +725 -0
  17. package/edge/components/cms/blockInput.vue +66 -0
  18. package/edge/components/cms/blockPicker.vue +486 -0
  19. package/edge/components/cms/blockRender.vue +78 -0
  20. package/edge/components/cms/blockSheetContent.vue +28 -0
  21. package/edge/components/cms/codeEditor.vue +466 -0
  22. package/edge/components/cms/fontUpload.vue +327 -0
  23. package/edge/components/cms/htmlContent.vue +807 -0
  24. package/edge/components/cms/init_blocks/api_with_subarrays.html +17 -0
  25. package/edge/components/cms/init_blocks/array_with_collection.html +7 -0
  26. package/edge/components/cms/init_blocks/array_with_objects.html +7 -0
  27. package/edge/components/cms/init_blocks/carousel.html +103 -0
  28. package/edge/components/cms/init_blocks/contact_us.html +69 -0
  29. package/edge/components/cms/init_blocks/content_with_left_image.html +27 -0
  30. package/edge/components/cms/init_blocks/footer.html +24 -0
  31. package/edge/components/cms/init_blocks/header_divider.html +7 -0
  32. package/edge/components/cms/init_blocks/hero.html +35 -0
  33. package/edge/components/cms/init_blocks/hero_carousel.html +52 -0
  34. package/edge/components/cms/init_blocks/newsletter.html +117 -0
  35. package/edge/components/cms/init_blocks/post_content.html +7 -0
  36. package/edge/components/cms/init_blocks/post_title_header.html +21 -0
  37. package/edge/components/cms/init_blocks/posts_list.html +20 -0
  38. package/edge/components/cms/init_blocks/properties_showcase.html +100 -0
  39. package/edge/components/cms/init_blocks/property_carousel.html +59 -0
  40. package/edge/components/cms/init_blocks/property_detail.html +112 -0
  41. package/edge/components/cms/init_blocks/property_detail_header.html +34 -0
  42. package/edge/components/cms/init_blocks/property_results.html +137 -0
  43. package/edge/components/cms/init_blocks/property_search.html +75 -0
  44. package/edge/components/cms/init_blocks/simple_array.html +7 -0
  45. package/edge/components/cms/mediaCard.vue +116 -0
  46. package/edge/components/cms/mediaManager.vue +386 -0
  47. package/edge/components/cms/menu.vue +1103 -0
  48. package/edge/components/cms/optionsSelect.vue +107 -0
  49. package/edge/components/cms/page.vue +1785 -0
  50. package/edge/components/cms/posts.vue +1083 -0
  51. package/edge/components/cms/site.vue +1298 -0
  52. package/edge/components/cms/themeDefaultMenu.vue +548 -0
  53. package/edge/components/cms/themeEditor.vue +426 -0
  54. package/edge/components/dashboard.vue +776 -0
  55. package/edge/components/editor.vue +671 -0
  56. package/edge/components/fileTree.vue +72 -0
  57. package/edge/components/files.vue +89 -0
  58. package/edge/components/formSubtypes/myOrgs.vue +214 -0
  59. package/edge/components/formSubtypes/users.vue +336 -0
  60. package/edge/components/functionChips.vue +57 -0
  61. package/edge/components/gError.vue +98 -0
  62. package/edge/components/gHelper.vue +67 -0
  63. package/edge/components/gInput.vue +1331 -0
  64. package/edge/components/loggingIn.vue +41 -0
  65. package/edge/components/menu.vue +137 -0
  66. package/edge/components/menuContent.vue +132 -0
  67. package/edge/components/myAccount.vue +317 -0
  68. package/edge/components/myOrganizations.vue +75 -0
  69. package/edge/components/myProfile.vue +122 -0
  70. package/edge/components/orgSwitcher.vue +25 -0
  71. package/edge/components/organizationMembers.vue +522 -0
  72. package/edge/components/organizationSettings.vue +271 -0
  73. package/edge/components/shad/breadcrumbs.vue +35 -0
  74. package/edge/components/shad/button.vue +43 -0
  75. package/edge/components/shad/checkbox.vue +73 -0
  76. package/edge/components/shad/combobox.vue +238 -0
  77. package/edge/components/shad/datepicker.vue +184 -0
  78. package/edge/components/shad/dialog.vue +32 -0
  79. package/edge/components/shad/dropdownMenu.vue +54 -0
  80. package/edge/components/shad/dropdownMenuItem.vue +21 -0
  81. package/edge/components/shad/form.vue +59 -0
  82. package/edge/components/shad/html.vue +877 -0
  83. package/edge/components/shad/input.vue +139 -0
  84. package/edge/components/shad/number.vue +109 -0
  85. package/edge/components/shad/select.vue +151 -0
  86. package/edge/components/shad/selectTags.vue +278 -0
  87. package/edge/components/shad/switch.vue +67 -0
  88. package/edge/components/shad/tags.vue +137 -0
  89. package/edge/components/shad/textarea.vue +102 -0
  90. package/edge/components/shad/typeMoney.vue +167 -0
  91. package/edge/components/sideBar.vue +288 -0
  92. package/edge/components/sideBarContent.vue +268 -0
  93. package/edge/components/sidebarProvider.vue +33 -0
  94. package/edge/components/tooltip.vue +16 -0
  95. package/edge/components/userMenu.vue +148 -0
  96. package/edge/components/v/alert.vue +59 -0
  97. package/edge/components/v/alertTitle.vue +18 -0
  98. package/edge/components/v/card.vue +53 -0
  99. package/edge/components/v/cardActions.vue +18 -0
  100. package/edge/components/v/cardText.vue +18 -0
  101. package/edge/components/v/cardTitle.vue +20 -0
  102. package/edge/components/v/col.vue +56 -0
  103. package/edge/components/v/list.vue +46 -0
  104. package/edge/components/v/listItem.vue +26 -0
  105. package/edge/components/v/listItemTitle.vue +18 -0
  106. package/edge/components/v/row.vue +42 -0
  107. package/edge/components/v/toolbar.vue +24 -0
  108. package/edge/composables/global.ts +519 -0
  109. package/edge-pull.sh +2 -0
  110. package/edge-push.sh +1 -0
  111. package/edge-status.sh +14 -0
  112. package/firebase.json +5 -2
  113. package/firebase_init.sh +21 -6
  114. package/package.json +1 -1
  115. package/plugins/firebase.client.ts +1 -0
  116. package/edge-components-install.sh +0 -1
@@ -0,0 +1,1103 @@
1
+ <script setup lang="js">
2
+ import { useVModel } from '@vueuse/core'
3
+ import { File, FileCheck, FileCog, FileDown, FileMinus2, FilePen, FilePlus2, FileUp, FileWarning, FileX, Folder, FolderMinus, FolderOpen, FolderPen, FolderPlus } from 'lucide-vue-next'
4
+ import { toTypedSchema } from '@vee-validate/zod'
5
+ import * as z from 'zod'
6
+
7
+ const props = defineProps({
8
+ prevModelValue: {
9
+ type: Object,
10
+ required: false,
11
+ default: () => ({}),
12
+ },
13
+ modelValue: {
14
+ type: Object,
15
+ required: true,
16
+ },
17
+ prevMenu: {
18
+ type: String,
19
+ default: '',
20
+ },
21
+ dataDraggable: {
22
+ type: Boolean,
23
+ default: true,
24
+ },
25
+ prevIndex: {
26
+ type: Number,
27
+ default: -1,
28
+ },
29
+ site: {
30
+ type: String,
31
+ required: true,
32
+ },
33
+ page: {
34
+ type: String,
35
+ required: false,
36
+ default: '',
37
+ },
38
+ isTemplateSite: {
39
+ type: Boolean,
40
+ default: false,
41
+ },
42
+ themeOptions: {
43
+ type: Array,
44
+ default: () => [],
45
+ },
46
+ })
47
+ const emit = defineEmits(['update:modelValue', 'pageSettingsUpdate'])
48
+ const ROOT_MENUS = ['Site Root', 'Not In Menu']
49
+ const router = useRouter()
50
+ const modelValue = useVModel(props, 'modelValue', emit)
51
+ const route = useRoute()
52
+ const edgeFirebase = inject('edgeFirebase')
53
+
54
+ const normalizeForCompare = (value) => {
55
+ if (Array.isArray(value))
56
+ return value.map(normalizeForCompare)
57
+ if (value && typeof value === 'object') {
58
+ return Object.keys(value).sort().reduce((acc, key) => {
59
+ acc[key] = normalizeForCompare(value[key])
60
+ return acc
61
+ }, {})
62
+ }
63
+ return value
64
+ }
65
+
66
+ const stableSerialize = value => JSON.stringify(normalizeForCompare(value))
67
+ const areEqualNormalized = (a, b) => stableSerialize(a) === stableSerialize(b)
68
+
69
+ const orderedMenus = computed(() => {
70
+ const menuEntries = Object.entries(modelValue.value || {}).map(([name, menu], originalIndex) => ({
71
+ name,
72
+ menu,
73
+ originalIndex,
74
+ }))
75
+ const priority = (name) => {
76
+ if (name === 'Site Root')
77
+ return 0
78
+ if (name === 'Not In Menu')
79
+ return 2
80
+ return 1
81
+ }
82
+ return menuEntries.sort((a, b) => priority(a.name) - priority(b.name) || a.originalIndex - b.originalIndex)
83
+ })
84
+
85
+ const pageRouteBase = computed(() => {
86
+ return props.site === 'templates'
87
+ ? '/app/dashboard/templates'
88
+ : `/app/dashboard/sites/${props.site}`
89
+ })
90
+
91
+ const isPublishedPageDiff = (pageId) => {
92
+ const publishedPage = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`]?.[pageId]
93
+ const draftPage = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`]?.[pageId]
94
+ if (!publishedPage && draftPage) {
95
+ return true
96
+ }
97
+ if (publishedPage && !draftPage) {
98
+ return true
99
+ }
100
+ if (publishedPage && draftPage) {
101
+ return !areEqualNormalized(
102
+ {
103
+ content: publishedPage.content,
104
+ postContent: publishedPage.postContent,
105
+ structure: publishedPage.structure,
106
+ postStructure: publishedPage.postStructure,
107
+ metaTitle: publishedPage.metaTitle,
108
+ metaDescription: publishedPage.metaDescription,
109
+ structuredData: publishedPage.structuredData,
110
+ },
111
+ {
112
+ content: draftPage.content,
113
+ postContent: draftPage.postContent,
114
+ structure: draftPage.structure,
115
+ postStructure: draftPage.postStructure,
116
+ metaTitle: draftPage.metaTitle,
117
+ metaDescription: draftPage.metaDescription,
118
+ structuredData: draftPage.structuredData,
119
+ },
120
+ )
121
+ }
122
+ return false
123
+ }
124
+
125
+ const isPublished = (pageId) => {
126
+ const publishedPage = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`]?.[pageId]
127
+ return !!publishedPage
128
+ }
129
+
130
+ const schemas = {
131
+ pages: toTypedSchema(z.object({
132
+ name: z.string({
133
+ required_error: 'Name is required',
134
+ }).min(1, { message: 'Name is required' }),
135
+ tags: z.array(z.string()).optional(),
136
+ allowedThemes: z.array(z.string()).optional(),
137
+ })),
138
+ }
139
+
140
+ const state = reactive({
141
+ addPageDialog: false,
142
+ newPageName: '',
143
+ indexPath: '',
144
+ addMenu: false,
145
+ deletePage: {},
146
+ renameItem: {},
147
+ renameFolderOrPageDialog: false,
148
+ deletePageDialog: false,
149
+ pageSettings: false,
150
+ pageData: {},
151
+ newDocs: {
152
+ pages: {
153
+ name: { value: '' },
154
+ content: { value: [] },
155
+ blockIds: { value: [] },
156
+ tags: { value: [] },
157
+ allowedThemes: { value: [] },
158
+ },
159
+ },
160
+ hasErrors: false,
161
+ templateFilter: 'quick-picks',
162
+ selectedTemplateId: 'blank',
163
+ showTemplatePicker: false,
164
+ })
165
+
166
+ const templateTagItems = computed(() => {
167
+ if (!props.isTemplateSite)
168
+ return []
169
+ const pages
170
+ = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
171
+ const tags = new Set()
172
+ for (const doc of Object.values(pages)) {
173
+ if (Array.isArray(doc?.tags)) {
174
+ for (const tag of doc.tags) {
175
+ const normalized = typeof tag === 'string' ? tag.trim() : ''
176
+ if (normalized && normalized.toLowerCase() !== 'quick picks')
177
+ tags.add(normalized)
178
+ }
179
+ }
180
+ }
181
+ const tagList = Array.from(tags).sort((a, b) => a.localeCompare(b)).map(tag => ({ name: tag, title: tag }))
182
+ return [{ name: 'Quick Picks', title: 'Quick Picks' }, ...tagList]
183
+ })
184
+
185
+ const resetAddPageDialogState = () => {
186
+ state.newPageName = ''
187
+ state.templateFilter = 'quick-picks'
188
+ state.selectedTemplateId = BLANK_TEMPLATE_ID
189
+ state.showTemplatePicker = false
190
+ }
191
+
192
+ watch(() => state.addPageDialog, (open) => {
193
+ if (!open)
194
+ resetAddPageDialogState()
195
+ })
196
+
197
+ onMounted(async () => {
198
+ if (!edgeGlobal.edgeState.organizationDocPath)
199
+ return
200
+ const path = TEMPLATE_COLLECTION_PATH.value
201
+ if (!edgeFirebase.data?.[path])
202
+ await edgeFirebase.startSnapshot(path)
203
+ })
204
+
205
+ const TEMPLATE_COLLECTION_PATH = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/sites/templates/pages`)
206
+
207
+ const templatePagesCollection = computed(() => {
208
+ return edgeFirebase.data?.[TEMPLATE_COLLECTION_PATH.value] || {}
209
+ })
210
+
211
+ const templatePagesList = computed(() => {
212
+ return Object.entries(templatePagesCollection.value).map(([docId, doc]) => ({
213
+ docId,
214
+ ...(doc || {}),
215
+ name: doc?.name || 'Untitled Template',
216
+ tags: Array.isArray(doc?.tags) ? doc.tags : [],
217
+ description: doc?.metaDescription || doc?.description || '',
218
+ content: Array.isArray(doc?.content) ? doc.content : [],
219
+ }))
220
+ })
221
+
222
+ const templateFilterOptions = computed(() => {
223
+ const tagSet = new Set()
224
+ for (const template of templatePagesList.value) {
225
+ for (const tag of template.tags || []) {
226
+ if (!tag)
227
+ continue
228
+ if (tag.toLowerCase() === 'quick picks')
229
+ continue
230
+ tagSet.add(tag)
231
+ }
232
+ }
233
+ const tagOptions = Array.from(tagSet)
234
+ .sort((a, b) => a.localeCompare(b))
235
+ .map(tag => ({ label: tag, value: tag }))
236
+ return [
237
+ { label: 'Quick Picks', value: 'quick-picks' },
238
+ ...tagOptions,
239
+ ]
240
+ })
241
+
242
+ const filterMatchesTemplate = (template, filterValue) => {
243
+ if (filterValue === 'all')
244
+ return true
245
+ if (filterValue === 'quick-picks')
246
+ return template.tags?.some(tag => tag?.toLowerCase() === 'quick picks'.toLowerCase())
247
+ return template.tags?.some(tag => tag === filterValue)
248
+ }
249
+
250
+ const filteredTemplates = computed(() => {
251
+ const templates = templatePagesList.value
252
+ const filterValue = state.templateFilter
253
+ const filtered = templates.filter(template => filterMatchesTemplate(template, filterValue))
254
+ if (filtered.length === 0 && filterValue === 'quick-picks')
255
+ return templates
256
+ if (filtered.length === 0 && filterValue !== 'all')
257
+ return templates
258
+ return filtered
259
+ })
260
+
261
+ watch(filteredTemplates, (templates) => {
262
+ if (state.selectedTemplateId === BLANK_TEMPLATE_ID)
263
+ return
264
+ if (!templates.some(template => template.docId === state.selectedTemplateId))
265
+ state.selectedTemplateId = BLANK_TEMPLATE_ID
266
+ })
267
+
268
+ const BLANK_TEMPLATE_ID = 'blank'
269
+
270
+ const blankTemplateTile = {
271
+ docId: BLANK_TEMPLATE_ID,
272
+ name: 'Blank Page',
273
+ tags: ['Start from scratch'],
274
+ description: 'Create a new page without any blocks.',
275
+ content: [],
276
+ }
277
+
278
+ const templateGridItems = computed(() => {
279
+ return [blankTemplateTile, ...filteredTemplates.value]
280
+ })
281
+
282
+ const hasValidNewPageName = computed(() => !!(state.newPageName && state.newPageName.trim().length))
283
+
284
+ const blocksCollection = computed(() => {
285
+ return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/blocks`] || {}
286
+ })
287
+
288
+ const resolveBlockForPreview = (block) => {
289
+ if (!block)
290
+ return null
291
+ if (block.content)
292
+ return {
293
+ content: block.content,
294
+ values: block.values || {},
295
+ meta: block.meta || {},
296
+ }
297
+ if (block.blockId && blocksCollection.value?.[block.blockId]) {
298
+ const libraryBlock = blocksCollection.value[block.blockId]
299
+ return {
300
+ content: libraryBlock.content,
301
+ values: block.values || libraryBlock.values || {},
302
+ meta: block.meta || libraryBlock.meta || {},
303
+ }
304
+ }
305
+ return null
306
+ }
307
+
308
+ const templateHasBlocks = template => Array.isArray(template?.content) && template.content.length > 0
309
+
310
+ const templatePreviewBlocks = (template) => {
311
+ if (!templateHasBlocks(template))
312
+ return []
313
+ return template.content
314
+ }
315
+
316
+ const renameFolderOrPageShow = (item) => {
317
+ // Work on a copy so edits in the dialog do not mutate the live menu entry.
318
+ state.renameItem = edgeGlobal.dupObject(item || {})
319
+ state.renameItem.previousName = item?.name
320
+ state.renameFolderOrPageDialog = true
321
+ }
322
+
323
+ const addPageShow = (menuName, isMenu = false) => {
324
+ state.addMenu = isMenu
325
+ state.menuName = menuName
326
+ resetAddPageDialogState()
327
+ state.addPageDialog = true
328
+ }
329
+
330
+ const deletePageShow = (page) => {
331
+ state.deletePage = page
332
+ state.deletePageDialog = true
333
+ }
334
+
335
+ const collectRootLevelSlugs = (excludeName = '') => {
336
+ const slugs = new Set()
337
+ if (!props.prevMenu) {
338
+ for (const root of ROOT_MENUS) {
339
+ const arr = modelValue.value?.[root] || []
340
+ for (const entry of arr) {
341
+ // Top-level page at "/<slug>"
342
+ if (typeof entry.item === 'string') {
343
+ if (entry.name && entry.name !== excludeName)
344
+ slugs.add(entry.name)
345
+ }
346
+ // Top-level folder at "/<folder>/*"
347
+ else if (entry && typeof entry.item === 'object') {
348
+ const key = Object.keys(entry.item)[0]
349
+ if (key && key !== excludeName)
350
+ slugs.add(key)
351
+ }
352
+ }
353
+ }
354
+ }
355
+ else {
356
+ if (state.renameItem.item === '') {
357
+ for (const root of ROOT_MENUS) {
358
+ const arr = props.prevModelValue?.[root] || []
359
+ for (const entry of arr) {
360
+ // Top-level page at "/<slug>"
361
+ if (typeof entry.item === 'string') {
362
+ if (entry.name && entry.name !== excludeName)
363
+ slugs.add(entry.name)
364
+ }
365
+ // Top-level folder at "/<folder>/*"
366
+ else if (entry && typeof entry.item === 'object') {
367
+ const key = Object.keys(entry.item)[0]
368
+ if (key && key !== excludeName)
369
+ slugs.add(key)
370
+ }
371
+ }
372
+ }
373
+ }
374
+ else {
375
+ const key = Object.keys(modelValue.value)[0]
376
+ const arr = modelValue.value?.[key] || []
377
+ for (const entry of arr) {
378
+ // Top-level page at "/<slug>"
379
+ if (typeof entry.item === 'string') {
380
+ if (entry.name && entry.name !== excludeName)
381
+ slugs.add(entry.name)
382
+ }
383
+ // Top-level folder at "/<folder>/*"
384
+ else if (entry && typeof entry.item === 'object') {
385
+ const key = Object.keys(entry.item)[0]
386
+ if (key && key !== excludeName)
387
+ slugs.add(key)
388
+ }
389
+ }
390
+ }
391
+ }
392
+ return slugs
393
+ }
394
+
395
+ const slugGenerator = (name, excludeName = '') => {
396
+ // Build a set of existing slugs that map to URLs off of "/" from *both* root menus.
397
+ const existing = collectRootLevelSlugs(excludeName)
398
+ console.log('Existing slugs:', existing)
399
+
400
+ const base = name ? name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, '') : ''
401
+ let unique = base
402
+ let suffix = 1
403
+ while (existing.has(unique)) {
404
+ unique = `${base}-${suffix}`
405
+ suffix += 1
406
+ }
407
+ return unique
408
+ }
409
+
410
+ const selectTemplate = (templateId) => {
411
+ state.selectedTemplateId = templateId
412
+ state.showTemplatePicker = false
413
+ }
414
+
415
+ const isTemplateSelected = templateId => state.selectedTemplateId === templateId
416
+
417
+ const getTemplateDoc = (templateId) => {
418
+ if (templateId === BLANK_TEMPLATE_ID)
419
+ return null
420
+ return templatePagesCollection.value?.[templateId] || null
421
+ }
422
+
423
+ const extractBlockIds = (blocks = []) => {
424
+ if (!Array.isArray(blocks))
425
+ return []
426
+ return blocks
427
+ .map(block => block?.blockId || block?.id)
428
+ .filter(Boolean)
429
+ }
430
+
431
+ const deriveBlockIds = (pageDoc = {}) => {
432
+ const ids = [
433
+ ...extractBlockIds(pageDoc.content),
434
+ ...extractBlockIds(pageDoc.postContent),
435
+ ]
436
+ return Array.from(new Set(ids))
437
+ }
438
+
439
+ const getSyncedBlockFromSite = (blockId) => {
440
+ if (!blockId)
441
+ return null
442
+ const pages = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
443
+ for (const page of Object.values(pages)) {
444
+ const contentBlocks = Array.isArray(page?.content) ? page.content : []
445
+ const postBlocks = Array.isArray(page?.postContent) ? page.postContent : []
446
+ for (const candidate of [...contentBlocks, ...postBlocks]) {
447
+ if (candidate?.blockId === blockId && candidate?.synced)
448
+ return edgeGlobal.dupObject(candidate)
449
+ }
450
+ }
451
+ return null
452
+ }
453
+
454
+ const hydrateSyncedBlocksFromSite = (blocks = []) => {
455
+ if (!Array.isArray(blocks) || !blocks.length)
456
+ return blocks
457
+
458
+ return blocks.map((block) => {
459
+ if (!block?.synced || !block.blockId)
460
+ return block
461
+ const existing = getSyncedBlockFromSite(block.blockId)
462
+ if (!existing)
463
+ return block
464
+ const hydrated = edgeGlobal.dupObject(existing)
465
+ hydrated.id = block.id || hydrated.id || edgeGlobal.generateShortId()
466
+ hydrated.blockId = block.blockId
467
+ hydrated.name = block.name || hydrated.name
468
+ return hydrated
469
+ })
470
+ }
471
+
472
+ const buildPagePayloadFromTemplate = (templateDoc, slug) => {
473
+ const timestamp = Date.now()
474
+ const basePayload = {
475
+ name: slug,
476
+ content: [],
477
+ postContent: [],
478
+ blockIds: [],
479
+ metaTitle: '',
480
+ metaDescription: '',
481
+ structuredData: '',
482
+ doc_created_at: timestamp,
483
+ last_updated: timestamp,
484
+ }
485
+ if (!templateDoc)
486
+ return basePayload
487
+ const copy = JSON.parse(JSON.stringify(templateDoc || {}))
488
+ delete copy.docId
489
+ copy.name = slug
490
+ copy.doc_created_at = timestamp
491
+ copy.last_updated = timestamp
492
+ copy.content = Array.isArray(copy.content) ? hydrateSyncedBlocksFromSite(copy.content) : []
493
+ copy.postContent = Array.isArray(copy.postContent) ? hydrateSyncedBlocksFromSite(copy.postContent) : []
494
+ copy.blockIds = deriveBlockIds(copy)
495
+ return { ...basePayload, ...copy }
496
+ }
497
+
498
+ const renameFolderOrPageAction = async () => {
499
+ const newSlug = slugGenerator(state.renameItem.name, state.renameItem.previousName || '')
500
+
501
+ if (state.renameItem.name === state.renameItem.previousName) {
502
+ state.renameFolderOrPageDialog = false
503
+ state.renameItem = {}
504
+ return
505
+ }
506
+
507
+ // If the item is an empty string, we are renaming a top-level folder (handled here)
508
+ if (state.renameItem.item === '') {
509
+ const original = edgeGlobal.dupObject(modelValue.value)
510
+ const originalItem = edgeGlobal.dupObject(modelValue.value[state.renameItem.previousName])
511
+ // Renaming a folder: if the new name is empty, abort and reset dialog state
512
+ if (!state.renameItem.name) {
513
+ state.renameFolderOrPageDialog = false
514
+ state.renameItem = {}
515
+ return
516
+ }
517
+ // Move the array from the old key to the new key, then delete the old key
518
+ modelValue.value[newSlug] = originalItem
519
+ console.log('updated modelValue:', modelValue.value)
520
+ delete modelValue.value[state.renameItem.previousName]
521
+ state.renameFolderOrPageDialog = false
522
+ state.renameItem = {}
523
+ return
524
+ }
525
+
526
+ // Renaming a page: the page is uniquely identified by its docId in `state.renameItem.item`.
527
+ // Traverse all menus and submenus; update the `name` where the `item` matches that docId (strings only).
528
+ const targetDocId = state.renameItem.item
529
+ // const newName = state.renameItem.name || ''
530
+
531
+ let renamed = false
532
+ for (const [menuName, items] of Object.entries(modelValue.value)) {
533
+ for (const item of items) {
534
+ if (typeof item.item === 'string' && item.item === targetDocId) {
535
+ const results = await edgeFirebase.changeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`, targetDocId, { name: newSlug })
536
+ if (results.success) {
537
+ item.name = newSlug
538
+ renamed = true
539
+ }
540
+ break
541
+ }
542
+ }
543
+ if (renamed)
544
+ break
545
+ }
546
+ console.log(modelValue.value)
547
+ // Close dialog and reset state regardless
548
+ state.renameFolderOrPageDialog = false
549
+ state.renameItem = {}
550
+ }
551
+
552
+ const addPageAction = async () => {
553
+ state.newPageName = state.newPageName?.trim() || ''
554
+ if (!state.newPageName)
555
+ return
556
+ const slug = slugGenerator(state.newPageName)
557
+ if (!state.menuName) {
558
+ modelValue.value[state.newPageName] = []
559
+ state.newPageName = ''
560
+ state.addPageDialog = false
561
+ return
562
+ }
563
+
564
+ if (!Array.isArray(modelValue.value[state.menuName]))
565
+ modelValue.value[state.menuName] = []
566
+
567
+ if (state.addMenu) {
568
+ modelValue.value[state.menuName].push({ item: { [slug]: [] } })
569
+ }
570
+ else {
571
+ const templateDoc = getTemplateDoc(state.selectedTemplateId)
572
+ const payload = buildPagePayloadFromTemplate(templateDoc, slug)
573
+ const result = await edgeFirebase.storeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`, payload)
574
+ const docId = result?.meta?.docId
575
+ if (docId) {
576
+ const targetMenu = modelValue.value[state.menuName]
577
+ const alreadyExists = Array.isArray(targetMenu) && targetMenu.some(entry => entry?.item === docId)
578
+ if (!alreadyExists)
579
+ targetMenu.push({ name: slug, item: docId })
580
+ }
581
+ }
582
+
583
+ state.addPageDialog = false
584
+ }
585
+ const deletePageAction = async () => {
586
+ if (state.deletePage.item === '') {
587
+ // deleting a folder
588
+ delete modelValue.value[state.deletePage.name]
589
+ state.deletePageDialog = false
590
+ state.deletePage = {}
591
+ return
592
+ }
593
+ if (props.page === state.deletePage.item) {
594
+ router.replace(pageRouteBase.value)
595
+ }
596
+ for (const [menuName, items] of Object.entries(modelValue.value)) {
597
+ for (const item of items) {
598
+ if (typeof item.item === 'string' && item.item === state.deletePage.item) {
599
+ item.name = 'Deleting...'
600
+ }
601
+ if (typeof item.item === 'object') {
602
+ for (const [subMenuName, subItems] of Object.entries(item.item)) {
603
+ for (const subItem of subItems) {
604
+ if (typeof subItem.item === 'string' && subItem.item === state.deletePage.item) {
605
+ subItem.name = 'Deleting...'
606
+ }
607
+ }
608
+ }
609
+ }
610
+ }
611
+ }
612
+ state.deletePageDialog = false
613
+ state.deletePage = {}
614
+ }
615
+
616
+ const pages = toTypedSchema(z.object({
617
+ name: z.string({
618
+ required_error: 'Name is required',
619
+ }).min(1, { message: 'Name is required' }),
620
+ }))
621
+
622
+ const disabledFolderDelete = (menuName, menu) => {
623
+ if (menuName === 'Site Root') {
624
+ return true
625
+ }
626
+ if (menu.length > 0) {
627
+ return true
628
+ }
629
+ return false
630
+ }
631
+
632
+ const canRename = (menuName) => {
633
+ if (props.prevMenu) {
634
+ return true
635
+ }
636
+ if (menuName === 'Site Root') {
637
+ return false
638
+ }
639
+ return true
640
+ }
641
+
642
+ const publishPage = async (pageId) => {
643
+ const pageData = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`] || {}
644
+ if (pageData[pageId]) {
645
+ await edgeFirebase.storeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`, pageData[pageId])
646
+ }
647
+ }
648
+ const unPublishPage = async (pageId) => {
649
+ await edgeFirebase.removeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`, pageId)
650
+ }
651
+
652
+ const discardPageChanges = async (pageId) => {
653
+ const publishedPage = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/published`]?.[pageId]
654
+ if (publishedPage) {
655
+ await edgeFirebase.storeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`, publishedPage, pageId)
656
+ }
657
+ }
658
+
659
+ const showPageSettings = (page) => {
660
+ console.log('showPageSettings', page)
661
+ state.pageData = page
662
+ state.pageSettings = true
663
+ }
664
+
665
+ const formErrors = (error) => {
666
+ console.log('Form errors:', error)
667
+ console.log(Object.values(error))
668
+ if (Object.values(error).length > 0) {
669
+ console.log('Form errors found')
670
+ state.hasError = true
671
+ console.log(state.hasError)
672
+ }
673
+ state.hasError = false
674
+ }
675
+
676
+ const onSubmit = () => {
677
+ if (!state.hasError) {
678
+ emit('pageSettingsUpdate', state.pageData)
679
+ state.pageSettings = false
680
+ }
681
+ }
682
+ const titleFromSlug = (slug) => {
683
+ if (!slug)
684
+ return ''
685
+ return slug.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
686
+ }
687
+
688
+ const theme = computed(() => {
689
+ const theme = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`]?.[props.site]?.theme || ''
690
+ console.log(`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}`)
691
+ let themeContents = null
692
+ if (theme) {
693
+ themeContents = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[theme]?.theme || null
694
+ }
695
+ if (themeContents) {
696
+ return JSON.parse(themeContents)
697
+ }
698
+ return null
699
+ })
700
+ </script>
701
+
702
+ <template>
703
+ <SidebarMenuItem v-for="({ menu, name: menuName }) in orderedMenus" :key="menuName">
704
+ <SidebarMenuButton class="!px-0 hover:!bg-transparent">
705
+ <FolderOpen
706
+ class="mr-2"
707
+ />
708
+ <span v-if="!props.isTemplateSite">{{ menuName === 'Site Root' ? 'Site Menu' : menuName }}</span>
709
+ <SidebarGroupAction class="absolute right-2 top-0 hover:!bg-transparent">
710
+ <DropdownMenu>
711
+ <DropdownMenuTrigger as-child>
712
+ <SidebarMenuAction>
713
+ <PlusIcon />
714
+ </SidebarMenuAction>
715
+ </DropdownMenuTrigger>
716
+ <DropdownMenuContent side="right" align="start">
717
+ <DropdownMenuLabel v-if="props.prevMenu" class="flex items-center gap-2">
718
+ <Folder class="w-5 h-5" /> {{ ROOT_MENUS.includes(props.prevMenu) ? '' : props.prevMenu }}/{{ menuName }}/
719
+ </DropdownMenuLabel>
720
+ <DropdownMenuLabel v-else class="flex items-center gap-2">
721
+ <Folder class="w-5 h-5" /> {{ ROOT_MENUS.includes(menuName) ? '' : menuName }}/
722
+ </DropdownMenuLabel>
723
+ <DropdownMenuSeparator />
724
+ <DropdownMenuItem @click="addPageShow(menuName, false)">
725
+ <FilePlus2 />
726
+ <span>New Page</span>
727
+ </DropdownMenuItem>
728
+ <DropdownMenuItem v-if="!props.prevMenu && !props.isTemplateSite" @click="addPageShow(menuName, true)">
729
+ <FolderPlus />
730
+ <span>New Folder</span>
731
+ </DropdownMenuItem>
732
+ <DropdownMenuItem v-if="canRename(menuName)" @click="renameFolderOrPageShow({ name: menuName, item: '' })">
733
+ <FolderPen />
734
+ <span>Rename Folder</span>
735
+ </DropdownMenuItem>
736
+ <DropdownMenuItem class="flex-col gap-0 items-start text-destructive" :disabled="disabledFolderDelete(menuName, menu) || ROOT_MENUS.includes(menuName)" @click="deletePageShow({ name: menuName, item: '' })">
737
+ <span class="my-0 py-0 flex"> <FolderMinus class="mr-2 h-4 w-4" />Delete Folder</span>
738
+ <span v-if="ROOT_MENUS.includes(menuName)" class="my-0 text-gray-400 py-0 text-xs">(Cannot delete {{ menuName }})</span>
739
+ <span v-else-if="disabledFolderDelete(menuName, menu)" class="my-0 text-gray-400 py-0 text-xs">(Folder must be empty to delete)</span>
740
+ </DropdownMenuItem>
741
+ </DropdownMenuContent>
742
+ </DropdownMenu>
743
+ </SidebarGroupAction>
744
+ </SidebarMenuButton>
745
+
746
+ <SidebarMenuSub class="mx-0 px-2">
747
+ <draggable
748
+ :list="modelValue[menuName]"
749
+ handle=".handle"
750
+ item-key="subindex"
751
+ class="list-group"
752
+ :group="{ name: 'menus', pull: true, put: true }"
753
+ >
754
+ <template #item="{ element, index }">
755
+ <div class="handle list-group-item">
756
+ <edge-cms-menu
757
+ v-if="typeof element.item === 'object'"
758
+ v-model="modelValue[menuName][index].item"
759
+ :prev-menu="menuName"
760
+ :prev-model-value="modelValue"
761
+ :site="props.site"
762
+ :page="props.page"
763
+ :prev-index="index"
764
+ :is-template-site="props.isTemplateSite"
765
+ />
766
+ <SidebarMenuSubItem v-else class="relative">
767
+ <SidebarMenuSubButton :class="{ 'text-gray-400': element.item === '' }" as-child :is-active="element.item === props.page">
768
+ <NuxtLink :disabled="element.item === ''" :class="{ '!text-red-500': element.name === 'Deleting...' }" class="text-xs" :to="`${pageRouteBase}/${element.item}`">
769
+ <Loader2 v-if="element.item === '' || element.name === 'Deleting...'" :class="{ '!text-red-500': element.name === 'Deleting...' }" class="w-4 h-4 animate-spin" />
770
+ <FileWarning v-else-if="isPublishedPageDiff(element.item) && !props.isTemplateSite" class="!text-yellow-600" />
771
+ <FileCheck v-else class="text-xs !text-green-700 font-normal" />
772
+ <span>{{ element.name }}</span>
773
+ </NuxtLink>
774
+ </SidebarMenuSubButton>
775
+ <div class="absolute right-0 -top-0.5">
776
+ <DropdownMenu>
777
+ <DropdownMenuTrigger as-child>
778
+ <SidebarMenuAction>
779
+ <MoreHorizontal />
780
+ </SidebarMenuAction>
781
+ </DropdownMenuTrigger>
782
+ <DropdownMenuContent side="right" align="start">
783
+ <DropdownMenuLabel v-if="props.prevMenu" class="flex items-center gap-2">
784
+ <File class="w-5 h-5" /> {{ ROOT_MENUS.includes(props.prevMenu) ? '' : props.prevMenu }}/{{ menuName }}/{{ element.name }}
785
+ </DropdownMenuLabel>
786
+ <DropdownMenuLabel v-else class="flex items-center gap-2">
787
+ <File class="w-5 h-5" /> {{ ROOT_MENUS.includes(menuName) ? '' : menuName }}/{{ element.name }}
788
+ </DropdownMenuLabel>
789
+ <DropdownMenuSeparator />
790
+ <DropdownMenuItem :disabled="edgeGlobal.edgeState.cmsPageWithUnsavedChanges === element.item" @click="showPageSettings(element)">
791
+ <FileCog />
792
+ <div class="flex flex-col">
793
+ <span>Settings</span>
794
+ <span v-if="edgeGlobal.edgeState.cmsPageWithUnsavedChanges === element.item" class="text-xs text-red-500">(Unsaved Changes)</span>
795
+ </div>
796
+ </DropdownMenuItem>
797
+ <DropdownMenuItem v-if="!props.isTemplateSite && isPublishedPageDiff(element.item)" @click="publishPage(element.item)">
798
+ <FileUp />
799
+ Publish
800
+ </DropdownMenuItem>
801
+ <DropdownMenuItem @click="renameFolderOrPageShow(element)">
802
+ <FilePen />
803
+ <span>Rename</span>
804
+ </DropdownMenuItem>
805
+ <DropdownMenuSeparator />
806
+ <DropdownMenuItem v-if="!props.isTemplateSite && isPublishedPageDiff(element.item) && isPublished(element.item)" @click="discardPageChanges(element.item)">
807
+ <FileX />
808
+ Discard Changes
809
+ </DropdownMenuItem>
810
+ <DropdownMenuItem v-if="!props.isTemplateSite && isPublished(element.item)" @click="unPublishPage(element.item)">
811
+ <FileDown />
812
+ Unpublish
813
+ </DropdownMenuItem>
814
+ <DropdownMenuItem class="text-destructive" @click="deletePageShow(element)">
815
+ <FileMinus2 />
816
+ <span>Delete</span>
817
+ </DropdownMenuItem>
818
+ </DropdownMenuContent>
819
+ </DropdownMenu>
820
+ </div>
821
+ </SidebarMenuSubItem>
822
+ </div>
823
+ </template>
824
+ </draggable>
825
+ </SidebarMenuSub>
826
+ </SidebarMenuItem>
827
+ <edge-shad-dialog
828
+ v-model="state.deletePageDialog"
829
+ >
830
+ <DialogContent class="pt-10">
831
+ <DialogHeader>
832
+ <DialogTitle class="text-left">
833
+ <span v-if="state.deletePage.item === ''">Delete Folder "{{ state.deletePage.name }}"</span>
834
+ <span v-else>Delete Page "{{ state.deletePage.name }}"</span>
835
+ </DialogTitle>
836
+ <DialogDescription />
837
+ </DialogHeader>
838
+ <div class="text-left px-1">
839
+ Are you sure you want to delete "{{ state.deletePage.name }}"? This action cannot be undone.
840
+ </div>
841
+ <DialogFooter class="pt-2 flex justify-between">
842
+ <edge-shad-button
843
+ class="text-white bg-slate-800 hover:bg-slate-400" @click="state.deletePageDialog = false"
844
+ >
845
+ Cancel
846
+ </edge-shad-button>
847
+ <edge-shad-button
848
+ variant="destructive" class="text-white w-full" @click="deletePageAction()"
849
+ >
850
+ Delete Page
851
+ </edge-shad-button>
852
+ </DialogFooter>
853
+ </DialogContent>
854
+ </edge-shad-dialog>
855
+ <edge-shad-dialog v-model="state.addPageDialog">
856
+ <DialogContent v-if="state.addMenu" class="pt-10">
857
+ <edge-shad-form :schema="pages" @submit="addPageAction">
858
+ <DialogHeader>
859
+ <DialogTitle class="text-left">
860
+ <span v-if="!state.menuName">Add Menu</span>
861
+ <span v-else>Add folder to "{{ state.menuName }}"</span>
862
+ </DialogTitle>
863
+ <DialogDescription />
864
+ </DialogHeader>
865
+ <edge-shad-input v-model="state.newPageName" name="name" placeholder="Folder Name" />
866
+ <DialogFooter class="pt-2 flex justify-between">
867
+ <edge-shad-button type="button" variant="destructive" @click="state.addPageDialog = false">
868
+ Cancel
869
+ </edge-shad-button>
870
+ <edge-shad-button type="submit" class="text-white bg-slate-800 hover:bg-slate-400 w-full">
871
+ Add Folder
872
+ </edge-shad-button>
873
+ </DialogFooter>
874
+ </edge-shad-form>
875
+ </DialogContent>
876
+ <DialogContent v-else class="pt-6 w-full max-w-6xl h-[90vh] flex flex-col">
877
+ <edge-shad-form :schema="pages" class="flex flex-col h-full" @submit="addPageAction">
878
+ <DialogHeader class="pb-2">
879
+ <DialogTitle class="text-left">
880
+ Add page to "{{ state.menuName }}"
881
+ </DialogTitle>
882
+ <DialogDescription>
883
+ Choose a template or start with a blank page. You can always customize it later.
884
+ </DialogDescription>
885
+ </DialogHeader>
886
+ <div>
887
+ <div class="w-full space-y-4">
888
+ <edge-shad-input v-model="state.newPageName" name="name" label="Page Name" placeholder="Enter page name" />
889
+ <edge-shad-select
890
+ v-model="state.templateFilter"
891
+ label="Template Tags"
892
+ :items="templateFilterOptions"
893
+ item-title="label"
894
+ item-value="value"
895
+ placeholder="Select tag"
896
+ />
897
+ <p class="text-xs text-muted-foreground">
898
+ Filter templates by tag or choose Quick Picks for the most commonly used layouts.
899
+ </p>
900
+ </div>
901
+ <edge-button-divider class="my-4">
902
+ <span class="text-xs text-muted-foreground !nowrap text-center">Select Template</span>
903
+ </edge-button-divider>
904
+ <div class="overflow-y-auto !h-[calc(100vh-510px)] pr-1">
905
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 auto-rows-fr pb-2">
906
+ <button
907
+ v-for="template in templateGridItems"
908
+ :key="template.docId"
909
+ type="button"
910
+ class="rounded-lg border bg-card text-left p-3 flex flex-col gap-3 transition focus:outline-none focus-visible:ring-2"
911
+ :class="isTemplateSelected(template.docId) ? 'border-primary ring-2 ring-primary/50 shadow-lg' : 'border-border hover:border-primary/40'"
912
+ :aria-pressed="isTemplateSelected(template.docId)"
913
+ @click="selectTemplate(template.docId)"
914
+ >
915
+ <div class="flex items-center justify-between gap-2">
916
+ <span class="font-semibold truncate">{{ template.name }}</span>
917
+ <File class="w-4 h-4 text-muted-foreground" />
918
+ </div>
919
+ <div class="template-scale-wrapper border border-dashed border-border/60 rounded-md bg-background/80">
920
+ <div class="template-scale-inner">
921
+ <div class="template-scale-content space-y-4">
922
+ <template v-if="template.docId === BLANK_TEMPLATE_ID">
923
+ <div class="flex h-32 items-center justify-center text-[100px] mt-[100px] text-muted-foreground">
924
+ Blank page
925
+ </div>
926
+ </template>
927
+ <template v-else-if="templateHasBlocks(template)">
928
+ <div
929
+ v-for="(block, idx) in templatePreviewBlocks(template)"
930
+ :key="`${template.docId}-block-${idx}`"
931
+ >
932
+ <edge-cms-block-api
933
+ v-if="resolveBlockForPreview(block)"
934
+ :content="resolveBlockForPreview(block).content"
935
+ :values="resolveBlockForPreview(block).values"
936
+ :meta="resolveBlockForPreview(block).meta"
937
+ :theme="theme"
938
+ :isolated="true"
939
+ />
940
+ </div>
941
+ </template>
942
+ <template v-else>
943
+ <div class="flex h-32 items-center justify-center text-[100px] mt-[100px] text-muted-foreground">
944
+ No blocks yet
945
+ </div>
946
+ </template>
947
+ </div>
948
+ </div>
949
+ </div>
950
+ </button>
951
+ </div>
952
+ </div>
953
+ </div>
954
+ <DialogFooter class="pt-4">
955
+ <edge-shad-button type="button" variant="destructive" @click="state.addPageDialog = false">
956
+ Cancel
957
+ </edge-shad-button>
958
+ <edge-shad-button type="submit" class="bg-slate-800 hover:bg-slate-400 text-white" :disabled="!hasValidNewPageName">
959
+ Create Page
960
+ </edge-shad-button>
961
+ </DialogFooter>
962
+ </edge-shad-form>
963
+ </DialogContent>
964
+ </edge-shad-dialog>
965
+ <edge-shad-dialog
966
+ v-model="state.renameFolderOrPageDialog"
967
+ >
968
+ <DialogContent class="pt-10">
969
+ <edge-shad-form :schema="pages" @submit="renameFolderOrPageAction">
970
+ <DialogHeader>
971
+ <DialogTitle class="text-left">
972
+ <span v-if="state.renameItem.item === ''">Rename Folder "{{ state.renameItem.name }}"</span>
973
+ <span v-else>Rename Page "{{ state.renameItem.name }}"</span>
974
+ </DialogTitle>
975
+ <DialogDescription />
976
+ </DialogHeader>
977
+ <edge-shad-input v-model="state.renameItem.name" name="name" placeholder="New Name" />
978
+ <DialogFooter class="pt-2 flex justify-between">
979
+ <edge-shad-button variant="destructive" @click="state.renameFolderOrPageDialog = false">
980
+ Cancel
981
+ </edge-shad-button>
982
+ <edge-shad-button type="submit" class="text-white bg-slate-800 hover:bg-slate-400 w-full">
983
+ Rename
984
+ </edge-shad-button>
985
+ </DialogFooter>
986
+ </edge-shad-form>
987
+ </DialogContent>
988
+ </edge-shad-dialog>
989
+ <Sheet v-model:open="state.pageSettings">
990
+ <SheetContent side="left" class="w-full md:w-1/2 max-w-none sm:max-w-none max-w-2xl">
991
+ <SheetHeader>
992
+ <SheetTitle>{{ state.pageData.name || 'Site' }}</SheetTitle>
993
+ <SheetDescription />
994
+ </SheetHeader>
995
+ <edge-editor
996
+ :collection="`sites/${props.site}/pages`"
997
+ :doc-id="state.pageData.item"
998
+ :schema="schemas.pages"
999
+ :new-doc-schema="state.newDocs.pages"
1000
+ class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none px-0shadow-none"
1001
+ :show-footer="false"
1002
+ :show-header="false"
1003
+ :save-function-override="onSubmit"
1004
+ card-content-class="px-0"
1005
+ @error="formErrors"
1006
+ >
1007
+ <template #main="slotProps">
1008
+ <div class="p-6 space-y-4 h-[calc(100vh-142px)] overflow-y-auto">
1009
+ <edge-shad-checkbox
1010
+ v-model="slotProps.workingDoc.post"
1011
+ label="Is a Post Template"
1012
+ name="post"
1013
+ >
1014
+ Creates both an Index Page and a Detail Page for this section.
1015
+ The Index Page lists all items (e.g., /{{ slotProps.workingDoc.name }}), while the Detail Page displays a single item (e.g., /{{ slotProps.workingDoc.name }}/:slug).
1016
+ </edge-shad-checkbox>
1017
+ <edge-shad-select-tags
1018
+ v-if="props.isTemplateSite"
1019
+ v-model="slotProps.workingDoc.tags"
1020
+ name="tags"
1021
+ label="Tags"
1022
+ placeholder="Add tags"
1023
+ :items="templateTagItems"
1024
+ :allow-additions="true"
1025
+ />
1026
+ <edge-shad-select-tags
1027
+ v-if="props.themeOptions.length"
1028
+ :model-value="Array.isArray(slotProps.workingDoc.allowedThemes) ? slotProps.workingDoc.allowedThemes : []"
1029
+ name="allowedThemes"
1030
+ label="Allowed Themes"
1031
+ placeholder="Select allowed themes"
1032
+ :items="props.themeOptions"
1033
+ item-title="label"
1034
+ item-value="value"
1035
+ @update:model-value="(value) => {
1036
+ slotProps.workingDoc.allowedThemes = Array.isArray(value) ? value : []
1037
+ }"
1038
+ />
1039
+ <Card>
1040
+ <CardHeader>
1041
+ <CardTitle>SEO</CardTitle>
1042
+ <CardDescription>Meta tags for the page.</CardDescription>
1043
+ </CardHeader>
1044
+ <CardContent class="pt-0">
1045
+ <edge-shad-input
1046
+ v-model="slotProps.workingDoc.metaTitle"
1047
+ label="Meta Title"
1048
+ name="metaTitle"
1049
+ />
1050
+ <edge-shad-textarea
1051
+ v-model="slotProps.workingDoc.metaDescription"
1052
+ label="Meta Description"
1053
+ name="metaDescription"
1054
+ />
1055
+ <edge-cms-code-editor
1056
+ v-model="slotProps.workingDoc.structuredData"
1057
+ title="Structured Data (JSON-LD)"
1058
+ language="json"
1059
+ name="structuredData"
1060
+ height="300px"
1061
+ class="mb-4 w-full"
1062
+ />
1063
+ </CardContent>
1064
+ </Card>
1065
+ </div>
1066
+ <SheetFooter class="pt-2 flex justify-between">
1067
+ <edge-shad-button variant="destructive" class="text-white" @click="state.pageSettings = false">
1068
+ Cancel
1069
+ </edge-shad-button>
1070
+ <edge-shad-button :disabled="slotProps.submitting" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
1071
+ <Loader2 v-if="slotProps.submitting" class=" h-4 w-4 animate-spin" />
1072
+ Update
1073
+ </edge-shad-button>
1074
+ </SheetFooter>
1075
+ </template>
1076
+ </edge-editor>
1077
+ </SheetContent>
1078
+ </Sheet>
1079
+ </template>
1080
+
1081
+ <style lang="scss">
1082
+ .template-scale-wrapper {
1083
+ width: 100%;
1084
+ overflow: hidden;
1085
+ position: relative;
1086
+ border-radius: 0.5rem;
1087
+ height: 400px;
1088
+ }
1089
+
1090
+ .template-scale-inner {
1091
+ transform-origin: top left;
1092
+ display: inline-block;
1093
+ width: 100%;
1094
+ height: 400px;
1095
+ overflow: hidden;
1096
+ }
1097
+
1098
+ .template-scale-content {
1099
+ transform: scale(0.2);
1100
+ transform-origin: top left;
1101
+ width: 500%;
1102
+ }
1103
+ </style>