@edgedev/create-edge-app 1.2.33 → 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.
- package/deploy.sh +77 -0
- package/edge/components/cms/block.vue +228 -18
- package/edge/components/cms/blockApi.vue +3 -3
- package/edge/components/cms/blockEditor.vue +374 -85
- package/edge/components/cms/blockPicker.vue +29 -3
- package/edge/components/cms/blockRender.vue +3 -3
- package/edge/components/cms/blocksManager.vue +755 -82
- package/edge/components/cms/codeEditor.vue +15 -6
- package/edge/components/cms/fontUpload.vue +318 -2
- package/edge/components/cms/htmlContent.vue +230 -89
- package/edge/components/cms/menu.vue +5 -4
- package/edge/components/cms/page.vue +750 -21
- package/edge/components/cms/site.vue +624 -84
- package/edge/components/cms/sitesManager.vue +5 -4
- package/edge/components/cms/themeEditor.vue +196 -162
- package/edge/components/editor.vue +5 -1
- package/edge/composables/global.ts +37 -5
- package/edge/composables/useCmsNewDocs.js +100 -0
- package/edge/composables/useEdgeCmsDialogPositionFix.js +19 -0
- package/edge/routes/cms/dashboard/blocks/[block].vue +5 -0
- package/edge/routes/cms/dashboard/blocks/index.vue +12 -1
- package/edge/routes/cms/dashboard/media/index.vue +5 -0
- package/edge/routes/cms/dashboard/sites/[site]/[[page]].vue +4 -0
- package/edge/routes/cms/dashboard/sites/[site].vue +4 -0
- package/edge/routes/cms/dashboard/sites/index.vue +4 -0
- package/edge/routes/cms/dashboard/templates/[page].vue +4 -0
- package/edge/routes/cms/dashboard/templates/index.vue +4 -0
- package/edge/routes/cms/dashboard/themes/[theme].vue +5 -0
- package/edge/routes/cms/dashboard/themes/index.vue +330 -1
- package/firebase.json +4 -0
- package/nuxt.config.ts +1 -1
- package/package.json +2 -2
|
@@ -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: '/
|
|
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
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
468
|
-
return
|
|
502
|
+
catch {
|
|
503
|
+
return null
|
|
469
504
|
}
|
|
470
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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="
|
|
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-
|
|
593
|
-
<div class="
|
|
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="
|
|
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
|
-
<
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
<
|
|
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-
|
|
688
|
-
@click="insertBlockContentSnippet(snippet.snippet)"
|
|
840
|
+
class="h-8 px-2 text-[11px] uppercase tracking-wide"
|
|
689
841
|
>
|
|
690
|
-
|
|
842
|
+
Dynamic Content
|
|
691
843
|
</edge-shad-button>
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
|
889
|
+
class="w-full mx-auto rounded-none overflow-visible"
|
|
732
890
|
:style="previewViewportStyle"
|
|
733
891
|
>
|
|
734
|
-
<
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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-
|
|
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><div class="cms-nav-root" data-cms-nav-root data-cms-nav-close-on-link="true">
|
|
1510
|
+
<nav class="fixed inset-x-0 top-0 z-30 w-full bg-transparent text-navText">
|
|
1511
|
+
{{{#array {"field":"siteDoc","as":"site","collection":{"path":"sites","query":[{"field":"docId","operator":"==","value":"{siteId}"}],"order":[]},"limit":1,"value":[]}}}}
|
|
1512
|
+
<div class="relative w-full px-6 md:px-12">
|
|
1513
|
+
<div class="flex h-[64px] md:h-[88px] items-center justify-between gap-6 py-6 md:py-8">
|
|
1514
|
+
<a href="/" class="cursor-pointer text-xl text-navText">
|
|
1515
|
+
<img src="{{site.logo}}" class="h-[56px] md:h-[72px] py-3" />
|
|
1516
|
+
</a>
|
|
1517
|
+
|
|
1518
|
+
<div class="ml-auto flex items-center gap-2">
|
|
1519
|
+
<ul class="hidden lg:flex items-center space-x-[20px] pt-1 text-sm uppercase tracking-widest">
|
|
1520
|
+
{{{#subarray:navItem {"field":"item.menus.Site Root","value":[]}}}}
|
|
1521
|
+
<li class="relative group">
|
|
1522
|
+
{{{#if {"cond":"navItem.item.type == external"}}}}
|
|
1523
|
+
<a href="{{navItem.item.url}}" class="nav-item cursor-pointer">{{navItem.name}}</a>
|
|
1524
|
+
{{{#else}}}
|
|
1525
|
+
{{{#if {"cond":"navItem.name == home"}}}}
|
|
1526
|
+
<a href="/" class="nav-item cursor-pointer">{{navItem.name}}</a>
|
|
1527
|
+
{{{#else}}}
|
|
1528
|
+
<a href="/{{navItem.name}}" class="nav-item cursor-pointer">{{navItem.name}}</a>
|
|
1529
|
+
{{{/if}}}
|
|
1530
|
+
{{{/if}}}
|
|
1531
|
+
</li>
|
|
1532
|
+
{{{/subarray}}}
|
|
1533
|
+
</ul>
|
|
1534
|
+
|
|
1535
|
+
<button class="cms-nav-toggle flex h-12 w-12 items-center justify-center rounded-full text-navText" type="button" aria-label="Open Menu">
|
|
1536
|
+
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
1537
|
+
<path d="M4 6h16M4 12h16M4 18h16" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
1538
|
+
</svg>
|
|
1539
|
+
</button>
|
|
1540
|
+
</div>
|
|
1541
|
+
</div>
|
|
1542
|
+
</div>
|
|
1543
|
+
{{{/array}}}
|
|
1544
|
+
</nav>
|
|
1545
|
+
|
|
1546
|
+
<div class="cms-nav-overlay fixed inset-0 z-[110] bg-black/50 transition-opacity duration-300 opacity-0 pointer-events-none"></div>
|
|
1547
|
+
|
|
1548
|
+
<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">
|
|
1549
|
+
<div class="relative h-full overflow-y-auto px-8 py-10">
|
|
1550
|
+
<button type="button" class="cms-nav-close absolute right-6 top-6 text-4xl text-sideNavText">&times;</button>
|
|
1551
|
+
|
|
1552
|
+
<ul class="mt-14 space-y-4 uppercase">
|
|
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
|
+
<li>
|
|
1556
|
+
{{{#if {"cond":"navItem.item.type == external"}}}}
|
|
1557
|
+
<a href="{{navItem.item.url}}" class="cms-nav-link block text-sideNavText">{{navItem.name}}</a>
|
|
1558
|
+
{{{#else}}}
|
|
1559
|
+
{{{#if {"cond":"navItem.name == home"}}}}
|
|
1560
|
+
<a href="/" class="cms-nav-link block text-sideNavText">{{navItem.name}}</a>
|
|
1561
|
+
{{{#else}}}
|
|
1562
|
+
<a href="/{{navItem.name}}" class="cms-nav-link block text-sideNavText">{{navItem.name}}</a>
|
|
1563
|
+
{{{/if}}}
|
|
1564
|
+
{{{/if}}}
|
|
1565
|
+
</li>
|
|
1566
|
+
{{{/subarray}}}
|
|
1567
|
+
{{{/array}}}
|
|
1568
|
+
</ul>
|
|
1569
|
+
</div>
|
|
1570
|
+
</aside>
|
|
1571
|
+
</div></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">
|