@edgedev/create-edge-app 1.2.32 → 1.2.34

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 (33) hide show
  1. package/deploy.sh +77 -0
  2. package/edge/components/cms/block.vue +228 -18
  3. package/edge/components/cms/blockApi.vue +3 -3
  4. package/edge/components/cms/blockEditor.vue +374 -85
  5. package/edge/components/cms/blockPicker.vue +29 -3
  6. package/edge/components/cms/blockRender.vue +3 -3
  7. package/edge/components/cms/blocksManager.vue +755 -82
  8. package/edge/components/cms/codeEditor.vue +15 -6
  9. package/edge/components/cms/fontUpload.vue +318 -2
  10. package/edge/components/cms/htmlContent.vue +230 -89
  11. package/edge/components/cms/menu.vue +5 -4
  12. package/edge/components/cms/page.vue +750 -21
  13. package/edge/components/cms/site.vue +624 -84
  14. package/edge/components/cms/sitesManager.vue +5 -4
  15. package/edge/components/cms/themeEditor.vue +196 -162
  16. package/edge/components/editor.vue +5 -1
  17. package/edge/composables/global.ts +37 -5
  18. package/edge/composables/useCmsNewDocs.js +100 -0
  19. package/edge/composables/useEdgeCmsDialogPositionFix.js +19 -0
  20. package/edge/routes/cms/dashboard/blocks/[block].vue +5 -0
  21. package/edge/routes/cms/dashboard/blocks/index.vue +12 -1
  22. package/edge/routes/cms/dashboard/media/index.vue +5 -0
  23. package/edge/routes/cms/dashboard/sites/[site]/[[page]].vue +4 -0
  24. package/edge/routes/cms/dashboard/sites/[site].vue +4 -0
  25. package/edge/routes/cms/dashboard/sites/index.vue +4 -0
  26. package/edge/routes/cms/dashboard/templates/[page].vue +4 -0
  27. package/edge/routes/cms/dashboard/templates/index.vue +4 -0
  28. package/edge/routes/cms/dashboard/themes/[theme].vue +5 -0
  29. package/edge/routes/cms/dashboard/themes/index.vue +330 -1
  30. package/firebase.json +4 -0
  31. package/nuxt.config.ts +1 -1
  32. package/package.json +2 -2
  33. package/pages/app.vue +12 -12
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { HelpCircle, Maximize2, Monitor, Smartphone, Tablet } from 'lucide-vue-next'
2
+ import { Download, HelpCircle, Maximize2, Monitor, Smartphone, Tablet } from 'lucide-vue-next'
3
3
  import { toTypedSchema } from '@vee-validate/zod'
4
4
  import * as z from 'zod'
