@edgedev/create-edge-app 1.2.33 → 1.2.35

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 (45) hide show
  1. package/README.md +1 -0
  2. package/agents.md +95 -2
  3. package/deploy.sh +136 -0
  4. package/edge/components/cms/block.vue +977 -305
  5. package/edge/components/cms/blockApi.vue +3 -3
  6. package/edge/components/cms/blockEditor.vue +688 -86
  7. package/edge/components/cms/blockPicker.vue +31 -5
  8. package/edge/components/cms/blockRender.vue +3 -3
  9. package/edge/components/cms/blocksManager.vue +790 -82
  10. package/edge/components/cms/codeEditor.vue +15 -6
  11. package/edge/components/cms/fontUpload.vue +318 -2
  12. package/edge/components/cms/htmlContent.vue +825 -93
  13. package/edge/components/cms/init_blocks/contact_us.html +55 -47
  14. package/edge/components/cms/init_blocks/newsletter.html +56 -96
  15. package/edge/components/cms/menu.vue +96 -34
  16. package/edge/components/cms/page.vue +902 -58
  17. package/edge/components/cms/posts.vue +13 -4
  18. package/edge/components/cms/site.vue +638 -87
  19. package/edge/components/cms/siteSettingsForm.vue +19 -9
  20. package/edge/components/cms/sitesManager.vue +5 -4
  21. package/edge/components/cms/themeDefaultMenu.vue +20 -2
  22. package/edge/components/cms/themeEditor.vue +196 -162
  23. package/edge/components/editor.vue +5 -1
  24. package/edge/composables/global.ts +37 -5
  25. package/edge/composables/siteSettingsTemplate.js +2 -0
  26. package/edge/composables/useCmsNewDocs.js +100 -0
  27. package/edge/composables/useEdgeCmsDialogPositionFix.js +19 -0
  28. package/edge/routes/cms/dashboard/blocks/[block].vue +5 -0
  29. package/edge/routes/cms/dashboard/blocks/index.vue +12 -1
  30. package/edge/routes/cms/dashboard/media/index.vue +5 -0
  31. package/edge/routes/cms/dashboard/sites/[site]/[[page]].vue +4 -0
  32. package/edge/routes/cms/dashboard/sites/[site].vue +4 -0
  33. package/edge/routes/cms/dashboard/sites/index.vue +4 -0
  34. package/edge/routes/cms/dashboard/templates/[page].vue +4 -0
  35. package/edge/routes/cms/dashboard/templates/index.vue +4 -0
  36. package/edge/routes/cms/dashboard/themes/[theme].vue +5 -0
  37. package/edge/routes/cms/dashboard/themes/index.vue +330 -1
  38. package/edge-pull.sh +16 -2
  39. package/edge-push.sh +9 -1
  40. package/edge-remote.sh +20 -0
  41. package/edge-status.sh +9 -5
  42. package/edge-update-all.sh +127 -0
  43. package/firebase.json +4 -0
  44. package/nuxt.config.ts +1 -1
  45. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { AlertTriangle, ArrowDown, ArrowUp, Maximize2, Monitor, Smartphone, Sparkles, Tablet, UploadCloud } from 'lucide-vue-next'
2
+ import { AlertTriangle, ArrowDown, ArrowUp, Download, Maximize2, Monitor, Smartphone, Sparkles, Tablet, UploadCloud } from 'lucide-vue-next'
3
3
  import { toTypedSchema } from '@vee-validate/zod'
4
4
  import * as z from 'zod'