5
5
  const props = defineProps({
@@ -12,20 +12,12 @@ const props = defineProps({
12
12
  const emit = defineEmits(['head'])
13
13
 
14
14
  const edgeFirebase = inject('edgeFirebase')
15
-
16
- const route = useRoute()
15
+ const { blocks: blockNewDocSchema } = useCmsNewDocs()
17
16
 
18
17
  const state = reactive({
19
18
  filter: '',
20
19
  newDocs: {
21
- blocks: {
22
- name: { value: '' },
23
- content: { value: '' },
24
- tags: { value: [] },
25
- themes: { value: [] },
26
- synced: { value: false },
27
- version: 1,
28
- },
20
+ blocks: blockNewDocSchema.value,
29
21
  },
30
22
  mounted: false,
31
23
  workingDoc: {},
@@ -40,6 +32,8 @@ const state = reactive({
40
32
  seedingInitialBlocks: false,
41
33
  previewViewport: 'full',
42
34
  previewBlock: null,
35
+ editorWorkingDoc: null,
36
+ themeDefaultAppliedForBlockId: '',
43
37
  })
44
38
 
45
39
  const blockSchema = toTypedSchema(z.object({
@@ -54,6 +48,14 @@ const previewViewportOptions = [
54
48
  { id: 'medium', label: 'Medium', width: '992px', icon: Tablet },
55
49
  { id: 'mobile', label: 'Mobile', width: '420px', icon: Smartphone },
56
50
  ]
51
+ const previewTypeOptions = [
52
+ { name: 'light', title: 'Light Preview' },
53
+ { name: 'dark', title: 'Dark Preview' },
54
+ ]
55
+
56
+ const normalizePreviewType = (value) => {
57
+ return value === 'dark' ? 'dark' : 'light'
58
+ }
57
59
 
58
60
  const selectedPreviewViewport = computed(() => previewViewportOptions.find(option => option.id === state.previewViewport) || previewViewportOptions[0])
59
61
 
@@ -79,6 +81,19 @@ const previewViewportMode = computed(() => {
79
81
  return state.previewViewport
80
82
  })
81
83
 
84
+ const previewSurfaceClass = computed(() => {
85
+ const previewType = normalizePreviewType(state.previewBlock?.previewType)
86
+ return previewType === 'light'
87
+ ? 'bg-white text-black'
88
+ : 'bg-neutral-950 text-neutral-50'
89
+ })
90
+
91
+ const previewCanvasClass = computed(() => {
92
+ const content = String(state.previewBlock?.content || '')
93
+ const hasFixedContent = /\bfixed\b/.test(content)
94
+ return hasFixedContent ? 'min-h-[calc(100vh-360px)]' : 'min-h-[88px]'
95
+ })
96
+
82
97
  onMounted(() => {
83
98
  // state.mounted = true
84
99
  })
@@ -92,7 +107,7 @@ const PLACEHOLDERS = {
92
107
  'Consectetur adipiscing elit.',
93
108
  'Sed do eiusmod tempor incididunt.',
94
109
  ],
95
- image: '/images/filler.png',
110
+ image: 'https://imagedelivery.net/h7EjKG0X9kOxmLp41mxOng/f1f7f610-dfa9-4011-08a3-7a98d95e7500/thumbnail',
96
111
  }
97
112
 
98
113
  const contentEditorRef = ref(null)
@@ -172,6 +187,14 @@ function insertBlockContentSnippet(snippet) {
172
187
  editor.insertSnippet(snippet)
173
188
  }
174
189
 
190
+ const updateWorkingPreviewType = (nextValue) => {
191
+ const normalized = normalizePreviewType(nextValue)
192
+ if (state.editorWorkingDoc)
193
+ state.editorWorkingDoc.previewType = normalized
194
+ if (state.previewBlock)
195
+ state.previewBlock.previewType = normalized
196
+ }
197
+
175
198
  function normalizeConfigLiteral(str) {
176
199
  // ensure keys are quoted: { title: "x", field: "y" } -> { "title": "x", "field": "y" }
177
200
  return str
@@ -451,6 +474,7 @@ const buildPreviewBlock = (workingDoc, parsed) => {
451
474
  id: state.previewBlock?.id || 'preview',
452
475
  blockId: props.blockId,
453
476
  name: workingDoc?.name || state.previewBlock?.name || '',
477
+ previewType: normalizePreviewType(workingDoc?.previewType),
454
478
  content,
455
479
  values: nextValues,
456
480
  meta: nextMeta,
@@ -459,15 +483,32 @@ const buildPreviewBlock = (workingDoc, parsed) => {
459
483
  }
460
484
 
461
485
  const theme = computed(() => {
462
- const theme = edgeGlobal.edgeState.blockEditorTheme || ''
463
- let themeContents = null
464
- if (theme) {
465
- themeContents = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[theme]?.theme || null
486
+ const selectedThemeId = String(edgeGlobal.edgeState.blockEditorTheme || '').trim()
487
+ if (!selectedThemeId)
488
+ return null
489
+ const themeDoc = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[selectedThemeId] || null
490
+ const themeContents = themeDoc?.theme || null
491
+ if (!themeContents)
492
+ return null
493
+ const extraCSS = typeof themeDoc?.extraCSS === 'string' ? themeDoc.extraCSS : ''
494
+ if (typeof themeContents === 'object' && !Array.isArray(themeContents))
495
+ return { ...themeContents, extraCSS }
496
+ try {
497
+ const parsedTheme = JSON.parse(themeContents)
498
+ if (!parsedTheme || typeof parsedTheme !== 'object' || Array.isArray(parsedTheme))
499
+ return null
500
+ return { ...parsedTheme, extraCSS }
466
501
  }
467
- if (themeContents) {
468
- return JSON.parse(themeContents)
502
+ catch {
503
+ return null
469
504
  }
470
- return null
505
+ })
506
+
507
+ const previewThemeRenderKey = computed(() => {
508
+ const themeId = String(edgeGlobal.edgeState.blockEditorTheme || 'no-theme')
509
+ const siteId = String(edgeGlobal.edgeState.blockEditorSite || 'no-site')
510
+ const previewType = normalizePreviewType(state.previewBlock?.previewType)
511
+ return `${themeId}:${siteId}:${state.previewViewport}:${previewType}`
471
512
  })
472
513
 
473
514
  const headObject = computed(() => {
@@ -485,6 +526,7 @@ watch(headObject, (newHeadElements) => {
485
526
  }, { immediate: true, deep: true })
486
527
 
487
528
  const editorDocUpdates = (workingDoc) => {
529
+ state.editorWorkingDoc = workingDoc || null
488
530
  const parsed = blockModel(workingDoc.content)
489
531
  state.workingDoc = parsed
490
532
  state.previewBlock = buildPreviewBlock(workingDoc, parsed)
@@ -510,11 +552,51 @@ const themes = computed(() => {
510
552
  return Object.values(edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`] || {})
511
553
  })
512
554
 
513
- watch (themes, async (newThemes) => {
514
- state.loading = true
515
- if (!edgeGlobal.edgeState.blockEditorTheme && newThemes.length > 0) {
516
- edgeGlobal.edgeState.blockEditorTheme = newThemes[0].docId
555
+ const availableThemeIds = computed(() => {
556
+ return themes.value
557
+ .map(themeDoc => String(themeDoc?.docId || '').trim())
558
+ .filter(Boolean)
559
+ })
560
+
561
+ const currentBlockAllowedThemeIds = computed(() => {
562
+ const currentBlockDoc = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/blocks`]?.[props.blockId]
563
+ if (!Array.isArray(currentBlockDoc?.themes))
564
+ return []
565
+ return currentBlockDoc.themes.map(themeId => String(themeId || '').trim()).filter(Boolean)
566
+ })
567
+
568
+ const preferredThemeDefaultForBlock = computed(() => {
569
+ const firstAllowedAvailable = currentBlockAllowedThemeIds.value.find(themeId => availableThemeIds.value.includes(themeId))
570
+ if (firstAllowedAvailable)
571
+ return firstAllowedAvailable
572
+ return availableThemeIds.value[0] || ''
573
+ })
574
+
575
+ const applyThemeDefaultForBlock = () => {
576
+ const blockId = String(props.blockId || '').trim()
577
+ if (!blockId)
578
+ return
579
+ if (state.themeDefaultAppliedForBlockId === blockId)
580
+ return
581
+
582
+ const preferredThemeId = preferredThemeDefaultForBlock.value
583
+ if (!preferredThemeId) {
584
+ if (!availableThemeIds.value.length)
585
+ edgeGlobal.edgeState.blockEditorTheme = ''
586
+ return
517
587
  }
588
+
589
+ edgeGlobal.edgeState.blockEditorTheme = preferredThemeId
590
+ state.themeDefaultAppliedForBlockId = blockId
591
+ }
592
+
593
+ watch(() => props.blockId, () => {
594
+ state.themeDefaultAppliedForBlockId = ''
595
+ }, { immediate: true })
596
+
597
+ watch([availableThemeIds, currentBlockAllowedThemeIds, () => props.blockId], async () => {
598
+ state.loading = true
599
+ applyThemeDefaultForBlock()
518
600
  await nextTick()
519
601
  state.loading = false
520
602
  }, { immediate: true, deep: true })
@@ -567,6 +649,59 @@ const getTagsFromBlocks = computed(() => {
567
649
  // Always prepend it
568
650
  return [{ name: 'Quick Picks', title: 'Quick Picks' }, ...filtered]
569
651
  })
652
+
653
+ const downloadJsonFile = (payload, filename) => {
654
+ if (typeof window === 'undefined')
655
+ return
656
+ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
657
+ const objectUrl = URL.createObjectURL(blob)
658
+ const anchor = document.createElement('a')
659
+ anchor.href = objectUrl
660
+ anchor.download = filename
661
+ document.body.appendChild(anchor)
662
+ anchor.click()
663
+ anchor.remove()
664
+ URL.revokeObjectURL(objectUrl)
665
+ }
666
+
667
+ const isPlainObject = value => !!value && typeof value === 'object' && !Array.isArray(value)
668
+
669
+ const cloneSchemaValue = (value) => {
670
+ if (isPlainObject(value) || Array.isArray(value))
671
+ return edgeGlobal.dupObject(value)
672
+ return value
673
+ }
674
+
675
+ const getDocDefaultsFromSchema = (schema = {}) => {
676
+ const defaults = {}
677
+ for (const [key, schemaEntry] of Object.entries(schema || {})) {
678
+ const hasValueProp = isPlainObject(schemaEntry) && Object.prototype.hasOwnProperty.call(schemaEntry, 'value')
679
+ const baseValue = hasValueProp ? schemaEntry.value : schemaEntry
680
+ defaults[key] = cloneSchemaValue(baseValue)
681
+ }
682
+ return defaults
683
+ }
684
+
685
+ const getBlockDocDefaults = () => getDocDefaultsFromSchema(blockNewDocSchema.value || {})
686
+
687
+ const notifySuccess = (message) => {
688
+ edgeFirebase?.toast?.success?.(message)
689
+ }
690
+
691
+ const notifyError = (message) => {
692
+ edgeFirebase?.toast?.error?.(message)
693
+ }
694
+
695
+ const exportCurrentBlock = () => {
696
+ const doc = blocks.value?.[props.blockId]
697
+ if (!doc || !doc.docId) {
698
+ notifyError('Save this block before exporting.')
699
+ return
700
+ }
701
+ const exportPayload = { ...getBlockDocDefaults(), ...doc }
702
+ downloadJsonFile(exportPayload, `block-${doc.docId}.json`)
703
+ notifySuccess(`Exported block "${doc.docId}".`)
704
+ }
570
705
  </script>
571
706
 
572
707
  <template>
@@ -578,7 +713,9 @@ const getTagsFromBlocks = computed(() => {
578
713
  :doc-id="props.blockId"
579
714
  :schema="blockSchema"
580
715
  :new-doc-schema="state.newDocs.blocks"
581
- class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none"
716
+ header-class="py-2 bg-secondary text-foreground rounded-none sticky top-0 border"
717
+ class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none pt-0 px-0"
718
+ card-content-class="px-0"
582
719
  :show-footer="false"
583
720
  :no-close-after-save="true"
584
721
  :working-doc-overrides="state.workingDoc"
@@ -589,29 +726,52 @@ const getTagsFromBlocks = computed(() => {
589
726
  {{ slotProps.title }}
590
727
  </template>
591
728
  <template #header-center>
592
- <div class="w-full flex gap-1 px-4">
593
- <div class="w-1/2">
729
+ <div class="w-full flex gap-2 px-4 items-center">
730
+ <div class="flex-1">
594
731
  <edge-shad-select
595
732
  v-if="!state.loading"
596
733
  v-model="edgeGlobal.edgeState.blockEditorTheme"
597
- label="Theme Viewer Select"
598
734
  name="theme"
599
735
  :items="themes.map(t => ({ title: t.name, name: t.docId }))"
600
736
  placeholder="Theme Viewer Select"
601
737
  class="w-full"
602
738
  />
603
739
  </div>
604
- <div class="w-1/2">
740
+ <div class="flex-1">
605
741
  <edge-shad-select
606
742
  v-if="!state.loading"
607
743
  v-model="edgeGlobal.edgeState.blockEditorSite"
608
- label="Site"
609
744
  name="site"
610
745
  :items="sites.map(s => ({ title: s.name, name: s.docId }))"
611
746
  placeholder="Select Site"
612
747
  class="w-full"
613
748
  />
614
749
  </div>
750
+ <div class="flex-1">
751
+ <edge-shad-select
752
+ v-if="!state.loading"
753
+ :model-value="state.editorWorkingDoc?.previewType || 'light'"
754
+ name="previewType"
755
+ :items="previewTypeOptions"
756
+ placeholder="Preview Surface"
757
+ class="w-full"
758
+ @update:model-value="updateWorkingPreviewType($event)"
759
+ />
760
+ </div>
761
+ <div class="flex items-center gap-2">
762
+ <edge-shad-button
763
+ type="button"
764
+ size="icon"
765
+ variant="outline"
766
+ class="h-9 w-9"
767
+ :disabled="props.blockId === 'new' || !blocks?.[props.blockId]"
768
+ title="Export Block"
769
+ aria-label="Export Block"
770
+ @click="exportCurrentBlock"
771
+ >
772
+ <Download class="h-4 w-4" />
773
+ </edge-shad-button>
774
+ </div>
615
775
  </div>
616
776
  </template>
617
777
  <template #main="slotProps">
@@ -659,55 +819,53 @@ const getTagsFromBlocks = computed(() => {
659
819
  </div>
660
820
  <div class="flex gap-4">
661
821
  <div class="w-1/2">
662
- <div class="flex gap-2">
663
- <div class="w-2/12 mb-3 rounded-md border border-slate-200 bg-white/80 p-3 shadow-sm shadow-slate-200/60 dark:border-slate-800 dark:bg-slate-900/60">
664
- <div class="flex flex-col gap-2">
665
- <edge-shad-button
666
- type="button"
667
- size="sm"
668
- variant="secondary"
669
- class="w-full h-8 px-2 text-[11px] uppercase tracking-wide gap-2"
670
- @click="state.helpOpen = true"
671
- >
672
- <HelpCircle class="w-4 h-4" />
673
- Block Help
674
- </edge-shad-button>
675
- <div class="text-xs font-semibold uppercase tracking-wide text-slate-600 dark:text-slate-300 whitespace-nowrap">
676
- Dynamic Content
677
- </div>
678
- </div>
679
- <div class="mt-2 flex flex-wrap gap-2">
680
- <edge-tooltip
681
- v-for="snippet in BLOCK_CONTENT_SNIPPETS"
682
- :key="snippet.label"
683
- >
822
+ <edge-cms-code-editor
823
+ ref="contentEditorRef"
824
+ v-model="slotProps.workingDoc.content"
825
+ title="Block Content"
826
+ language="handlebars"
827
+ name="content"
828
+ :enable-formatting="false"
829
+ height="calc(100vh - 300px)"
830
+ class="mb-4 flex-1"
831
+ @line-click="payload => handleEditorLineClick(payload, slotProps.workingDoc)"
832
+ >
833
+ <template #end-actions>
834
+ <DropdownMenu>
835
+ <DropdownMenuTrigger as-child>
684
836
  <edge-shad-button
837
+ type="button"
685
838
  size="sm"
686
839
  variant="outline"
687
- class="text-xs w-full"
688
- @click="insertBlockContentSnippet(snippet.snippet)"
840
+ class="h-8 px-2 text-[11px] uppercase tracking-wide"
689
841
  >
690
- {{ snippet.label }}
842
+ Dynamic Content
691
843
  </edge-shad-button>
692
- <template #content>
693
- <pre class="max-w-[320px] whitespace-pre-wrap break-words text-left text-xs font-mono leading-tight">{{ snippet.snippet }}</pre>
694
- </template>
695
- </edge-tooltip>
696
- </div>
697
- </div>
698
- <div class="w-10/12">
699
- <edge-cms-code-editor
700
- ref="contentEditorRef"
701
- v-model="slotProps.workingDoc.content"
702
- title="Block Content"
703
- language="html"
704
- name="content"
705
- height="calc(100vh - 300px)"
706
- class="mb-4 flex-1"
707
- @line-click="payload => handleEditorLineClick(payload, slotProps.workingDoc)"
708
- />
709
- </div>
710
- </div>
844
+ </DropdownMenuTrigger>
845
+ <DropdownMenuContent align="end" class="w-72">
846
+ <DropdownMenuItem
847
+ v-for="snippet in BLOCK_CONTENT_SNIPPETS"
848
+ :key="snippet.label"
849
+ class="cursor-pointer flex-col items-start gap-0.5"
850
+ @click="insertBlockContentSnippet(snippet.snippet)"
851
+ >
852
+ <span class="text-sm font-medium">{{ snippet.label }}</span>
853
+ <span class="text-xs text-muted-foreground whitespace-normal">{{ snippet.description }}</span>
854
+ </DropdownMenuItem>
855
+ </DropdownMenuContent>
856
+ </DropdownMenu>
857
+ <edge-shad-button
858
+ type="button"
859
+ size="sm"
860
+ variant="secondary"
861
+ class="h-8 px-2 text-[11px] uppercase tracking-wide gap-2"
862
+ @click="state.helpOpen = true"
863
+ >
864
+ <HelpCircle class="w-4 h-4" />
865
+ Block Help
866
+ </edge-shad-button>
867
+ </template>
868
+ </edge-cms-code-editor>
711
869
  </div>
712
870
  <div class="w-1/2 space-y-2">
713
871
  <div class="flex items-center justify-between">
@@ -728,19 +886,24 @@ const getTagsFromBlocks = computed(() => {
728
886
  </div>
729
887
  </div>
730
888
  <div
731
- class="w-full mx-auto bg-card border border-border rounded-lg shadow-sm md:shadow-md"
889
+ class="w-full mx-auto rounded-none overflow-visible"
732
890
  :style="previewViewportStyle"
733
891
  >
734
- <edge-cms-block
735
- v-if="state.previewBlock"
736
- v-model="state.previewBlock"
737
- :site-id="edgeGlobal.edgeState.blockEditorSite"
738
- :theme="theme"
739
- :edit-mode="true"
740
- :viewport-mode="previewViewportMode"
741
- :block-id="state.previewBlock.id"
742
- @delete="ignorePreviewDelete"
743
- />
892
+ <div class="relative overflow-visible rounded-none" :class="[previewSurfaceClass, previewCanvasClass]" style="transform: translateZ(0);">
893
+ <edge-cms-block
894
+ v-if="state.previewBlock"
895
+ :key="previewThemeRenderKey"
896
+ v-model="state.previewBlock"
897
+ :site-id="edgeGlobal.edgeState.blockEditorSite"
898
+ :theme="theme"
899
+ :edit-mode="true"
900
+ :contain-fixed="true"
901
+ :allow-delete="false"
902
+ :viewport-mode="previewViewportMode"
903
+ :block-id="state.previewBlock.id"
904
+ @delete="ignorePreviewDelete"
905
+ />
906
+ </div>
744
907
  </div>
745
908
  </div>
746
909
  </div>
@@ -759,13 +922,16 @@ const getTagsFromBlocks = computed(() => {
759
922
  </SheetHeader>
760
923
  <div class="px-6 pb-6">
761
924
  <Tabs class="w-full" default-value="guide">
762
- <TabsList class="w-full mt-3 bg-secondary rounded-sm grid grid-cols-3">
925
+ <TabsList class="w-full mt-3 bg-secondary rounded-sm grid grid-cols-4">
763
926
  <TabsTrigger value="guide" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
764
927
  Block Guide
765
928
  </TabsTrigger>
766
929
  <TabsTrigger value="carousel" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
767
930
  Carousel Usage
768
931
  </TabsTrigger>
932
+ <TabsTrigger value="nav-bar" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
933
+ Nav Bar
934
+ </TabsTrigger>
769
935
  <TabsTrigger value="scroll-reveals" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
770
936
  Scroll Reveals
771
937
  </TabsTrigger>
@@ -1296,6 +1462,129 @@ const getTagsFromBlocks = computed(() => {
1296
1462
  </div>
1297
1463
  </TabsContent>
1298
1464
 
1465
+ <TabsContent value="nav-bar">
1466
+ <div class="h-[calc(100vh-190px)] overflow-y-auto pr-1 pb-6">
1467
+ <div class="space-y-6">
1468
+ <section class="space-y-2">
1469
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
1470
+ What This Does
1471
+ </h3>
1472
+ <p class="text-sm text-foreground">
1473
+ Use helper classes to make a CMS nav block interactive: hamburger toggle, right slide-out menu, close actions, and contained preview behavior.
1474
+ </p>
1475
+ <p class="text-sm text-foreground">
1476
+ The runtime in <code>htmlContent.vue</code> auto-wires these helpers and marks them as interactive so they do not open the block editor when clicked.
1477
+ </p>
1478
+ </section>
1479
+
1480
+ <section class="space-y-2">
1481
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
1482
+ Helper Class Contract
1483
+ </h3>
1484
+ <div class="text-sm text-foreground space-y-1">
1485
+ <div><code>cms-nav-root</code>: nav behavior root (required).</div>
1486
+ <div><code>cms-nav-toggle</code>: button that toggles open/closed (required).</div>
1487
+ <div><code>cms-nav-panel</code>: right slide-out panel (required).</div>
1488
+ <div><code>cms-nav-overlay</code>: backdrop click-to-close (optional but recommended).</div>
1489
+ <div><code>cms-nav-close</code>: explicit close button in panel (optional).</div>
1490
+ <div><code>cms-nav-link</code>: links that should close panel on click (optional).</div>
1491
+ </div>
1492
+ </section>
1493
+
1494
+ <section class="space-y-2">
1495
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
1496
+ Optional Root Attributes
1497
+ </h3>
1498
+ <div class="text-sm text-foreground space-y-1">
1499
+ <div><code>data-cms-nav-open="true"</code> to start open.</div>
1500
+ <div><code>data-cms-nav-open-class="your-class"</code> to change the root open class (default <code>is-open</code>).</div>
1501
+ <div><code>data-cms-nav-close-on-link="false"</code> to keep panel open after link clicks.</div>
1502
+ </div>
1503
+ </section>
1504
+
1505
+ <section class="space-y-3">
1506
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
1507
+ Nav Block Template (Copy / Paste)
1508
+ </h3>
1509
+ <pre v-pre class="rounded-md bg-muted p-3 text-xs overflow-auto"><code>&lt;div class="cms-nav-root" data-cms-nav-root data-cms-nav-close-on-link="true"&gt;
1510
+ &lt;nav class="fixed inset-x-0 top-0 z-30 w-full bg-transparent text-navText"&gt;
1511
+ {{{#array {"field":"siteDoc","as":"site","collection":{"path":"sites","query":[{"field":"docId","operator":"==","value":"{siteId}"}],"order":[]},"limit":1,"value":[]}}}}
1512
+ &lt;div class="relative w-full px-6 md:px-12"&gt;
1513
+ &lt;div class="flex h-[64px] md:h-[88px] items-center justify-between gap-6 py-6 md:py-8"&gt;
1514
+ &lt;a href="/" class="cursor-pointer text-xl text-navText"&gt;
1515
+ &lt;img src="{{site.logo}}" class="h-[56px] md:h-[72px] py-3" /&gt;
1516
+ &lt;/a&gt;
1517
+
1518
+ &lt;div class="ml-auto flex items-center gap-2"&gt;
1519
+ &lt;ul class="hidden lg:flex items-center space-x-[20px] pt-1 text-sm uppercase tracking-widest"&gt;
1520
+ {{{#subarray:navItem {"field":"item.menus.Site Root","value":[]}}}}
1521
+ &lt;li class="relative group"&gt;
1522
+ {{{#if {"cond":"navItem.item.type == external"}}}}
1523
+ &lt;a href="{{navItem.item.url}}" class="nav-item cursor-pointer"&gt;{{navItem.name}}&lt;/a&gt;
1524
+ {{{#else}}}
1525
+ {{{#if {"cond":"navItem.name == home"}}}}
1526
+ &lt;a href="/" class="nav-item cursor-pointer"&gt;{{navItem.name}}&lt;/a&gt;
1527
+ {{{#else}}}
1528
+ &lt;a href="/{{navItem.name}}" class="nav-item cursor-pointer"&gt;{{navItem.name}}&lt;/a&gt;
1529
+ {{{/if}}}
1530
+ {{{/if}}}
1531
+ &lt;/li&gt;
1532
+ {{{/subarray}}}
1533
+ &lt;/ul&gt;
1534
+
1535
+ &lt;button class="cms-nav-toggle flex h-12 w-12 items-center justify-center rounded-full text-navText" type="button" aria-label="Open Menu"&gt;
1536
+ &lt;svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg"&gt;
1537
+ &lt;path d="M4 6h16M4 12h16M4 18h16" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /&gt;
1538
+ &lt;/svg&gt;
1539
+ &lt;/button&gt;
1540
+ &lt;/div&gt;
1541
+ &lt;/div&gt;
1542
+ &lt;/div&gt;
1543
+ {{{/array}}}
1544
+ &lt;/nav&gt;
1545
+
1546
+ &lt;div class="cms-nav-overlay fixed inset-0 z-[110] bg-black/50 transition-opacity duration-300 opacity-0 pointer-events-none"&gt;&lt;/div&gt;
1547
+
1548
+ &lt;aside class="cms-nav-panel fixed inset-y-0 right-0 z-[120] w-full max-w-md bg-sideNavBg text-sideNavText transition-all duration-300 translate-x-full opacity-0 pointer-events-none"&gt;
1549
+ &lt;div class="relative h-full overflow-y-auto px-8 py-10"&gt;
1550
+ &lt;button type="button" class="cms-nav-close absolute right-6 top-6 text-4xl text-sideNavText"&gt;&amp;times;&lt;/button&gt;
1551
+
1552
+ &lt;ul class="mt-14 space-y-4 uppercase"&gt;
1553
+ {{{#array {"field":"siteDoc","as":"site","collection":{"path":"sites","query":[{"field":"docId","operator":"==","value":"{siteId}"}],"order":[]},"limit":1,"value":[]}}}}
1554
+ {{{#subarray:navItem {"field":"item.menus.Site Root","value":[]}}}}
1555
+ &lt;li&gt;
1556
+ {{{#if {"cond":"navItem.item.type == external"}}}}
1557
+ &lt;a href="{{navItem.item.url}}" class="cms-nav-link block text-sideNavText"&gt;{{navItem.name}}&lt;/a&gt;
1558
+ {{{#else}}}
1559
+ {{{#if {"cond":"navItem.name == home"}}}}
1560
+ &lt;a href="/" class="cms-nav-link block text-sideNavText"&gt;{{navItem.name}}&lt;/a&gt;
1561
+ {{{#else}}}
1562
+ &lt;a href="/{{navItem.name}}" class="cms-nav-link block text-sideNavText"&gt;{{navItem.name}}&lt;/a&gt;
1563
+ {{{/if}}}
1564
+ {{{/if}}}
1565
+ &lt;/li&gt;
1566
+ {{{/subarray}}}
1567
+ {{{/array}}}
1568
+ &lt;/ul&gt;
1569
+ &lt;/div&gt;
1570
+ &lt;/aside&gt;
1571
+ &lt;/div&gt;</code></pre>
1572
+ </section>
1573
+
1574
+ <section class="space-y-2">
1575
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
1576
+ Preview + Edit Behavior
1577
+ </h3>
1578
+ <div class="text-sm text-foreground space-y-1">
1579
+ <div>Clicking the nav button opens the slide-out in Block Editor preview and Page Preview mode.</div>
1580
+ <div>Interactive nav elements do not trigger “Edit Block”. Clicking outside them still opens the editor in edit mode.</div>
1581
+ <div>In CMS preview, fixed nav and panel are contained to the preview surface by the block wrapper.</div>
1582
+ </div>
1583
+ </section>
1584
+ </div>
1585
+ </div>
1586
+ </TabsContent>
1587
+
1299
1588
  <TabsContent value="scroll-reveals">
1300
1589
  <div class="h-[calc(100vh-190px)] overflow-y-auto pr-1 pb-6">
1301
1590
  <div class="space-y-6">