5
5
  const props = defineProps({
@@ -20,7 +20,18 @@ const props = defineProps({
20
20
  const emit = defineEmits(['head'])
21
21
 
22
22
  const edgeFirebase = inject('edgeFirebase')
23
+ const router = useRouter()
23
24
  const { buildPageStructuredData } = useStructuredDataTemplates()
25
+ const cmsMultiOrg = useState('cmsMultiOrg', () => true)
26
+ const isAdmin = computed(() => edgeGlobal.isAdminGlobal(edgeFirebase).value)
27
+ const isDevModeEnabled = computed(() => process.dev || Boolean(edgeGlobal.edgeState.devOverride))
28
+ const canOpenPreviewBlockContentEditor = computed(() => {
29
+ if (!isAdmin.value)
30
+ return false
31
+ if (cmsMultiOrg.value)
32
+ return true
33
+ return isDevModeEnabled.value
34
+ })
24
35
 
25
36
  const state = reactive({
26
37
  newDocs: {
@@ -41,7 +52,15 @@ const state = reactive({
41
52
  workingDoc: {},
42
53
  seoAiLoading: false,
43
54
  seoAiError: '',
55
+ importingJson: false,
56
+ importDocIdDialogOpen: false,
57
+ importDocIdValue: '',
58
+ importConflictDialogOpen: false,
59
+ importConflictDocId: '',
60
+ importErrorDialogOpen: false,
61
+ importErrorMessage: '',
44
62
  previewViewport: 'full',
63
+ previewPageView: 'list',
45
64
  newRowLayout: '6',
46
65
  newPostRowLayout: '6',
47
66
  rowSettings: {
@@ -68,6 +87,10 @@ const state = reactive({
68
87
  },
69
88
  })
70
89
 
90
+ const pageImportInputRef = ref(null)
91
+ const pageImportDocIdResolver = ref(null)
92
+ const pageImportConflictResolver = ref(null)
93
+
71
94
  const schemas = {
72
95
  pages: toTypedSchema(z.object({
73
96
  name: z.string({
@@ -97,10 +120,28 @@ const previewViewportStyle = computed(() => {
97
120
  }
98
121
  })
99
122
 
123
+ const previewViewportContainStyle = computed(() => {
124
+ return {
125
+ ...(previewViewportStyle.value || {}),
126
+ }
127
+ })
128
+
100
129
  const setPreviewViewport = (viewportId) => {
101
130
  state.previewViewport = viewportId
102
131
  }
103
132
 
133
+ const hasPostView = (workingDoc) => {
134
+ if (!workingDoc || typeof workingDoc !== 'object')
135
+ return false
136
+ return Boolean(workingDoc.post)
137
+ || (Array.isArray(workingDoc.postContent) && workingDoc.postContent.length > 0)
138
+ || (Array.isArray(workingDoc.postStructure) && workingDoc.postStructure.length > 0)
139
+ }
140
+
141
+ const setPreviewPageView = (view) => {
142
+ state.previewPageView = view === 'post' ? 'post' : 'list'
143
+ }
144
+
104
145
  const previewViewportMode = computed(() => {
105
146
  if (state.previewViewport === 'full')
106
147
  return 'auto'
@@ -108,6 +149,13 @@ const previewViewportMode = computed(() => {
108
149
  })
109
150
 
110
151
  const isMobilePreview = computed(() => previewViewportMode.value === 'mobile')
152
+ const pagePreviewRenderKey = computed(() => {
153
+ const siteKey = String(props.site || '')
154
+ const pageKey = String(props.page || '')
155
+ const themeKey = String(effectiveThemeId.value || selectedThemeId.value || 'no-theme')
156
+ const modeKey = state.editMode ? 'edit' : 'preview'
157
+ return `${siteKey}:${pageKey}:${themeKey}:${modeKey}`
158
+ })
111
159
 
112
160
  const GRID_CLASSES = {
113
161
  1: 'grid grid-cols-1 gap-4',
@@ -135,6 +183,14 @@ const ROW_GAP_OPTIONS = [
135
183
  { name: '8', title: 'X-Large' },
136
184
  ]
137
185
 
186
+ const ROW_GAP_CLASS_MAP = {
187
+ 0: 'gap-0 sm:gap-0',
188
+ 2: 'gap-0 sm:gap-2',
189
+ 4: 'gap-0 sm:gap-4',
190
+ 6: 'gap-0 sm:gap-6',
191
+ 8: 'gap-0 sm:gap-8',
192
+ }
193
+
138
194
  const ROW_MOBILE_STACK_OPTIONS = [
139
195
  { name: 'normal', title: 'Left first' },
140
196
  { name: 'reverse', title: 'Right first' },
@@ -440,17 +496,51 @@ const blockPick = (block, index, slotProps, post = false) => {
440
496
  }
441
497
 
442
498
  const applyCollectionUniqueKeys = (workingDoc) => {
499
+ const hasTemplateToken = (value) => {
500
+ if (typeof value === 'string')
501
+ return value.includes('{orgId}') || value.includes('{siteId}')
502
+ if (Array.isArray(value))
503
+ return value.some(entry => hasTemplateToken(entry))
504
+ if (value && typeof value === 'object')
505
+ return Object.values(value).some(entry => hasTemplateToken(entry))
506
+ return false
507
+ }
508
+
509
+ const resolveTokens = (value) => {
510
+ if (typeof value === 'string') {
511
+ let resolved = value
512
+ const orgId = edgeGlobal.edgeState.currentOrganization || ''
513
+ const siteId = props.site || ''
514
+ if (resolved.includes('{orgId}') && orgId)
515
+ resolved = resolved.replaceAll('{orgId}', orgId)
516
+ if (resolved.includes('{siteId}') && siteId)
517
+ resolved = resolved.replaceAll('{siteId}', siteId)
518
+ return resolved
519
+ }
520
+ if (Array.isArray(value))
521
+ return value.map(entry => resolveTokens(entry))
522
+ if (value && typeof value === 'object') {
523
+ const out = {}
524
+ Object.keys(value).forEach((key) => {
525
+ out[key] = resolveTokens(value[key])
526
+ })
527
+ return out
528
+ }
529
+ return value
530
+ }
531
+
532
+ const isEmptyQueryItem = (value) => {
533
+ if (value === undefined || value === null || value === '')
534
+ return true
535
+ if (Array.isArray(value))
536
+ return value.length === 0
537
+ return false
538
+ }
539
+
443
540
  const resolveUniqueKey = (template) => {
444
541
  if (!template || typeof template !== 'string')
445
542
  return ''
446
- let resolved = template
447
- const orgId = edgeGlobal.edgeState.currentOrganization || ''
448
- const siteId = props.site || ''
449
- if (resolved.includes('{orgId}') && orgId)
450
- resolved = resolved.replaceAll('{orgId}', orgId)
451
- if (resolved.includes('{siteId}') && siteId)
452
- resolved = resolved.replaceAll('{siteId}', siteId)
453
- return resolved
543
+ return resolveTokens(template)
454
544
  }
455
545
 
456
546
  const applyToBlocks = (blocks) => {
@@ -462,6 +552,27 @@ const applyCollectionUniqueKeys = (workingDoc) => {
462
552
  return
463
553
  Object.keys(meta).forEach((fieldKey) => {
464
554
  const cfg = meta[fieldKey]
555
+ if (!cfg || typeof cfg !== 'object')
556
+ return
557
+
558
+ // Materialize tokenized collection.query filters (e.g. {siteId}) into queryItems
559
+ // so frontend hydration receives concrete runtime filter selections.
560
+ if (Array.isArray(cfg?.collection?.query)) {
561
+ if (!cfg.queryItems || typeof cfg.queryItems !== 'object')
562
+ cfg.queryItems = {}
563
+ for (const queryFilter of cfg.collection.query) {
564
+ const queryField = queryFilter?.field
565
+ if (!queryField || typeof queryField !== 'string')
566
+ continue
567
+ const rawValue = queryFilter?.value
568
+ if (!hasTemplateToken(rawValue))
569
+ continue
570
+ if (!isEmptyQueryItem(cfg.queryItems[queryField]))
571
+ continue
572
+ cfg.queryItems[queryField] = resolveTokens(rawValue)
573
+ }
574
+ }
575
+
465
576
  if (!cfg?.collection?.uniqueKey)
466
577
  return
467
578
  const resolved = resolveUniqueKey(cfg.collection.uniqueKey)
@@ -493,11 +604,48 @@ onMounted(() => {
493
604
  }
494
605
  })
495
606
 
607
+ const previewSnapshotsBootstrapping = ref(false)
608
+
609
+ const ensurePreviewSnapshots = async () => {
610
+ const orgId = String(edgeGlobal.edgeState.currentOrganization || '').trim()
611
+ if (!orgId)
612
+ return
613
+
614
+ if (previewSnapshotsBootstrapping.value)
615
+ return
616
+ previewSnapshotsBootstrapping.value = true
617
+
618
+ const themesPath = `organizations/${orgId}/themes`
619
+ const sitesPath = `organizations/${orgId}/sites`
620
+
621
+ // Non-blocking bootstrap: never hold page render on snapshot latency.
622
+ try {
623
+ if (!edgeFirebase.data?.[themesPath]) {
624
+ await edgeFirebase.startSnapshot(themesPath)
625
+ }
626
+ if (!edgeFirebase.data?.[sitesPath]) {
627
+ await edgeFirebase.startSnapshot(sitesPath)
628
+ }
629
+ }
630
+ catch (error) {
631
+ console.error('Failed to start page preview snapshots', error)
632
+ }
633
+ finally {
634
+ previewSnapshotsBootstrapping.value = false
635
+ }
636
+ }
637
+
638
+ onBeforeMount(() => {
639
+ ensurePreviewSnapshots()
640
+ })
641
+
496
642
  const editorDocUpdates = (workingDoc) => {
497
643
  ensureStructureDefaults(workingDoc, false)
498
644
  if (workingDoc?.post || (Array.isArray(workingDoc?.postContent) && workingDoc.postContent.length > 0) || Array.isArray(workingDoc?.postStructure))
499
645
  ensureStructureDefaults(workingDoc, true)
500
646
  applyCollectionUniqueKeys(workingDoc)
647
+ if (!hasPostView(workingDoc) && state.previewPageView === 'post')
648
+ state.previewPageView = 'list'
501
649
  const blockIds = (workingDoc.content || []).map(block => block.blockId).filter(id => id)
502
650
  const postBlockIds = workingDoc.postContent ? workingDoc.postContent.map(block => block.blockId).filter(id => id) : []
503
651
  blockIds.push(...postBlockIds)
@@ -528,19 +676,204 @@ const selectedThemeId = computed(() => {
528
676
  return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`]?.[props.site]?.theme || ''
529
677
  })
530
678
 
531
- const theme = computed(() => {
532
- const themeId = selectedThemeId.value
533
- if (!themeId)
534
- return null
535
- const themeContents = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[themeId]?.theme || null
679
+ const themePreviewCache = useState('edge-cms-page-theme-preview-cache', () => ({}))
680
+ const themeCacheKey = computed(() => {
681
+ const orgId = String(edgeGlobal.edgeState.currentOrganization || 'no-org').trim() || 'no-org'
682
+ const siteKey = props.isTemplateSite ? 'templates' : String(props.site || 'no-site').trim() || 'no-site'
683
+ return `${orgId}:${siteKey}`
684
+ })
685
+
686
+ const hydrateThemeCache = () => {
687
+ const cache = themePreviewCache.value?.[themeCacheKey.value] || {}
688
+ return {
689
+ themeId: typeof cache?.themeId === 'string' ? cache.themeId : '',
690
+ theme: cache?.theme && typeof cache.theme === 'object' ? cache.theme : null,
691
+ head: cache?.head && typeof cache.head === 'object' ? cache.head : {},
692
+ }
693
+ }
694
+
695
+ const writeThemeCache = (patch = {}) => {
696
+ const current = themePreviewCache.value?.[themeCacheKey.value] || {}
697
+ themePreviewCache.value = {
698
+ ...(themePreviewCache.value || {}),
699
+ [themeCacheKey.value]: {
700
+ ...current,
701
+ ...patch,
702
+ },
703
+ }
704
+ }
705
+
706
+ const initialThemeCache = hydrateThemeCache()
707
+ const lastStableThemeId = ref(initialThemeCache.themeId)
708
+ const lastResolvedTheme = ref(initialThemeCache.theme)
709
+ const lastResolvedHead = ref(initialThemeCache.head)
710
+
711
+ const parseThemeDoc = (themeDoc) => {
712
+ const themeContents = themeDoc?.theme || null
536
713
  if (!themeContents)
537
714
  return null
715
+ const extraCSS = typeof themeDoc?.extraCSS === 'string' ? themeDoc.extraCSS : ''
538
716
  try {
539
- return typeof themeContents === 'string' ? JSON.parse(themeContents) : themeContents
717
+ const parsed = typeof themeContents === 'string' ? JSON.parse(themeContents) : themeContents
718
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
719
+ return null
720
+ return { ...parsed, extraCSS }
540
721
  }
541
- catch (e) {
722
+ catch {
723
+ return null
724
+ }
725
+ }
726
+
727
+ const parseHeadDoc = (themeDoc) => {
728
+ try {
729
+ const parsed = JSON.parse(themeDoc?.headJSON || '{}')
730
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
731
+ return parsed
732
+ }
733
+ catch {}
734
+ return {}
735
+ }
736
+
737
+ const applyResolvedTheme = (themeDoc, themeId = '') => {
738
+ const normalizedThemeId = String(themeId || themeDoc?.docId || '').trim()
739
+ if (normalizedThemeId)
740
+ lastStableThemeId.value = normalizedThemeId
741
+
742
+ const parsedTheme = parseThemeDoc(themeDoc)
743
+ if (parsedTheme && typeof parsedTheme === 'object') {
744
+ lastResolvedTheme.value = parsedTheme
745
+ writeThemeCache({ theme: parsedTheme })
746
+ }
747
+
748
+ const parsedHead = parseHeadDoc(themeDoc)
749
+ if (parsedHead && typeof parsedHead === 'object') {
750
+ lastResolvedHead.value = parsedHead
751
+ writeThemeCache({ head: parsedHead })
752
+ }
753
+
754
+ if (normalizedThemeId)
755
+ writeThemeCache({ themeId: normalizedThemeId })
756
+ }
757
+
758
+ const themeFallbackLoading = ref(false)
759
+ const loadSiteThemeFallback = async () => {
760
+ if (themeFallbackLoading.value)
761
+ return
762
+
763
+ const orgPath = String(edgeGlobal.edgeState.organizationDocPath || '').trim()
764
+ if (!orgPath)
765
+ return
766
+
767
+ const selectedId = String(selectedThemeId.value || '').trim()
768
+ if (props.isTemplateSite) {
769
+ if (!selectedId)
770
+ return
771
+ const fromSnapshot = edgeFirebase.data?.[`${orgPath}/themes`]?.[selectedId] || null
772
+ if (fromSnapshot)
773
+ applyResolvedTheme(fromSnapshot, selectedId)
774
+ return
775
+ }
776
+
777
+ const siteId = String(props.site || '').trim()
778
+ if (!siteId || siteId === 'new')
779
+ return
780
+
781
+ themeFallbackLoading.value = true
782
+ try {
783
+ let themeId = selectedId
784
+ if (!themeId) {
785
+ const siteDoc = await edgeFirebase.getDocData(`${orgPath}/sites`, siteId)
786
+ themeId = String(siteDoc?.theme || '').trim()
787
+ }
788
+ if (!themeId)
789
+ return
790
+
791
+ writeThemeCache({ themeId })
792
+ lastStableThemeId.value = themeId
793
+
794
+ const fromSnapshot = edgeFirebase.data?.[`${orgPath}/themes`]?.[themeId] || null
795
+ if (fromSnapshot) {
796
+ applyResolvedTheme(fromSnapshot, themeId)
797
+ if (lastResolvedTheme.value)
798
+ return
799
+ }
800
+
801
+ const themeDoc = await edgeFirebase.getDocData(`${orgPath}/themes`, themeId)
802
+ if (themeDoc)
803
+ applyResolvedTheme(themeDoc, themeId)
804
+ }
805
+ catch (error) {
806
+ console.error('Failed to load fallback theme for page preview', error)
807
+ }
808
+ finally {
809
+ themeFallbackLoading.value = false
810
+ }
811
+ }
812
+
813
+ watch(
814
+ () => edgeGlobal.edgeState.currentOrganization,
815
+ () => {
816
+ ensurePreviewSnapshots()
817
+ loadSiteThemeFallback()
818
+ },
819
+ { immediate: true },
820
+ )
821
+
822
+ watch(
823
+ () => [props.site, props.page, props.isTemplateSite],
824
+ () => {
825
+ loadSiteThemeFallback()
826
+ },
827
+ { immediate: true },
828
+ )
829
+
830
+ watch(
831
+ themeCacheKey,
832
+ () => {
833
+ const hydrated = hydrateThemeCache()
834
+ if (hydrated.themeId)
835
+ lastStableThemeId.value = hydrated.themeId
836
+ if (hydrated.theme && typeof hydrated.theme === 'object')
837
+ lastResolvedTheme.value = hydrated.theme
838
+ if (hydrated.head && typeof hydrated.head === 'object')
839
+ lastResolvedHead.value = hydrated.head
840
+ },
841
+ { immediate: true },
842
+ )
843
+
844
+ watch(selectedThemeId, (themeId) => {
845
+ const normalized = String(themeId || '').trim()
846
+ if (normalized) {
847
+ lastStableThemeId.value = normalized
848
+ writeThemeCache({ themeId: normalized })
849
+ }
850
+ loadSiteThemeFallback()
851
+ }, { immediate: true })
852
+
853
+ const effectiveThemeId = computed(() => {
854
+ const normalized = String(selectedThemeId.value || '').trim()
855
+ if (normalized)
856
+ return normalized
857
+ return lastStableThemeId.value
858
+ })
859
+
860
+ const parsedTheme = computed(() => {
861
+ const themeId = effectiveThemeId.value
862
+ if (!themeId)
542
863
  return null
864
+ const themeDoc = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[themeId] || null
865
+ return parseThemeDoc(themeDoc)
866
+ })
867
+
868
+ watch(parsedTheme, (nextTheme) => {
869
+ if (nextTheme && typeof nextTheme === 'object') {
870
+ lastResolvedTheme.value = nextTheme
871
+ writeThemeCache({ theme: nextTheme })
543
872
  }
873
+ }, { immediate: true, deep: true })
874
+
875
+ const theme = computed(() => {
876
+ return parsedTheme.value || lastResolvedTheme.value || null
544
877
  })
545
878
 
546
879
  const themeColorMap = computed(() => {
@@ -612,11 +945,7 @@ const rowUsesSpans = row => (row?.columns || []).some(col => Number.isFinite(col
612
945
 
613
946
  const rowGapClass = (row) => {
614
947
  const gap = Number(row?.gap)
615
- const allowed = new Set([0, 2, 4, 6, 8])
616
- const safeGap = allowed.has(gap) ? gap : 4
617
- if (safeGap === 0)
618
- return 'gap-0'
619
- return ['gap-0', `sm:gap-${safeGap}`].join(' ')
948
+ return ROW_GAP_CLASS_MAP[gap] || ROW_GAP_CLASS_MAP[4]
620
949
  }
621
950
 
622
951
  const rowGridClass = (row) => {
@@ -847,14 +1176,20 @@ const addRowAt = (workingDoc, layoutValue = '6', insertIndex = 0, isPost = false
847
1176
  }
848
1177
 
849
1178
  const headObject = computed(() => {
850
- const themeId = selectedThemeId.value
1179
+ const themeId = effectiveThemeId.value
851
1180
  if (!themeId)
852
- return {}
1181
+ return lastResolvedHead.value || {}
853
1182
  try {
854
- return JSON.parse(edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[themeId]?.headJSON || '{}')
1183
+ const parsedHead = parseHeadDoc(edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[themeId] || null)
1184
+ if (parsedHead && typeof parsedHead === 'object') {
1185
+ lastResolvedHead.value = parsedHead
1186
+ writeThemeCache({ head: parsedHead })
1187
+ return parsedHead
1188
+ }
1189
+ return lastResolvedHead.value || {}
855
1190
  }
856
1191
  catch (e) {
857
- return {}
1192
+ return lastResolvedHead.value || {}
858
1193
  }
859
1194
  })
860
1195
 
@@ -918,6 +1253,403 @@ const currentPage = computed(() => {
918
1253
  return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`]?.[props.page] || null
919
1254
  })
920
1255
 
1256
+ const pagesCollectionPath = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/sites/${props.site}/pages`)
1257
+ const pagesCollection = computed(() => edgeFirebase.data?.[pagesCollectionPath.value] || {})
1258
+ const pageEditorBasePath = computed(() => (props.isTemplateSite ? '/app/dashboard/templates' : `/app/dashboard/sites/${props.site}`))
1259
+ const INVALID_PAGE_IMPORT_MESSAGE = 'Invalid file. Please import a valid page file.'
1260
+
1261
+ const downloadJsonFile = (payload, filename) => {
1262
+ if (typeof window === 'undefined')
1263
+ return
1264
+ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
1265
+ const objectUrl = URL.createObjectURL(blob)
1266
+ const anchor = document.createElement('a')
1267
+ anchor.href = objectUrl
1268
+ anchor.download = filename
1269
+ document.body.appendChild(anchor)
1270
+ anchor.click()
1271
+ anchor.remove()
1272
+ URL.revokeObjectURL(objectUrl)
1273
+ }
1274
+
1275
+ const readTextFile = file => new Promise((resolve, reject) => {
1276
+ if (typeof FileReader === 'undefined') {
1277
+ reject(new Error('File import is only available in the browser.'))
1278
+ return
1279
+ }
1280
+ const reader = new FileReader()
1281
+ reader.onload = () => resolve(String(reader.result || ''))
1282
+ reader.onerror = () => reject(new Error('Could not read the selected file.'))
1283
+ reader.readAsText(file)
1284
+ })
1285
+
1286
+ const normalizeImportedDoc = (payload, fallbackDocId = '') => {
1287
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
1288
+ throw new Error(INVALID_PAGE_IMPORT_MESSAGE)
1289
+
1290
+ if (payload.document && typeof payload.document === 'object' && !Array.isArray(payload.document)) {
1291
+ const normalized = { ...payload.document }
1292
+ if (!normalized.docId && payload.docId)
1293
+ normalized.docId = payload.docId
1294
+ if (!normalized.docId && fallbackDocId)
1295
+ normalized.docId = fallbackDocId
1296
+ return normalized
1297
+ }
1298
+
1299
+ const normalized = { ...payload }
1300
+ if (!normalized.docId && fallbackDocId)
1301
+ normalized.docId = fallbackDocId
1302
+ return normalized
1303
+ }
1304
+
1305
+ const isPlainObject = value => !!value && typeof value === 'object' && !Array.isArray(value)
1306
+
1307
+ const cloneSchemaValue = (value) => {
1308
+ if (isPlainObject(value) || Array.isArray(value))
1309
+ return edgeGlobal.dupObject(value)
1310
+ return value
1311
+ }
1312
+
1313
+ const getDocDefaultsFromSchema = (schema = {}) => {
1314
+ const defaults = {}
1315
+ for (const [key, schemaEntry] of Object.entries(schema || {})) {
1316
+ const hasValueProp = isPlainObject(schemaEntry) && Object.prototype.hasOwnProperty.call(schemaEntry, 'value')
1317
+ const baseValue = hasValueProp ? schemaEntry.value : schemaEntry
1318
+ defaults[key] = cloneSchemaValue(baseValue)
1319
+ }
1320
+ return defaults
1321
+ }
1322
+
1323
+ const getPageDocDefaults = () => getDocDefaultsFromSchema(state.newDocs?.pages || {})
1324
+
1325
+ const isBlankString = value => String(value || '').trim() === ''
1326
+
1327
+ const applyImportedPageSeoDefaults = (doc) => {
1328
+ if (!isPlainObject(doc))
1329
+ return doc
1330
+
1331
+ if (isBlankString(doc.structuredData))
1332
+ doc.structuredData = buildPageStructuredData()
1333
+
1334
+ if (doc.post && isBlankString(doc.postStructuredData))
1335
+ doc.postStructuredData = doc.structuredData || buildPageStructuredData()
1336
+
1337
+ return doc
1338
+ }
1339
+
1340
+ const validateImportedPageDoc = (doc) => {
1341
+ if (!isPlainObject(doc))
1342
+ throw new Error(INVALID_PAGE_IMPORT_MESSAGE)
1343
+
1344
+ const requiredKeys = Object.keys(state.newDocs?.pages || {})
1345
+ const missing = requiredKeys.filter(key => !Object.prototype.hasOwnProperty.call(doc, key))
1346
+ if (missing.length)
1347
+ throw new Error(INVALID_PAGE_IMPORT_MESSAGE)
1348
+
1349
+ return doc
1350
+ }
1351
+
1352
+ const normalizeMenusForImport = (menus) => {
1353
+ const normalized = isPlainObject(menus) ? edgeGlobal.dupObject(menus) : {}
1354
+ if (!Array.isArray(normalized['Site Root']))
1355
+ normalized['Site Root'] = []
1356
+ if (!Array.isArray(normalized['Not In Menu']))
1357
+ normalized['Not In Menu'] = []
1358
+ return normalized
1359
+ }
1360
+
1361
+ const walkMenuEntries = (items, callback) => {
1362
+ if (!Array.isArray(items))
1363
+ return
1364
+ for (const entry of items) {
1365
+ if (!entry || typeof entry !== 'object')
1366
+ continue
1367
+ callback(entry)
1368
+ if (isPlainObject(entry.item)) {
1369
+ for (const nested of Object.values(entry.item)) {
1370
+ if (Array.isArray(nested))
1371
+ walkMenuEntries(nested, callback)
1372
+ }
1373
+ }
1374
+ }
1375
+ }
1376
+
1377
+ const menuIncludesDocId = (menus, docId) => {
1378
+ let found = false
1379
+ const checkEntry = (entry) => {
1380
+ if (found)
1381
+ return
1382
+ if (typeof entry?.item === 'string' && entry.item === docId)
1383
+ found = true
1384
+ }
1385
+ for (const menuItems of Object.values(menus || {})) {
1386
+ walkMenuEntries(menuItems, checkEntry)
1387
+ if (found)
1388
+ return true
1389
+ }
1390
+ return false
1391
+ }
1392
+
1393
+ const collectMenuPageNames = (menus) => {
1394
+ const names = new Set()
1395
+ const collectEntry = (entry) => {
1396
+ if (typeof entry?.item !== 'string')
1397
+ return
1398
+ const name = String(entry?.name || '').trim()
1399
+ if (name)
1400
+ names.add(name)
1401
+ }
1402
+ for (const menuItems of Object.values(menus || {}))
1403
+ walkMenuEntries(menuItems, collectEntry)
1404
+ return names
1405
+ }
1406
+
1407
+ const slugifyMenuPageName = (value) => {
1408
+ return String(value || '')
1409
+ .trim()
1410
+ .toLowerCase()
1411
+ .replace(/[^a-z0-9]+/g, '-')
1412
+ .replace(/(^-|-$)+/g, '') || 'page'
1413
+ }
1414
+ const titleFromSlug = (slug) => {
1415
+ if (!slug)
1416
+ return ''
1417
+ return String(slug).replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
1418
+ }
1419
+
1420
+ const makeUniqueMenuPageName = (value, existingNames = new Set()) => {
1421
+ const base = slugifyMenuPageName(value)
1422
+ let candidate = base
1423
+ let suffix = 2
1424
+ while (existingNames.has(candidate)) {
1425
+ candidate = `${base}-${suffix}`
1426
+ suffix += 1
1427
+ }
1428
+ return candidate
1429
+ }
1430
+
1431
+ const addImportedPageToSiteMenu = async (docId, pageName = '') => {
1432
+ const nextDocId = String(docId || '').trim()
1433
+ if (!nextDocId)
1434
+ return
1435
+ const siteId = String(props.site || '').trim()
1436
+ if (!siteId)
1437
+ return
1438
+
1439
+ const sitesCollectionPath = `${edgeGlobal.edgeState.organizationDocPath}/sites`
1440
+ const siteDoc = edgeFirebase.data?.[sitesCollectionPath]?.[siteId] || {}
1441
+ const menus = normalizeMenusForImport(siteDoc?.menus)
1442
+ if (menuIncludesDocId(menus, nextDocId))
1443
+ return
1444
+
1445
+ const existingNames = collectMenuPageNames(menus)
1446
+ const menuName = makeUniqueMenuPageName(pageName || nextDocId, existingNames)
1447
+ const menuTitle = String(pageName || '').trim() || titleFromSlug(menuName)
1448
+ menus['Site Root'].push({ name: menuName, menuTitle, item: nextDocId })
1449
+
1450
+ const results = await edgeFirebase.changeDoc(sitesCollectionPath, siteId, { menus })
1451
+ if (results?.success === false)
1452
+ throw new Error('Could not save updated site menu.')
1453
+ }
1454
+
1455
+ const makeRandomPageDocId = (docsMap = {}) => {
1456
+ let nextDocId = String(edgeGlobal.generateShortId() || '').trim()
1457
+ while (!nextDocId || docsMap[nextDocId])
1458
+ nextDocId = String(edgeGlobal.generateShortId() || '').trim()
1459
+ return nextDocId
1460
+ }
1461
+
1462
+ const makeImportedPageNameForNew = (baseName, docsMap = {}) => {
1463
+ const normalizedBase = String(baseName || '').trim() || 'page'
1464
+ const existingNames = new Set(
1465
+ Object.values(docsMap || {})
1466
+ .map(doc => String(doc?.name || '').trim().toLowerCase())
1467
+ .filter(Boolean),
1468
+ )
1469
+
1470
+ let suffix = 1
1471
+ let candidate = `${normalizedBase}-${suffix}`
1472
+ while (existingNames.has(candidate.toLowerCase())) {
1473
+ suffix += 1
1474
+ candidate = `${normalizedBase}-${suffix}`
1475
+ }
1476
+ return candidate
1477
+ }
1478
+
1479
+ const requestPageImportDocId = (initialValue = '') => {
1480
+ state.importDocIdValue = String(initialValue || '')
1481
+ state.importDocIdDialogOpen = true
1482
+ return new Promise((resolve) => {
1483
+ pageImportDocIdResolver.value = resolve
1484
+ })
1485
+ }
1486
+
1487
+ const resolvePageImportDocId = (value = '') => {
1488
+ const resolver = pageImportDocIdResolver.value
1489
+ pageImportDocIdResolver.value = null
1490
+ state.importDocIdDialogOpen = false
1491
+ if (resolver)
1492
+ resolver(String(value || '').trim())
1493
+ }
1494
+
1495
+ const requestPageImportConflict = (docId) => {
1496
+ state.importConflictDocId = String(docId || '')
1497
+ state.importConflictDialogOpen = true
1498
+ return new Promise((resolve) => {
1499
+ pageImportConflictResolver.value = resolve
1500
+ })
1501
+ }
1502
+
1503
+ const resolvePageImportConflict = (action = 'cancel') => {
1504
+ const resolver = pageImportConflictResolver.value
1505
+ pageImportConflictResolver.value = null
1506
+ state.importConflictDialogOpen = false
1507
+ if (resolver)
1508
+ resolver(action)
1509
+ }
1510
+
1511
+ watch(() => state.importDocIdDialogOpen, (open) => {
1512
+ if (!open && pageImportDocIdResolver.value) {
1513
+ const resolver = pageImportDocIdResolver.value
1514
+ pageImportDocIdResolver.value = null
1515
+ resolver('')
1516
+ }
1517
+ })
1518
+
1519
+ watch(() => state.importConflictDialogOpen, (open) => {
1520
+ if (!open && pageImportConflictResolver.value) {
1521
+ const resolver = pageImportConflictResolver.value
1522
+ pageImportConflictResolver.value = null
1523
+ resolver('cancel')
1524
+ }
1525
+ })
1526
+
1527
+ const getImportDocId = async (incomingDoc, fallbackDocId = '') => {
1528
+ let nextDocId = String(incomingDoc?.docId || '').trim()
1529
+ if (!nextDocId)
1530
+ nextDocId = await requestPageImportDocId(fallbackDocId)
1531
+ if (!nextDocId)
1532
+ throw new Error('Import canceled. A docId is required.')
1533
+ if (nextDocId.includes('/'))
1534
+ throw new Error('docId cannot include "/".')
1535
+ return nextDocId
1536
+ }
1537
+
1538
+ const notifySuccess = (message) => {
1539
+ edgeFirebase?.toast?.success?.(message)
1540
+ }
1541
+
1542
+ const notifyError = (message) => {
1543
+ edgeFirebase?.toast?.error?.(message)
1544
+ }
1545
+
1546
+ const openImportErrorDialog = (message) => {
1547
+ state.importErrorMessage = String(message || 'Failed to import page JSON.')
1548
+ state.importErrorDialogOpen = true
1549
+ }
1550
+
1551
+ const exportCurrentPage = () => {
1552
+ const doc = currentPage.value
1553
+ if (!doc || !props.page || props.page === 'new') {
1554
+ notifyError('Save this page before exporting.')
1555
+ return
1556
+ }
1557
+ const docId = String(doc.docId || props.page).trim()
1558
+ const exportPayload = { ...getPageDocDefaults(), ...doc, docId }
1559
+ downloadJsonFile(exportPayload, `page-${docId}.json`)
1560
+ notifySuccess(`Exported page "${docId}".`)
1561
+ }
1562
+
1563
+ const triggerPageImport = () => {
1564
+ pageImportInputRef.value?.click()
1565
+ }
1566
+
1567
+ const importSinglePageFile = async (file, existingPages = {}, fallbackDocId = '') => {
1568
+ const fileText = await readTextFile(file)
1569
+ const parsed = JSON.parse(fileText)
1570
+ const importedDoc = applyImportedPageSeoDefaults(validateImportedPageDoc(normalizeImportedDoc(parsed, fallbackDocId)))
1571
+ const incomingDocId = await getImportDocId(importedDoc, fallbackDocId)
1572
+ let targetDocId = incomingDocId
1573
+ let importDecision = 'create'
1574
+
1575
+ if (existingPages[targetDocId]) {
1576
+ const decision = await requestPageImportConflict(targetDocId)
1577
+ if (decision === 'cancel')
1578
+ return ''
1579
+ if (decision === 'new') {
1580
+ targetDocId = makeRandomPageDocId(existingPages)
1581
+ importedDoc.name = makeImportedPageNameForNew(importedDoc.name || incomingDocId, existingPages)
1582
+ importDecision = 'new'
1583
+ }
1584
+ else {
1585
+ importDecision = 'overwrite'
1586
+ }
1587
+ }
1588
+
1589
+ const isCreatingNewPage = !existingPages[targetDocId]
1590
+ const payload = { ...getPageDocDefaults(), ...importedDoc, docId: targetDocId }
1591
+ await edgeFirebase.storeDoc(pagesCollectionPath.value, payload, targetDocId)
1592
+ existingPages[targetDocId] = payload
1593
+
1594
+ if (isCreatingNewPage) {
1595
+ try {
1596
+ await addImportedPageToSiteMenu(targetDocId, importedDoc.name)
1597
+ }
1598
+ catch (menuError) {
1599
+ console.error('Imported page but failed to update site menu', menuError)
1600
+ openImportErrorDialog('Imported page, but could not add it to Site Menu automatically.')
1601
+ }
1602
+ }
1603
+
1604
+ if (importDecision === 'overwrite')
1605
+ notifySuccess(`Overwrote page "${targetDocId}".`)
1606
+ else if (importDecision === 'new')
1607
+ notifySuccess(`Imported page as new "${targetDocId}".`)
1608
+ else
1609
+ notifySuccess(`Imported page "${targetDocId}".`)
1610
+
1611
+ return targetDocId
1612
+ }
1613
+
1614
+ const handlePageImport = async (event) => {
1615
+ const input = event?.target
1616
+ const files = Array.from(input?.files || [])
1617
+ if (!files.length)
1618
+ return
1619
+
1620
+ state.importingJson = true
1621
+ const fallbackDocId = props.page !== 'new' ? props.page : ''
1622
+ const existingPages = { ...(pagesCollection.value || {}) }
1623
+ let lastImportedDocId = ''
1624
+ try {
1625
+ for (const file of files) {
1626
+ try {
1627
+ const importedDocId = await importSinglePageFile(file, existingPages, fallbackDocId)
1628
+ if (importedDocId)
1629
+ lastImportedDocId = importedDocId
1630
+ }
1631
+ catch (error) {
1632
+ console.error('Failed to import page JSON', error)
1633
+ const message = error?.message || 'Failed to import page JSON.'
1634
+ if (/^Import canceled\./i.test(message))
1635
+ continue
1636
+ if (error instanceof SyntaxError || message === INVALID_PAGE_IMPORT_MESSAGE)
1637
+ openImportErrorDialog(INVALID_PAGE_IMPORT_MESSAGE)
1638
+ else
1639
+ openImportErrorDialog(message)
1640
+ }
1641
+ }
1642
+
1643
+ if (files.length === 1 && lastImportedDocId && lastImportedDocId !== props.page)
1644
+ await router.push(`${pageEditorBasePath.value}/${lastImportedDocId}`)
1645
+ }
1646
+ finally {
1647
+ state.importingJson = false
1648
+ if (input)
1649
+ input.value = ''
1650
+ }
1651
+ }
1652
+
921
1653
  watch (currentPage, (newPage) => {
922
1654
  state.workingDoc.last_updated = newPage?.last_updated
923
1655
  state.workingDoc.metaTitle = newPage?.metaTitle
@@ -1132,7 +1864,7 @@ const hasUnsavedChanges = (changes) => {
1132
1864
  :doc-id="page"
1133
1865
  :schema="schemas.pages"
1134
1866
  :new-doc-schema="state.newDocs.pages"
1135
- class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none pt-0 px-0"
1867
+ class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none pt-0 px-0" :class="[!state.editMode ? 'cms-page-preview-mode' : '']"
1136
1868
  :show-footer="false"
1137
1869
  :save-redirect-override="`/app/dashboard/sites/${site}`"
1138
1870
  :no-close-after-save="true"
@@ -1141,7 +1873,7 @@ const hasUnsavedChanges = (changes) => {
1141
1873
  @unsaved-changes="hasUnsavedChanges"
1142
1874
  >
1143
1875
  <template #header="slotProps">
1144
- <div class="relative flex items-center bg-secondary p-2 justify-between sticky top-0 z-50 bg-primary rounded h-[50px]">
1876
+ <div class="relative flex items-center p-2 justify-between top-0 z-50 bg-gray-100 rounded h-[50px]">
1145
1877
  <span class="text-lg font-semibold whitespace-nowrap pr-1">{{ pageName }}</span>
1146
1878
 
1147
1879
  <div class="flex w-full items-center">
@@ -1188,22 +1920,62 @@ const hasUnsavedChanges = (changes) => {
1188
1920
  </div>
1189
1921
  <div class="w-full border-t border-border" aria-hidden="true" />
1190
1922
 
1191
- <div class="flex items-center gap-1 pr-3">
1192
- <span class="text-[11px] uppercase tracking-wide text-muted-foreground">Viewport</span>
1923
+ <div class="flex items-center gap-2 px-3">
1193
1924
  <edge-shad-button
1194
- v-for="option in previewViewportOptions"
1195
- :key="option.id"
1196
1925
  type="button"
1197
- variant="ghost"
1198
1926
  size="icon"
1199
- class="h-[26px] w-[26px] text-xs gap-1 border transition-colors"
1200
- :class="state.previewViewport === option.id ? 'bg-primary text-primary-foreground border-primary shadow-sm' : 'bg-muted text-foreground border-border hover:bg-muted/80'"
1201
- @click="setPreviewViewport(option.id)"
1927
+ variant="outline"
1928
+ class="h-8 w-8"
1929
+ :disabled="!currentPage || !props.page || props.page === 'new'"
1930
+ title="Export Page"
1931
+ aria-label="Export Page"
1932
+ @click="exportCurrentPage"
1202
1933
  >
1203
- <component :is="option.icon" class="w-3.5 h-3.5" />
1934
+ <Download class="w-3.5 h-3.5" />
1204
1935
  </edge-shad-button>
1205
1936
  </div>
1206
1937
 
1938
+ <div class="flex flex-col items-center gap-1 px-2">
1939
+ <div class="flex items-center gap-1">
1940
+ <edge-shad-button
1941
+ v-for="option in previewViewportOptions"
1942
+ :key="option.id"
1943
+ type="button"
1944
+ variant="ghost"
1945
+ size="icon"
1946
+ class="h-[26px] w-[26px] text-xs gap-1 border transition-colors"
1947
+ :class="state.previewViewport === option.id ? 'bg-primary text-primary-foreground border-primary shadow-sm' : 'bg-muted text-foreground border-border hover:bg-muted/80'"
1948
+ @click="setPreviewViewport(option.id)"
1949
+ >
1950
+ <component :is="option.icon" class="w-3.5 h-3.5" />
1951
+ </edge-shad-button>
1952
+ </div>
1953
+ <span class="text-[10px] leading-tight text-muted-foreground">Viewport</span>
1954
+ </div>
1955
+ <div v-if="hasPostView(slotProps.workingDoc)" class="flex flex-col items-center gap-1 px-2">
1956
+ <div class="flex items-center gap-1">
1957
+ <edge-shad-button
1958
+ type="button"
1959
+ variant="ghost"
1960
+ class="h-[26px] px-2 text-xs border transition-colors"
1961
+ :class="state.previewPageView === 'list' ? 'bg-primary text-primary-foreground border-primary shadow-sm' : 'bg-muted text-foreground border-border hover:bg-muted/80'"
1962
+ @click="setPreviewPageView('list')"
1963
+ >
1964
+ Index
1965
+ </edge-shad-button>
1966
+ <edge-shad-button
1967
+ type="button"
1968
+ variant="ghost"
1969
+ class="h-[26px] px-2 text-xs border transition-colors"
1970
+ :class="state.previewPageView === 'post' ? 'bg-primary text-primary-foreground border-primary shadow-sm' : 'bg-muted text-foreground border-border hover:bg-muted/80'"
1971
+ @click="setPreviewPageView('post')"
1972
+ >
1973
+ Detail
1974
+ </edge-shad-button>
1975
+ </div>
1976
+ <span class="text-[10px] leading-tight text-muted-foreground">View</span>
1977
+ </div>
1978
+
1207
1979
  <edge-shad-button variant="text" class="hover:text-primary/50 text-xs h-[26px] text-primary" @click="state.editMode = !state.editMode">
1208
1980
  <template v-if="state.editMode">
1209
1981
  <Eye class="w-4 h-4" />
@@ -1247,7 +2019,7 @@ const hasUnsavedChanges = (changes) => {
1247
2019
  </div>
1248
2020
  </template>
1249
2021
  <template #success-alert>
1250
- <div v-if="!props.isTemplateSite" class="mt-2 flex flex-wrap items-center gap-2">
2022
+ <div v-if="state.editMode && !props.isTemplateSite" class="mt-2 flex flex-wrap items-center gap-2">
1251
2023
  <edge-shad-button
1252
2024
  variant="outline"
1253
2025
  class="text-xs h-[28px] gap-1"
@@ -1264,22 +2036,15 @@ const hasUnsavedChanges = (changes) => {
1264
2036
  </div>
1265
2037
  </template>
1266
2038
  <template #main="slotProps">
1267
- <Tabs class="w-full" default-value="list">
1268
- <TabsList v-if="slotProps.workingDoc?.post" class="w-full mt-3 bg-primary rounded-sm">
1269
- <TabsTrigger value="list">
1270
- Index Page
1271
- </TabsTrigger>
1272
- <TabsTrigger value="post">
1273
- Detail Page
1274
- </TabsTrigger>
1275
- </TabsList>
1276
- <TabsContent value="list">
1277
- <Separator class="my-4" />
2039
+ <Tabs class="w-full" :model-value="hasPostView(slotProps.workingDoc) ? state.previewPageView : 'list'">
2040
+ <TabsContent value="list" class="mt-0">
1278
2041
  <div
1279
- :key="selectedThemeId"
1280
- class="w-full mx-auto bg-card border border-border rounded-lg shadow-sm md:shadow-md p-0 space-y-6"
1281
- :class="{ 'transition-all duration-300': !state.editMode }"
1282
- :style="previewViewportStyle"
2042
+ :key="`${pagePreviewRenderKey}:list`"
2043
+ data-cms-preview-surface="page"
2044
+ :data-cms-preview-mode="state.editMode ? 'edit' : 'preview'"
2045
+ class="w-full h-[calc(100vh-180px)] mt-2 overflow-y-auto mx-auto bg-card border border-border shadow-sm md:shadow-md p-0 space-y-6"
2046
+ :class="[{ 'transition-all duration-300': !state.editMode }, state.editMode ? 'rounded-lg' : 'rounded-none']"
2047
+ :style="previewViewportContainStyle"
1283
2048
  >
1284
2049
  <edge-button-divider v-if="state.editMode" class="my-2">
1285
2050
  <Popover v-model:open="state.addRowPopoverOpen.listTop">
@@ -1419,9 +2184,13 @@ const hasUnsavedChanges = (changes) => {
1419
2184
  <div :key="blockId" class="relative group">
1420
2185
  <edge-cms-block
1421
2186
  v-if="blockIndex(slotProps.workingDoc, blockId, false) !== -1"
2187
+ :key="`${pagePreviewRenderKey}:${blockId}:${effectiveThemeId}:list`"
1422
2188
  v-model="slotProps.workingDoc.content[blockIndex(slotProps.workingDoc, blockId, false)]"
1423
2189
  :site-id="props.site"
1424
2190
  :edit-mode="state.editMode"
2191
+ :override-clicks-in-edit-mode="state.editMode"
2192
+ :allow-preview-content-edit="!state.editMode && canOpenPreviewBlockContentEditor"
2193
+ :contain-fixed="state.editMode"
1425
2194
  :viewport-mode="previewViewportMode"
1426
2195
  :block-id="blockId"
1427
2196
  :theme="theme"
@@ -1524,13 +2293,14 @@ const hasUnsavedChanges = (changes) => {
1524
2293
  </edge-button-divider>
1525
2294
  </div>
1526
2295
  </TabsContent>
1527
- <TabsContent value="post">
1528
- <Separator class="my-4" />
2296
+ <TabsContent v-if="hasPostView(slotProps.workingDoc)" value="post" class="mt-0">
1529
2297
  <div
1530
- :key="`${selectedThemeId}-post`"
1531
- class="w-full mx-auto bg-card border border-border rounded-lg shadow-sm md:shadow-md p-4 space-y-6"
1532
- :class="{ 'transition-all duration-300': !state.editMode }"
1533
- :style="previewViewportStyle"
2298
+ :key="`${pagePreviewRenderKey}:post`"
2299
+ data-cms-preview-surface="page"
2300
+ :data-cms-preview-mode="state.editMode ? 'edit' : 'preview'"
2301
+ class="w-full h-[calc(100vh-180px)] mt-2 overflow-y-auto mx-auto bg-card border border-border shadow-sm md:shadow-md p-0 space-y-6"
2302
+ :class="[{ 'transition-all duration-300': !state.editMode }, state.editMode ? 'rounded-lg' : 'rounded-none']"
2303
+ :style="previewViewportContainStyle"
1534
2304
  >
1535
2305
  <edge-button-divider v-if="state.editMode" class="my-2">
1536
2306
  <Popover v-model:open="state.addRowPopoverOpen.postTop">
@@ -1670,8 +2440,12 @@ const hasUnsavedChanges = (changes) => {
1670
2440
  <div :key="blockId" class="relative group">
1671
2441
  <edge-cms-block
1672
2442
  v-if="blockIndex(slotProps.workingDoc, blockId, true) !== -1"
2443
+ :key="`${pagePreviewRenderKey}:${blockId}:${effectiveThemeId}:post`"
1673
2444
  v-model="slotProps.workingDoc.postContent[blockIndex(slotProps.workingDoc, blockId, true)]"
1674
2445
  :edit-mode="state.editMode"
2446
+ :override-clicks-in-edit-mode="state.editMode"
2447
+ :allow-preview-content-edit="!state.editMode && canOpenPreviewBlockContentEditor"
2448
+ :contain-fixed="state.editMode"
1675
2449
  :viewport-mode="previewViewportMode"
1676
2450
  :block-id="blockId"
1677
2451
  :theme="theme"
@@ -1846,6 +2620,72 @@ const hasUnsavedChanges = (changes) => {
1846
2620
  </Sheet>
1847
2621
  </template>
1848
2622
  </edge-editor>
2623
+ <edge-shad-dialog v-model="state.importDocIdDialogOpen">
2624
+ <DialogContent class="pt-8">
2625
+ <DialogHeader>
2626
+ <DialogTitle class="text-left">
2627
+ Enter Page Doc ID
2628
+ </DialogTitle>
2629
+ <DialogDescription>
2630
+ This JSON file does not include a <code>docId</code>. Enter the doc ID you want to import into this site.
2631
+ </DialogDescription>
2632
+ </DialogHeader>
2633
+ <edge-shad-input
2634
+ v-model="state.importDocIdValue"
2635
+ name="page-import-doc-id"
2636
+ label="Doc ID"
2637
+ placeholder="example-page-id"
2638
+ />
2639
+ <DialogFooter class="pt-2 flex justify-between">
2640
+ <edge-shad-button variant="outline" @click="resolvePageImportDocId('')">
2641
+ Cancel
2642
+ </edge-shad-button>
2643
+ <edge-shad-button @click="resolvePageImportDocId(state.importDocIdValue)">
2644
+ Continue
2645
+ </edge-shad-button>
2646
+ </DialogFooter>
2647
+ </DialogContent>
2648
+ </edge-shad-dialog>
2649
+ <edge-shad-dialog v-model="state.importConflictDialogOpen">
2650
+ <DialogContent class="pt-8">
2651
+ <DialogHeader>
2652
+ <DialogTitle class="text-left">
2653
+ Page Already Exists
2654
+ </DialogTitle>
2655
+ <DialogDescription>
2656
+ <code>{{ state.importConflictDocId }}</code> already exists in this site. Choose to overwrite it or import as a new page.
2657
+ </DialogDescription>
2658
+ </DialogHeader>
2659
+ <DialogFooter class="pt-2 flex justify-between">
2660
+ <edge-shad-button variant="outline" @click="resolvePageImportConflict('cancel')">
2661
+ Cancel
2662
+ </edge-shad-button>
2663
+ <edge-shad-button variant="outline" @click="resolvePageImportConflict('new')">
2664
+ Add As New
2665
+ </edge-shad-button>
2666
+ <edge-shad-button @click="resolvePageImportConflict('overwrite')">
2667
+ Overwrite
2668
+ </edge-shad-button>
2669
+ </DialogFooter>
2670
+ </DialogContent>
2671
+ </edge-shad-dialog>
2672
+ <edge-shad-dialog v-model="state.importErrorDialogOpen">
2673
+ <DialogContent class="pt-8">
2674
+ <DialogHeader>
2675
+ <DialogTitle class="text-left">
2676
+ Import Failed
2677
+ </DialogTitle>
2678
+ <DialogDescription class="text-left">
2679
+ {{ state.importErrorMessage }}
2680
+ </DialogDescription>
2681
+ </DialogHeader>
2682
+ <DialogFooter class="pt-2">
2683
+ <edge-shad-button @click="state.importErrorDialogOpen = false">
2684
+ Close
2685
+ </edge-shad-button>
2686
+ </DialogFooter>
2687
+ </DialogContent>
2688
+ </edge-shad-dialog>
1849
2689
  <edge-shad-dialog v-model="state.showUnpublishedChangesDialog">
1850
2690
  <DialogContent class="max-w-2xl">
1851
2691
  <DialogHeader>
@@ -1924,4 +2764,8 @@ const hasUnsavedChanges = (changes) => {
1924
2764
  .block-drag-handle:active {
1925
2765
  cursor: grabbing;
1926
2766
  }
2767
+
2768
+ .cms-page-preview-mode :deep(.border-emerald-200.bg-emerald-50) {
2769
+ display: none !important;
2770
+ }
1927
2771
  </style>