@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.
- package/README.md +1 -0
- package/agents.md +95 -2
- package/deploy.sh +136 -0
- package/edge/components/cms/block.vue +977 -305
- package/edge/components/cms/blockApi.vue +3 -3
- package/edge/components/cms/blockEditor.vue +688 -86
- package/edge/components/cms/blockPicker.vue +31 -5
- package/edge/components/cms/blockRender.vue +3 -3
- package/edge/components/cms/blocksManager.vue +790 -82
- package/edge/components/cms/codeEditor.vue +15 -6
- package/edge/components/cms/fontUpload.vue +318 -2
- package/edge/components/cms/htmlContent.vue +825 -93
- package/edge/components/cms/init_blocks/contact_us.html +55 -47
- package/edge/components/cms/init_blocks/newsletter.html +56 -96
- package/edge/components/cms/menu.vue +96 -34
- package/edge/components/cms/page.vue +902 -58
- package/edge/components/cms/posts.vue +13 -4
- package/edge/components/cms/site.vue +638 -87
- package/edge/components/cms/siteSettingsForm.vue +19 -9
- package/edge/components/cms/sitesManager.vue +5 -4
- package/edge/components/cms/themeDefaultMenu.vue +20 -2
- 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/siteSettingsTemplate.js +2 -0
- 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/edge-pull.sh +16 -2
- package/edge-push.sh +9 -1
- package/edge-remote.sh +20 -0
- package/edge-status.sh +9 -5
- package/edge-update-all.sh +127 -0
- 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)
|
|
@@ -105,6 +120,11 @@ const BLOCK_CONTENT_SNIPPETS = [
|
|
|
105
120
|
snippet: '{{{#text {"field": "fieldName", "value": "" }}}}',
|
|
106
121
|
description: 'Simple text field placeholder',
|
|
107
122
|
},
|
|
123
|
+
{
|
|
124
|
+
label: 'Text with Options',
|
|
125
|
+
snippet: '{{{#text {"field":"fieldName","title":"Field Label","option":{"field":"fieldName","options":[{"title":"Option 1","name":"option1"},{"title":"Option 2","name":"option2"}],"optionsKey":"title","optionsValue":"name"},"value":"option1"}}}}',
|
|
126
|
+
description: 'Text field with selectable options',
|
|
127
|
+
},
|
|
108
128
|
{
|
|
109
129
|
label: 'Text Area',
|
|
110
130
|
snippet: '{{{#textarea {"field": "fieldName", "value": "" }}}}',
|
|
@@ -172,6 +192,14 @@ function insertBlockContentSnippet(snippet) {
|
|
|
172
192
|
editor.insertSnippet(snippet)
|
|
173
193
|
}
|
|
174
194
|
|
|
195
|
+
const updateWorkingPreviewType = (nextValue) => {
|
|
196
|
+
const normalized = normalizePreviewType(nextValue)
|
|
197
|
+
if (state.editorWorkingDoc)
|
|
198
|
+
state.editorWorkingDoc.previewType = normalized
|
|
199
|
+
if (state.previewBlock)
|
|
200
|
+
state.previewBlock.previewType = normalized
|
|
201
|
+
}
|
|
202
|
+
|
|
175
203
|
function normalizeConfigLiteral(str) {
|
|
176
204
|
// ensure keys are quoted: { title: "x", field: "y" } -> { "title": "x", "field": "y" }
|
|
177
205
|
return str
|
|
@@ -451,6 +479,7 @@ const buildPreviewBlock = (workingDoc, parsed) => {
|
|
|
451
479
|
id: state.previewBlock?.id || 'preview',
|
|
452
480
|
blockId: props.blockId,
|
|
453
481
|
name: workingDoc?.name || state.previewBlock?.name || '',
|
|
482
|
+
previewType: normalizePreviewType(workingDoc?.previewType),
|
|
454
483
|
content,
|
|
455
484
|
values: nextValues,
|
|
456
485
|
meta: nextMeta,
|
|
@@ -459,15 +488,32 @@ const buildPreviewBlock = (workingDoc, parsed) => {
|
|
|
459
488
|
}
|
|
460
489
|
|
|
461
490
|
const theme = computed(() => {
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
491
|
+
const selectedThemeId = String(edgeGlobal.edgeState.blockEditorTheme || '').trim()
|
|
492
|
+
if (!selectedThemeId)
|
|
493
|
+
return null
|
|
494
|
+
const themeDoc = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[selectedThemeId] || null
|
|
495
|
+
const themeContents = themeDoc?.theme || null
|
|
496
|
+
if (!themeContents)
|
|
497
|
+
return null
|
|
498
|
+
const extraCSS = typeof themeDoc?.extraCSS === 'string' ? themeDoc.extraCSS : ''
|
|
499
|
+
if (typeof themeContents === 'object' && !Array.isArray(themeContents))
|
|
500
|
+
return { ...themeContents, extraCSS }
|
|
501
|
+
try {
|
|
502
|
+
const parsedTheme = JSON.parse(themeContents)
|
|
503
|
+
if (!parsedTheme || typeof parsedTheme !== 'object' || Array.isArray(parsedTheme))
|
|
504
|
+
return null
|
|
505
|
+
return { ...parsedTheme, extraCSS }
|
|
466
506
|
}
|
|
467
|
-
|
|
468
|
-
return
|
|
507
|
+
catch {
|
|
508
|
+
return null
|
|
469
509
|
}
|
|
470
|
-
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
const previewThemeRenderKey = computed(() => {
|
|
513
|
+
const themeId = String(edgeGlobal.edgeState.blockEditorTheme || 'no-theme')
|
|
514
|
+
const siteId = String(edgeGlobal.edgeState.blockEditorSite || 'no-site')
|
|
515
|
+
const previewType = normalizePreviewType(state.previewBlock?.previewType)
|
|
516
|
+
return `${themeId}:${siteId}:${state.previewViewport}:${previewType}`
|
|
471
517
|
})
|
|
472
518
|
|
|
473
519
|
const headObject = computed(() => {
|
|
@@ -485,6 +531,7 @@ watch(headObject, (newHeadElements) => {
|
|
|
485
531
|
}, { immediate: true, deep: true })
|
|
486
532
|
|
|
487
533
|
const editorDocUpdates = (workingDoc) => {
|
|
534
|
+
state.editorWorkingDoc = workingDoc || null
|
|
488
535
|
const parsed = blockModel(workingDoc.content)
|
|
489
536
|
state.workingDoc = parsed
|
|
490
537
|
state.previewBlock = buildPreviewBlock(workingDoc, parsed)
|
|
@@ -510,11 +557,51 @@ const themes = computed(() => {
|
|
|
510
557
|
return Object.values(edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`] || {})
|
|
511
558
|
})
|
|
512
559
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
560
|
+
const availableThemeIds = computed(() => {
|
|
561
|
+
return themes.value
|
|
562
|
+
.map(themeDoc => String(themeDoc?.docId || '').trim())
|
|
563
|
+
.filter(Boolean)
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
const currentBlockAllowedThemeIds = computed(() => {
|
|
567
|
+
const currentBlockDoc = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/blocks`]?.[props.blockId]
|
|
568
|
+
if (!Array.isArray(currentBlockDoc?.themes))
|
|
569
|
+
return []
|
|
570
|
+
return currentBlockDoc.themes.map(themeId => String(themeId || '').trim()).filter(Boolean)
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
const preferredThemeDefaultForBlock = computed(() => {
|
|
574
|
+
const firstAllowedAvailable = currentBlockAllowedThemeIds.value.find(themeId => availableThemeIds.value.includes(themeId))
|
|
575
|
+
if (firstAllowedAvailable)
|
|
576
|
+
return firstAllowedAvailable
|
|
577
|
+
return availableThemeIds.value[0] || ''
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
const applyThemeDefaultForBlock = () => {
|
|
581
|
+
const blockId = String(props.blockId || '').trim()
|
|
582
|
+
if (!blockId)
|
|
583
|
+
return
|
|
584
|
+
if (state.themeDefaultAppliedForBlockId === blockId)
|
|
585
|
+
return
|
|
586
|
+
|
|
587
|
+
const preferredThemeId = preferredThemeDefaultForBlock.value
|
|
588
|
+
if (!preferredThemeId) {
|
|
589
|
+
if (!availableThemeIds.value.length)
|
|
590
|
+
edgeGlobal.edgeState.blockEditorTheme = ''
|
|
591
|
+
return
|
|
517
592
|
}
|
|
593
|
+
|
|
594
|
+
edgeGlobal.edgeState.blockEditorTheme = preferredThemeId
|
|
595
|
+
state.themeDefaultAppliedForBlockId = blockId
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
watch(() => props.blockId, () => {
|
|
599
|
+
state.themeDefaultAppliedForBlockId = ''
|
|
600
|
+
}, { immediate: true })
|
|
601
|
+
|
|
602
|
+
watch([availableThemeIds, currentBlockAllowedThemeIds, () => props.blockId], async () => {
|
|
603
|
+
state.loading = true
|
|
604
|
+
applyThemeDefaultForBlock()
|
|
518
605
|
await nextTick()
|
|
519
606
|
state.loading = false
|
|
520
607
|
}, { immediate: true, deep: true })
|
|
@@ -567,6 +654,59 @@ const getTagsFromBlocks = computed(() => {
|
|
|
567
654
|
// Always prepend it
|
|
568
655
|
return [{ name: 'Quick Picks', title: 'Quick Picks' }, ...filtered]
|
|
569
656
|
})
|
|
657
|
+
|
|
658
|
+
const downloadJsonFile = (payload, filename) => {
|
|
659
|
+
if (typeof window === 'undefined')
|
|
660
|
+
return
|
|
661
|
+
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
|
|
662
|
+
const objectUrl = URL.createObjectURL(blob)
|
|
663
|
+
const anchor = document.createElement('a')
|
|
664
|
+
anchor.href = objectUrl
|
|
665
|
+
anchor.download = filename
|
|
666
|
+
document.body.appendChild(anchor)
|
|
667
|
+
anchor.click()
|
|
668
|
+
anchor.remove()
|
|
669
|
+
URL.revokeObjectURL(objectUrl)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const isPlainObject = value => !!value && typeof value === 'object' && !Array.isArray(value)
|
|
673
|
+
|
|
674
|
+
const cloneSchemaValue = (value) => {
|
|
675
|
+
if (isPlainObject(value) || Array.isArray(value))
|
|
676
|
+
return edgeGlobal.dupObject(value)
|
|
677
|
+
return value
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const getDocDefaultsFromSchema = (schema = {}) => {
|
|
681
|
+
const defaults = {}
|
|
682
|
+
for (const [key, schemaEntry] of Object.entries(schema || {})) {
|
|
683
|
+
const hasValueProp = isPlainObject(schemaEntry) && Object.prototype.hasOwnProperty.call(schemaEntry, 'value')
|
|
684
|
+
const baseValue = hasValueProp ? schemaEntry.value : schemaEntry
|
|
685
|
+
defaults[key] = cloneSchemaValue(baseValue)
|
|
686
|
+
}
|
|
687
|
+
return defaults
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const getBlockDocDefaults = () => getDocDefaultsFromSchema(blockNewDocSchema.value || {})
|
|
691
|
+
|
|
692
|
+
const notifySuccess = (message) => {
|
|
693
|
+
edgeFirebase?.toast?.success?.(message)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const notifyError = (message) => {
|
|
697
|
+
edgeFirebase?.toast?.error?.(message)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const exportCurrentBlock = () => {
|
|
701
|
+
const doc = blocks.value?.[props.blockId]
|
|
702
|
+
if (!doc || !doc.docId) {
|
|
703
|
+
notifyError('Save this block before exporting.')
|
|
704
|
+
return
|
|
705
|
+
}
|
|
706
|
+
const exportPayload = { ...getBlockDocDefaults(), ...doc }
|
|
707
|
+
downloadJsonFile(exportPayload, `block-${doc.docId}.json`)
|
|
708
|
+
notifySuccess(`Exported block "${doc.docId}".`)
|
|
709
|
+
}
|
|
570
710
|
</script>
|
|
571
711
|
|
|
572
712
|
<template>
|
|
@@ -578,7 +718,9 @@ const getTagsFromBlocks = computed(() => {
|
|
|
578
718
|
:doc-id="props.blockId"
|
|
579
719
|
:schema="blockSchema"
|
|
580
720
|
:new-doc-schema="state.newDocs.blocks"
|
|
581
|
-
class="
|
|
721
|
+
header-class="py-2 bg-secondary text-foreground rounded-none sticky top-0 border"
|
|
722
|
+
class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none pt-0 px-0"
|
|
723
|
+
card-content-class="px-0"
|
|
582
724
|
:show-footer="false"
|
|
583
725
|
:no-close-after-save="true"
|
|
584
726
|
:working-doc-overrides="state.workingDoc"
|
|
@@ -589,29 +731,52 @@ const getTagsFromBlocks = computed(() => {
|
|
|
589
731
|
{{ slotProps.title }}
|
|
590
732
|
</template>
|
|
591
733
|
<template #header-center>
|
|
592
|
-
<div class="w-full flex gap-
|
|
593
|
-
<div class="
|
|
734
|
+
<div class="w-full flex gap-2 px-4 items-center">
|
|
735
|
+
<div class="flex-1">
|
|
594
736
|
<edge-shad-select
|
|
595
737
|
v-if="!state.loading"
|
|
596
738
|
v-model="edgeGlobal.edgeState.blockEditorTheme"
|
|
597
|
-
label="Theme Viewer Select"
|
|
598
739
|
name="theme"
|
|
599
740
|
:items="themes.map(t => ({ title: t.name, name: t.docId }))"
|
|
600
741
|
placeholder="Theme Viewer Select"
|
|
601
742
|
class="w-full"
|
|
602
743
|
/>
|
|
603
744
|
</div>
|
|
604
|
-
<div class="
|
|
745
|
+
<div class="flex-1">
|
|
605
746
|
<edge-shad-select
|
|
606
747
|
v-if="!state.loading"
|
|
607
748
|
v-model="edgeGlobal.edgeState.blockEditorSite"
|
|
608
|
-
label="Site"
|
|
609
749
|
name="site"
|
|
610
750
|
:items="sites.map(s => ({ title: s.name, name: s.docId }))"
|
|
611
751
|
placeholder="Select Site"
|
|
612
752
|
class="w-full"
|
|
613
753
|
/>
|
|
614
754
|
</div>
|
|
755
|
+
<div class="flex-1">
|
|
756
|
+
<edge-shad-select
|
|
757
|
+
v-if="!state.loading"
|
|
758
|
+
:model-value="state.editorWorkingDoc?.previewType || 'light'"
|
|
759
|
+
name="previewType"
|
|
760
|
+
:items="previewTypeOptions"
|
|
761
|
+
placeholder="Preview Surface"
|
|
762
|
+
class="w-full"
|
|
763
|
+
@update:model-value="updateWorkingPreviewType($event)"
|
|
764
|
+
/>
|
|
765
|
+
</div>
|
|
766
|
+
<div class="flex items-center gap-2">
|
|
767
|
+
<edge-shad-button
|
|
768
|
+
type="button"
|
|
769
|
+
size="icon"
|
|
770
|
+
variant="outline"
|
|
771
|
+
class="h-9 w-9"
|
|
772
|
+
:disabled="props.blockId === 'new' || !blocks?.[props.blockId]"
|
|
773
|
+
title="Export Block"
|
|
774
|
+
aria-label="Export Block"
|
|
775
|
+
@click="exportCurrentBlock"
|
|
776
|
+
>
|
|
777
|
+
<Download class="h-4 w-4" />
|
|
778
|
+
</edge-shad-button>
|
|
779
|
+
</div>
|
|
615
780
|
</div>
|
|
616
781
|
</template>
|
|
617
782
|
<template #main="slotProps">
|
|
@@ -659,55 +824,53 @@ const getTagsFromBlocks = computed(() => {
|
|
|
659
824
|
</div>
|
|
660
825
|
<div class="flex gap-4">
|
|
661
826
|
<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
|
-
>
|
|
827
|
+
<edge-cms-code-editor
|
|
828
|
+
ref="contentEditorRef"
|
|
829
|
+
v-model="slotProps.workingDoc.content"
|
|
830
|
+
title="Block Content"
|
|
831
|
+
language="handlebars"
|
|
832
|
+
name="content"
|
|
833
|
+
:enable-formatting="false"
|
|
834
|
+
height="calc(100vh - 300px)"
|
|
835
|
+
class="mb-4 flex-1"
|
|
836
|
+
@line-click="payload => handleEditorLineClick(payload, slotProps.workingDoc)"
|
|
837
|
+
>
|
|
838
|
+
<template #end-actions>
|
|
839
|
+
<DropdownMenu>
|
|
840
|
+
<DropdownMenuTrigger as-child>
|
|
684
841
|
<edge-shad-button
|
|
842
|
+
type="button"
|
|
685
843
|
size="sm"
|
|
686
844
|
variant="outline"
|
|
687
|
-
class="text-
|
|
688
|
-
@click="insertBlockContentSnippet(snippet.snippet)"
|
|
845
|
+
class="h-8 px-2 text-[11px] uppercase tracking-wide"
|
|
689
846
|
>
|
|
690
|
-
|
|
847
|
+
Dynamic Content
|
|
691
848
|
</edge-shad-button>
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
849
|
+
</DropdownMenuTrigger>
|
|
850
|
+
<DropdownMenuContent align="end" class="w-72">
|
|
851
|
+
<DropdownMenuItem
|
|
852
|
+
v-for="snippet in BLOCK_CONTENT_SNIPPETS"
|
|
853
|
+
:key="snippet.label"
|
|
854
|
+
class="cursor-pointer flex-col items-start gap-0.5"
|
|
855
|
+
@click="insertBlockContentSnippet(snippet.snippet)"
|
|
856
|
+
>
|
|
857
|
+
<span class="text-sm font-medium">{{ snippet.label }}</span>
|
|
858
|
+
<span class="text-xs text-muted-foreground whitespace-normal">{{ snippet.description }}</span>
|
|
859
|
+
</DropdownMenuItem>
|
|
860
|
+
</DropdownMenuContent>
|
|
861
|
+
</DropdownMenu>
|
|
862
|
+
<edge-shad-button
|
|
863
|
+
type="button"
|
|
864
|
+
size="sm"
|
|
865
|
+
variant="secondary"
|
|
866
|
+
class="h-8 px-2 text-[11px] uppercase tracking-wide gap-2"
|
|
867
|
+
@click="state.helpOpen = true"
|
|
868
|
+
>
|
|
869
|
+
<HelpCircle class="w-4 h-4" />
|
|
870
|
+
Block Help
|
|
871
|
+
</edge-shad-button>
|
|
872
|
+
</template>
|
|
873
|
+
</edge-cms-code-editor>
|
|
711
874
|
</div>
|
|
712
875
|
<div class="w-1/2 space-y-2">
|
|
713
876
|
<div class="flex items-center justify-between">
|
|
@@ -728,19 +891,25 @@ const getTagsFromBlocks = computed(() => {
|
|
|
728
891
|
</div>
|
|
729
892
|
</div>
|
|
730
893
|
<div
|
|
731
|
-
class="w-full mx-auto
|
|
894
|
+
class="w-full mx-auto rounded-none overflow-visible"
|
|
732
895
|
:style="previewViewportStyle"
|
|
733
896
|
>
|
|
734
|
-
<
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
897
|
+
<div class="relative overflow-visible rounded-none" :class="[previewSurfaceClass, previewCanvasClass]" style="transform: translateZ(0);">
|
|
898
|
+
<edge-cms-block
|
|
899
|
+
v-if="state.previewBlock"
|
|
900
|
+
:key="previewThemeRenderKey"
|
|
901
|
+
v-model="state.previewBlock"
|
|
902
|
+
:site-id="edgeGlobal.edgeState.blockEditorSite"
|
|
903
|
+
:theme="theme"
|
|
904
|
+
:edit-mode="true"
|
|
905
|
+
:contain-fixed="true"
|
|
906
|
+
:disable-interactive-preview-in-edit="false"
|
|
907
|
+
:allow-delete="false"
|
|
908
|
+
:viewport-mode="previewViewportMode"
|
|
909
|
+
:block-id="state.previewBlock.id"
|
|
910
|
+
@delete="ignorePreviewDelete"
|
|
911
|
+
/>
|
|
912
|
+
</div>
|
|
744
913
|
</div>
|
|
745
914
|
</div>
|
|
746
915
|
</div>
|
|
@@ -759,13 +928,19 @@ const getTagsFromBlocks = computed(() => {
|
|
|
759
928
|
</SheetHeader>
|
|
760
929
|
<div class="px-6 pb-6">
|
|
761
930
|
<Tabs class="w-full" default-value="guide">
|
|
762
|
-
<TabsList class="w-full mt-3 bg-secondary rounded-sm grid grid-cols-
|
|
931
|
+
<TabsList class="w-full mt-3 bg-secondary rounded-sm grid grid-cols-5">
|
|
763
932
|
<TabsTrigger value="guide" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
764
933
|
Block Guide
|
|
765
934
|
</TabsTrigger>
|
|
766
935
|
<TabsTrigger value="carousel" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
767
936
|
Carousel Usage
|
|
768
937
|
</TabsTrigger>
|
|
938
|
+
<TabsTrigger value="form-helpers" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
939
|
+
Form Helpers
|
|
940
|
+
</TabsTrigger>
|
|
941
|
+
<TabsTrigger value="nav-bar" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
942
|
+
Nav Bar
|
|
943
|
+
</TabsTrigger>
|
|
769
944
|
<TabsTrigger value="scroll-reveals" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
|
|
770
945
|
Scroll Reveals
|
|
771
946
|
</TabsTrigger>
|
|
@@ -793,6 +968,7 @@ const getTagsFromBlocks = computed(() => {
|
|
|
793
968
|
<a href="#arrays-filters" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Filters</a>
|
|
794
969
|
<a href="#conditionals" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Conditionals</a>
|
|
795
970
|
<a href="#subarrays" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Subarrays</a>
|
|
971
|
+
<a href="#entries" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Entries</a>
|
|
796
972
|
<a href="#rendering-rules" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Rendering</a>
|
|
797
973
|
<a href="#loading-tokens" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Loading</a>
|
|
798
974
|
<a href="#validation" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Validation</a>
|
|
@@ -1077,6 +1253,29 @@ const getTagsFromBlocks = computed(() => {
|
|
|
1077
1253
|
</p>
|
|
1078
1254
|
</section>
|
|
1079
1255
|
|
|
1256
|
+
<section id="entries" class="space-y-3">
|
|
1257
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1258
|
+
Entries (Object Key/Value Loops)
|
|
1259
|
+
</h3>
|
|
1260
|
+
<pre v-pre class="rounded-md bg-muted p-3 text-xs overflow-auto"><code>{{{#entries:pair {"field":"settings","value":{"theme":"dark","ctaText":"Contact Us"}}}}}
|
|
1261
|
+
<div><strong>{{pair.key}}</strong>: {{pair.value}}</div>
|
|
1262
|
+
{{{/entries}}}
|
|
1263
|
+
|
|
1264
|
+
{{{#entries:group {"field":"groupedItems","value":{"featured":["One","Two"],"archive":["Three"]}}}}}
|
|
1265
|
+
<h4>{{group.key}}</h4>
|
|
1266
|
+
{{{#subarray:child {"field":"item.value","value":[]}}}}
|
|
1267
|
+
<div>{{child}}</div>
|
|
1268
|
+
{{{/subarray}}}
|
|
1269
|
+
{{{/entries}}}</code></pre>
|
|
1270
|
+
<div class="text-sm text-foreground space-y-1">
|
|
1271
|
+
<div><code>entries</code> loops object fields instead of arrays.</div>
|
|
1272
|
+
<div>Use it at the root or inside other loops; it does not need to be inside <code>subarray</code>.</div>
|
|
1273
|
+
<div>Each iteration exposes <code>item.key</code> and <code>item.value</code>, plus alias access like <code v-pre>{{pair.key}}</code>.</div>
|
|
1274
|
+
<div>If a value is an array, use nested <code>subarray</code> on <code>item.value</code>.</div>
|
|
1275
|
+
<div>If <code>field</code> is not an object, it renders nothing.</div>
|
|
1276
|
+
</div>
|
|
1277
|
+
</section>
|
|
1278
|
+
|
|
1080
1279
|
<section id="rendering-rules" class="space-y-2">
|
|
1081
1280
|
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1082
1281
|
Rendering Rules
|
|
@@ -1206,7 +1405,6 @@ const getTagsFromBlocks = computed(() => {
|
|
|
1206
1405
|
</h3>
|
|
1207
1406
|
<p class="text-sm text-foreground">
|
|
1208
1407
|
Add <code>data-carousel</code> markup to any CMS block and the runtime auto-initializes Embla on the client.
|
|
1209
|
-
This is initialized in <code>htmlContent.vue</code> and works inside raw block HTML.
|
|
1210
1408
|
</p>
|
|
1211
1409
|
</section>
|
|
1212
1410
|
|
|
@@ -1296,6 +1494,410 @@ const getTagsFromBlocks = computed(() => {
|
|
|
1296
1494
|
</div>
|
|
1297
1495
|
</TabsContent>
|
|
1298
1496
|
|
|
1497
|
+
<TabsContent value="nav-bar">
|
|
1498
|
+
<div class="h-[calc(100vh-190px)] overflow-y-auto pr-1 pb-6">
|
|
1499
|
+
<div class="space-y-6">
|
|
1500
|
+
<section class="space-y-2">
|
|
1501
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1502
|
+
What This Does
|
|
1503
|
+
</h3>
|
|
1504
|
+
<p class="text-sm text-foreground">
|
|
1505
|
+
Use helper classes to make a CMS nav block interactive: hamburger toggle, right slide-out menu, close actions, and contained preview behavior.
|
|
1506
|
+
</p>
|
|
1507
|
+
<p class="text-sm text-foreground">
|
|
1508
|
+
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.
|
|
1509
|
+
</p>
|
|
1510
|
+
</section>
|
|
1511
|
+
|
|
1512
|
+
<section class="space-y-2">
|
|
1513
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1514
|
+
Helper Class Contract
|
|
1515
|
+
</h3>
|
|
1516
|
+
<div class="text-sm text-foreground space-y-1">
|
|
1517
|
+
<div><code>cms-nav-root</code>: nav behavior root (required).</div>
|
|
1518
|
+
<div><code>cms-nav-toggle</code>: button that toggles open/closed (required).</div>
|
|
1519
|
+
<div><code>cms-nav-panel</code>: right slide-out panel (required).</div>
|
|
1520
|
+
<div><code>cms-nav-overlay</code>: backdrop click-to-close (optional but recommended).</div>
|
|
1521
|
+
<div><code>cms-nav-close</code>: explicit close button in panel (optional).</div>
|
|
1522
|
+
<div><code>cms-nav-link</code>: links that should close panel on click (optional).</div>
|
|
1523
|
+
<div><code>cms-nav-folder</code>: desktop folder wrapper for dropdown behavior (recommended).</div>
|
|
1524
|
+
<div><code>cms-nav-folder-toggle</code>: desktop folder trigger link/button (recommended).</div>
|
|
1525
|
+
<div><code>cms-nav-folder-menu</code>: desktop dropdown menu panel for folder items (recommended).</div>
|
|
1526
|
+
<div><code>cms-nav-main</code>: optional hook for scroll/sticky/hide classes (defaults to first <code><nav></code>).</div>
|
|
1527
|
+
<div><code>cms-nav-pos-right</code>, <code>cms-nav-pos-left</code>, <code>cms-nav-pos-center</code>: helper classes for menu position behavior.</div>
|
|
1528
|
+
<div><code>cms-nav-layout</code>, <code>cms-nav-logo</code>, <code>cms-nav-desktop</code>: optional structure hooks for precise layout mapping.</div>
|
|
1529
|
+
<div><code>cms-nav-sticky</code>: force sticky top behavior even if your nav did not include fixed classes.</div>
|
|
1530
|
+
<div><code>cms-nav-hide-on-down</code>: hide nav on scroll down, show on scroll up.</div>
|
|
1531
|
+
</div>
|
|
1532
|
+
</section>
|
|
1533
|
+
|
|
1534
|
+
<section class="space-y-2">
|
|
1535
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1536
|
+
Optional Root Attributes
|
|
1537
|
+
</h3>
|
|
1538
|
+
<div class="text-sm text-foreground space-y-1">
|
|
1539
|
+
<div><code>data-cms-nav-open="true"</code> to start open.</div>
|
|
1540
|
+
<div><code>data-cms-nav-open-class="your-class"</code> to change the root open class (default <code>is-open</code>).</div>
|
|
1541
|
+
<div><code>data-cms-nav-close-on-link="false"</code> to keep panel open after link clicks.</div>
|
|
1542
|
+
<div><code>data-cms-nav-position="right|left|center"</code> as an alternative to helper classes.</div>
|
|
1543
|
+
<div><code>data-cms-nav-scrolled-class</code> / <code>data-cms-nav-top-class</code>: classes toggled on nav main target.</div>
|
|
1544
|
+
<div><code>data-cms-nav-scrolled-row-class</code> / <code>data-cms-nav-top-row-class</code>: classes toggled on <code>cms-nav-layout</code> for shrink/expand.</div>
|
|
1545
|
+
<div><code>data-cms-nav-scroll-threshold</code>: px before “scrolled” classes apply (default 10).</div>
|
|
1546
|
+
<div><code>data-cms-nav-hide-on-down="true"</code>, <code>data-cms-nav-hide-threshold</code> (default 80), <code>data-cms-nav-hide-delta</code> (default 6).</div>
|
|
1547
|
+
<div><code>data-cms-nav-hidden-class</code> / <code>data-cms-nav-visible-class</code> / <code>data-cms-nav-transition-class</code> for hide/show animation control.</div>
|
|
1548
|
+
</div>
|
|
1549
|
+
</section>
|
|
1550
|
+
|
|
1551
|
+
<section class="space-y-3">
|
|
1552
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1553
|
+
Nav Block Template (Copy / Paste)
|
|
1554
|
+
</h3>
|
|
1555
|
+
<pre v-pre class="rounded-md bg-muted p-3 text-xs overflow-auto"><code><div class="cms-nav-root cms-nav-sticky" data-cms-nav-root data-cms-nav-position="{{{#text {"field":"navPosition","title":"Menu Position","option":{"field":"navPosition","options":[{"title":"Right","name":"right"},{"title":"Left","name":"left"},{"title":"Center","name":"center"}],"optionsKey":"title","optionsValue":"name"},"value":"right"}}}}" data-cms-nav-close-on-link="true" data-cms-nav-top-class="bg-transparent border-transparent" data-cms-nav-scrolled-class="bg-navBg/80 backdrop-blur-lg shadow-lg" data-cms-nav-top-row-class="h-[64px] md:h-[88px] py-6 md:py-8" data-cms-nav-scrolled-row-class="h-[56px] md:h-[68px] py-5 md:py-4">
|
|
1556
|
+
{{{#array {"field":"siteDoc","collection":{"path":"sites","uniqueKey":"{orgId}","query":[{"field":"docId","operator":"==","value":"{siteId}"}],"order":[]},"limit":1,"value":[]}}}}
|
|
1557
|
+
<nav class="cms-nav-main fixed inset-x-0 top-0 z-30 w-full bg-transparent text-navText">
|
|
1558
|
+
<div class="relative w-full px-6 md:px-12">
|
|
1559
|
+
<div class="cms-nav-layout flex h-[64px] md:h-[88px] items-center justify-between gap-6 py-6 md:py-8">
|
|
1560
|
+
<a href="/" class="cms-nav-logo cursor-pointer text-xl text-navText">
|
|
1561
|
+
{{{#if {"cond":"item.logoLight"}}}}
|
|
1562
|
+
<img src="{{item.logoLight}}" class="h-[56px] md:h-[72px] py-3" />
|
|
1563
|
+
{{{#else}}}
|
|
1564
|
+
<img src="{{item.logo}}" class="h-[56px] md:h-[72px] py-3" />
|
|
1565
|
+
{{{/if}}}
|
|
1566
|
+
</a>
|
|
1567
|
+
|
|
1568
|
+
<div class="cms-nav-desktop ml-auto flex items-center gap-2">
|
|
1569
|
+
<ul class="hidden lg:flex items-center gap-x-[20px] pt-1 text-sm uppercase tracking-widest list-none m-0 p-0 [&>li]:m-0 [&>li>a]:m-0">
|
|
1570
|
+
{{{#subarray:menuItem {"field":"item.menus.Site Root","limit":5,"value":[]}}}}
|
|
1571
|
+
<li class="relative group cms-nav-folder" data-cms-nav-folder>
|
|
1572
|
+
{{{#if {"cond":"menuItem.item.type == 'external'"}}}}
|
|
1573
|
+
<a href="{{menuItem.item.url}}" class="cursor-pointer">{{menuItem.name}}</a>
|
|
1574
|
+
{{{#else}}}
|
|
1575
|
+
{{{#if {"cond":"menuItem.item == '[object Object]'"}}}}
|
|
1576
|
+
{{{#entries:folderEntry {"field":"menuItem.item","value":{}}}}}
|
|
1577
|
+
{{{#if {"cond":"folderEntry.key == 'home'"}}}}
|
|
1578
|
+
<a href="/" class="cms-nav-folder-toggle cursor-pointer text-sideNavText" data-cms-nav-folder-toggle>{{menuItem.menuTitle}}</a>
|
|
1579
|
+
{{{#else}}}
|
|
1580
|
+
<a href="/{{folderEntry.key}}" class="cms-nav-folder-toggle cursor-pointer text-sideNavText" data-cms-nav-folder-toggle>{{menuItem.menuTitle}}</a>
|
|
1581
|
+
{{{/if}}}
|
|
1582
|
+
<div class="cms-nav-folder-menu absolute left-0 top-full z-40 hidden min-w-max whitespace-nowrap bg-sideNavBg text-sideNavText py-2 text-left px-12 normal-case tracking-normal shadow-xl" data-cms-nav-folder-menu>
|
|
1583
|
+
<ul>
|
|
1584
|
+
{{{#subarray:folderChild {"field":"item.value","value":[]}}}}
|
|
1585
|
+
<li class="py-1">
|
|
1586
|
+
{{{#if {"cond":"folderChild.item.type == 'external'"}}}}
|
|
1587
|
+
<a href="{{folderChild.item.url}}" class="block cursor-pointer whitespace-nowrap text-sideNavText">{{folderChild.name}}</a>
|
|
1588
|
+
{{{#else}}}
|
|
1589
|
+
{{{#if {"cond":"folderChild.menuTitle"}}}}
|
|
1590
|
+
<a href="/{{folderEntry.key}}/{{folderChild.name}}" class="block cursor-pointer whitespace-nowrap text-sideNavText">{{folderChild.menuTitle}}</a>
|
|
1591
|
+
{{{#else}}}
|
|
1592
|
+
<a href="/{{folderEntry.key}}/{{folderChild.name}}" class="block cursor-pointer whitespace-nowrap text-sideNavText">{{folderChild.name}}</a>
|
|
1593
|
+
{{{/if}}}
|
|
1594
|
+
{{{/if}}}
|
|
1595
|
+
</li>
|
|
1596
|
+
{{{/subarray}}}
|
|
1597
|
+
</ul>
|
|
1598
|
+
</div>
|
|
1599
|
+
{{{/entries}}}
|
|
1600
|
+
{{{#else}}}
|
|
1601
|
+
{{{#if {"cond":"menuItem.name == 'home'"}}}}
|
|
1602
|
+
{{{#if {"cond":"menuItem.menuTitle"}}}}
|
|
1603
|
+
<a href="/" class="cursor-pointer">{{menuItem.menuTitle}}</a>
|
|
1604
|
+
{{{#else}}}
|
|
1605
|
+
<a href="/" class="cursor-pointer">{{menuItem.name}}</a>
|
|
1606
|
+
{{{/if}}}
|
|
1607
|
+
{{{#else}}}
|
|
1608
|
+
{{{#if {"cond":"menuItem.menuTitle"}}}}
|
|
1609
|
+
<a href="/{{menuItem.name}}" class="cursor-pointer">{{menuItem.menuTitle}}</a>
|
|
1610
|
+
{{{#else}}}
|
|
1611
|
+
<a href="/{{menuItem.name}}" class="cursor-pointer">{{menuItem.name}}</a>
|
|
1612
|
+
{{{/if}}}
|
|
1613
|
+
{{{/if}}}
|
|
1614
|
+
{{{/if}}}
|
|
1615
|
+
{{{/if}}}
|
|
1616
|
+
</li>
|
|
1617
|
+
{{{/subarray}}}
|
|
1618
|
+
</ul>
|
|
1619
|
+
|
|
1620
|
+
<button class="cms-nav-toggle flex h-12 w-12 items-center justify-center rounded-full text-navText" type="button" aria-label="Open Menu">
|
|
1621
|
+
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
1622
|
+
<path d="M4 6h16M4 12h16M4 18h16" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
1623
|
+
</svg>
|
|
1624
|
+
</button>
|
|
1625
|
+
</div>
|
|
1626
|
+
</div>
|
|
1627
|
+
</div>
|
|
1628
|
+
</nav>
|
|
1629
|
+
|
|
1630
|
+
<div class="cms-nav-overlay fixed inset-0 z-[110] bg-black/50 transition-opacity duration-300 opacity-0 pointer-events-none"></div>
|
|
1631
|
+
|
|
1632
|
+
<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">
|
|
1633
|
+
<div class="relative flex h-full flex-col overflow-y-auto px-8 py-10 text-center">
|
|
1634
|
+
<button type="button" class="cms-nav-close absolute right-6 top-6 text-4xl text-sideNavText">&times;</button>
|
|
1635
|
+
|
|
1636
|
+
<div class="mb-8 mt-2 flex items-center justify-center gap-4">
|
|
1637
|
+
<a href="/" class="flex items-center gap-4 text-navText">
|
|
1638
|
+
<img src="{{item.logo}}" class="h-[30px] w-auto max-w-full object-contain" />
|
|
1639
|
+
{{{#if {"cond":"item.brandLogoDark"}}}}
|
|
1640
|
+
<span class="h-10 w-px bg-black" aria-hidden="true"></span>
|
|
1641
|
+
<img src="{{item.brandLogoDark}}" class="h-[30px] w-auto max-w-full object-contain" />
|
|
1642
|
+
{{{/if}}}
|
|
1643
|
+
</a>
|
|
1644
|
+
</div>
|
|
1645
|
+
|
|
1646
|
+
<ul class="w-full space-y-4 border-b border-black pb-4 uppercase">
|
|
1647
|
+
{{{#subarray:menuItem {"field":"item.menus.Site Root","value":[]}}}}
|
|
1648
|
+
<li class="border-t border-black pt-4">
|
|
1649
|
+
{{{#if {"cond":"menuItem.item.type == 'external'"}}}}
|
|
1650
|
+
<a href="{{menuItem.item.url}}" class="cms-nav-link block text-sideNavText tracking-widest text-sm">{{menuItem.name}}</a>
|
|
1651
|
+
{{{#else}}}
|
|
1652
|
+
{{{#if {"cond":"menuItem.item == '[object Object]'"}}}}
|
|
1653
|
+
{{{#entries:folderEntry {"field":"menuItem.item","value":{}}}}}
|
|
1654
|
+
{{{#if {"cond":"folderEntry.key == 'home'"}}}}
|
|
1655
|
+
<a href="/" class="cms-nav-link block text-sideNavText tracking-widest text-sm">{{menuItem.menuTitle}}</a>
|
|
1656
|
+
{{{#else}}}
|
|
1657
|
+
<a href="/{{folderEntry.key}}" class="cms-nav-link block text-sideNavText tracking-widest text-sm">{{menuItem.menuTitle}}</a>
|
|
1658
|
+
{{{/if}}}
|
|
1659
|
+
<ul class="mt-2 space-y-2 border-l border-black/40 pl-4">
|
|
1660
|
+
{{{#subarray:folderChild {"field":"item.value","value":[]}}}}
|
|
1661
|
+
<li>
|
|
1662
|
+
{{{#if {"cond":"folderChild.item.type == 'external'"}}}}
|
|
1663
|
+
<a href="{{folderChild.item.url}}" class="cms-nav-link block text-sideNavText tracking-widest text-xs">{{folderChild.name}}</a>
|
|
1664
|
+
{{{#else}}}
|
|
1665
|
+
{{{#if {"cond":"folderChild.menuTitle"}}}}
|
|
1666
|
+
<a href="/{{folderEntry.key}}/{{folderChild.name}}" class="cms-nav-link block text-sideNavText tracking-widest text-xs">{{folderChild.menuTitle}}</a>
|
|
1667
|
+
{{{#else}}}
|
|
1668
|
+
<a href="/{{folderEntry.key}}/{{folderChild.name}}" class="cms-nav-link block text-sideNavText tracking-widest text-xs">{{folderChild.name}}</a>
|
|
1669
|
+
{{{/if}}}
|
|
1670
|
+
{{{/if}}}
|
|
1671
|
+
</li>
|
|
1672
|
+
{{{/subarray}}}
|
|
1673
|
+
</ul>
|
|
1674
|
+
{{{/entries}}}
|
|
1675
|
+
{{{#else}}}
|
|
1676
|
+
{{{#if {"cond":"menuItem.name == 'home'"}}}}
|
|
1677
|
+
{{{#if {"cond":"menuItem.menuTitle"}}}}
|
|
1678
|
+
<a href="/" class="cms-nav-link block text-sideNavText tracking-widest text-sm">{{menuItem.menuTitle}}</a>
|
|
1679
|
+
{{{#else}}}
|
|
1680
|
+
<a href="/" class="cms-nav-link block text-sideNavText tracking-widest text-sm">{{menuItem.name}}</a>
|
|
1681
|
+
{{{/if}}}
|
|
1682
|
+
{{{#else}}}
|
|
1683
|
+
{{{#if {"cond":"menuItem.menuTitle"}}}}
|
|
1684
|
+
<a href="/{{menuItem.name}}" class="cms-nav-link block text-sideNavText tracking-widest text-sm">{{menuItem.menuTitle}}</a>
|
|
1685
|
+
{{{#else}}}
|
|
1686
|
+
<a href="/{{menuItem.name}}" class="cms-nav-link block text-sideNavText tracking-widest text-sm">{{menuItem.name}}</a>
|
|
1687
|
+
{{{/if}}}
|
|
1688
|
+
{{{/if}}}
|
|
1689
|
+
{{{/if}}}
|
|
1690
|
+
{{{/if}}}
|
|
1691
|
+
</li>
|
|
1692
|
+
{{{/subarray}}}
|
|
1693
|
+
</ul>
|
|
1694
|
+
|
|
1695
|
+
<div class="mt-10 flex w-full items-center justify-center gap-4">
|
|
1696
|
+
{{{#if {"cond":"item.socialFacebook"}}}}
|
|
1697
|
+
<a href="{{item.socialFacebook}}" target="_blank" rel="noopener" class="flex h-10 w-10 items-center justify-center rounded-full border border-sideNavText text-sideNavText transition-colors duration-200 hover:bg-sideNavText hover:text-sideNavBg">
|
|
1698
|
+
<span class="sr-only">Facebook</span>
|
|
1699
|
+
<span class="h-5 w-5 [&>svg]:h-5 [&>svg]:w-5 [&>svg]:fill-current" aria-hidden="true">
|
|
1700
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
|
|
1701
|
+
<path d="M80 299.3V512H196V299.3h86.5l18-97.8H196V166.9c0-51.7 20.3-71.5 72.7-71.5c16.3 0 29.4 .4 37 1.2V7.9C291.4 4 256.4 0 236.2 0C129.3 0 80 50.5 80 159.4v42.1H14v97.8H80z"></path>
|
|
1702
|
+
</svg>
|
|
1703
|
+
</span>
|
|
1704
|
+
</a>
|
|
1705
|
+
{{{/if}}}
|
|
1706
|
+
{{{#if {"cond":"item.socialInstagram"}}}}
|
|
1707
|
+
<a href="{{item.socialInstagram}}" target="_blank" rel="noopener" class="flex h-10 w-10 items-center justify-center rounded-full border border-sideNavText text-sideNavText transition-colors duration-200 hover:bg-sideNavText hover:text-sideNavBg">
|
|
1708
|
+
<span class="sr-only">Instagram</span>
|
|
1709
|
+
<span class="h-5 w-5 [&>svg]:h-5 [&>svg]:w-5 [&>svg]:fill-current" aria-hidden="true">
|
|
1710
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
|
1711
|
+
<path d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"></path>
|
|
1712
|
+
</svg>
|
|
1713
|
+
</span>
|
|
1714
|
+
</a>
|
|
1715
|
+
{{{/if}}}
|
|
1716
|
+
{{{#if {"cond":"item.socialLinkedIn"}}}}
|
|
1717
|
+
<a href="{{item.socialLinkedIn}}" target="_blank" rel="noopener" class="flex h-10 w-10 items-center justify-center rounded-full border border-sideNavText text-sideNavText transition-colors duration-200 hover:bg-sideNavText hover:text-sideNavBg">
|
|
1718
|
+
<span class="sr-only">LinkedIn</span>
|
|
1719
|
+
<span class="h-5 w-5 [&>svg]:h-5 [&>svg]:w-5 [&>svg]:fill-current" aria-hidden="true">
|
|
1720
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
|
1721
|
+
<path d="M100.28 448H7.4V148.9h92.88zM53.79 108.1C24.09 108.1 0 83.5 0 53.8a53.79 53.79 0 0 1 107.58 0c0 29.7-24.1 54.3-53.79 54.3zM447.9 448h-92.68V302.4c0-34.7-.7-79.2-48.29-79.2-48.29 0-55.69 37.7-55.69 76.7V448h-92.78V148.9h89.08v40.8h1.3c12.4-23.5 42.69-48.3 87.88-48.3 94 0 111.28 61.9 111.28 142.3V448z"></path>
|
|
1722
|
+
</svg>
|
|
1723
|
+
</span>
|
|
1724
|
+
</a>
|
|
1725
|
+
{{{/if}}}
|
|
1726
|
+
{{{#if {"cond":"item.socialYouTube"}}}}
|
|
1727
|
+
<a href="{{item.socialYouTube}}" target="_blank" rel="noopener" class="flex h-10 w-10 items-center justify-center rounded-full border border-sideNavText text-sideNavText transition-colors duration-200 hover:bg-sideNavText hover:text-sideNavBg">
|
|
1728
|
+
<span class="sr-only">YouTube</span>
|
|
1729
|
+
<span class="h-5 w-5 [&>svg]:h-5 [&>svg]:w-5 [&>svg]:fill-current" aria-hidden="true">
|
|
1730
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
|
|
1731
|
+
<path d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"></path>
|
|
1732
|
+
</svg>
|
|
1733
|
+
</span>
|
|
1734
|
+
</a>
|
|
1735
|
+
{{{/if}}}
|
|
1736
|
+
</div>
|
|
1737
|
+
</div>
|
|
1738
|
+
</aside>
|
|
1739
|
+
{{{/array}}}
|
|
1740
|
+
</div></code></pre>
|
|
1741
|
+
</section>
|
|
1742
|
+
|
|
1743
|
+
<section class="space-y-2">
|
|
1744
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1745
|
+
Preview + Edit Behavior
|
|
1746
|
+
</h3>
|
|
1747
|
+
<div class="text-sm text-foreground space-y-1">
|
|
1748
|
+
<div>Clicking the nav button opens the slide-out in Block Editor preview and Page Preview mode.</div>
|
|
1749
|
+
<div>Interactive nav elements do not trigger “Edit Block”. Clicking outside them still opens the editor in edit mode.</div>
|
|
1750
|
+
<div>In CMS preview, fixed nav and panel are contained to the preview surface by the block wrapper.</div>
|
|
1751
|
+
<div><code>cms-nav-pos-left</code> also switches the slide-out panel to the left side.</div>
|
|
1752
|
+
</div>
|
|
1753
|
+
</section>
|
|
1754
|
+
</div>
|
|
1755
|
+
</div>
|
|
1756
|
+
</TabsContent>
|
|
1757
|
+
|
|
1758
|
+
<TabsContent value="form-helpers">
|
|
1759
|
+
<div class="h-[calc(100vh-190px)] overflow-y-auto pr-1 pb-6">
|
|
1760
|
+
<div class="space-y-6">
|
|
1761
|
+
<section class="space-y-2">
|
|
1762
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1763
|
+
What This Does
|
|
1764
|
+
</h3>
|
|
1765
|
+
<p class="text-sm text-foreground">
|
|
1766
|
+
Add helper classes or data attributes to a CMS block form, and the client runtime will submit to
|
|
1767
|
+
<code>/api/contact</code> with anti-bot checks and submit history tracking.
|
|
1768
|
+
</p>
|
|
1769
|
+
</section>
|
|
1770
|
+
|
|
1771
|
+
<section class="space-y-2">
|
|
1772
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1773
|
+
CMS Preview Scope
|
|
1774
|
+
</h3>
|
|
1775
|
+
<p class="text-sm text-foreground">
|
|
1776
|
+
In Block Editor, this is for structure and messaging preview only. Use it to verify markup and required-state UX,
|
|
1777
|
+
not to validate end-to-end delivery.
|
|
1778
|
+
</p>
|
|
1779
|
+
</section>
|
|
1780
|
+
|
|
1781
|
+
<section class="space-y-2">
|
|
1782
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1783
|
+
Helper Contract
|
|
1784
|
+
</h3>
|
|
1785
|
+
<div class="text-sm text-foreground space-y-1">
|
|
1786
|
+
<div><code>form.cms-form</code> or <code>form[data-cms-form]</code>: form root.</div>
|
|
1787
|
+
<div><code>.cms-form-required</code> or <code>[data-cms-required="true"]</code>: required field markers.</div>
|
|
1788
|
+
<div><code>.cms-form-submit</code> or <code>[data-cms-form-submit]</code>: submit button.</div>
|
|
1789
|
+
<div><code>.cms-form-message</code> or <code>[data-cms-form-message]</code>: status/error message container.</div>
|
|
1790
|
+
</div>
|
|
1791
|
+
</section>
|
|
1792
|
+
|
|
1793
|
+
<section class="space-y-2">
|
|
1794
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1795
|
+
Defaults + Messages
|
|
1796
|
+
</h3>
|
|
1797
|
+
<div class="text-sm text-foreground space-y-1">
|
|
1798
|
+
<div>Default endpoint: <code>/api/contact</code>.</div>
|
|
1799
|
+
<div><code>data-cms-success-message</code>: override success copy.</div>
|
|
1800
|
+
<div><code>data-cms-error-message</code>: override error copy.</div>
|
|
1801
|
+
<div><code>data-cms-required-message</code>: override required-field copy.</div>
|
|
1802
|
+
</div>
|
|
1803
|
+
</section>
|
|
1804
|
+
|
|
1805
|
+
<section class="space-y-2">
|
|
1806
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1807
|
+
Context IDs
|
|
1808
|
+
</h3>
|
|
1809
|
+
<p class="text-sm text-foreground">
|
|
1810
|
+
Block/Page/Site/Org IDs are inherited from the CMS HTML wrapper automatically, so forms in blocks
|
|
1811
|
+
do not need manual context wiring.
|
|
1812
|
+
</p>
|
|
1813
|
+
</section>
|
|
1814
|
+
|
|
1815
|
+
<section class="space-y-3">
|
|
1816
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1817
|
+
Contact Form Example (Block HTML)
|
|
1818
|
+
</h3>
|
|
1819
|
+
<pre v-pre class="rounded-md bg-muted p-3 text-xs overflow-auto"><code><section
|
|
1820
|
+
class="relative cms-block cms-block-contact-form-placeholder rounded-2xl border border-dashed border-slate-300 bg-slate-50/70 px-4 py-6 sm:px-6 sm:py-8"
|
|
1821
|
+
data-block-type="contact-form-placeholder"
|
|
1822
|
+
>
|
|
1823
|
+
<div class="mx-auto max-w-3xl pt-6">
|
|
1824
|
+
<div class="mb-6 space-y-2 text-center sm:text-left">
|
|
1825
|
+
<h2 class="text-xl font-semibold text-slate-900">
|
|
1826
|
+
{{{#text {"field":"formHeader","title":"Form Header","value":"Contact Us"}}}}
|
|
1827
|
+
</h2>
|
|
1828
|
+
<p class="text-sm text-slate-600">
|
|
1829
|
+
{{{#text {"field":"formSubheader","title":"Form Subheader","value":"Subheader content"}}}}
|
|
1830
|
+
</p>
|
|
1831
|
+
</div>
|
|
1832
|
+
|
|
1833
|
+
<form
|
|
1834
|
+
class="cms-form space-y-4"
|
|
1835
|
+
data-cms-form
|
|
1836
|
+
data-cms-required-message="Please complete all required fields."
|
|
1837
|
+
data-cms-success-message="Thanks! Your message has been sent."
|
|
1838
|
+
data-cms-error-message="Sorry, we could not send your message. Please try again."
|
|
1839
|
+
data-cms-success-class="cms-form-message cms-form-message-success"
|
|
1840
|
+
data-cms-error-class="cms-form-message cms-form-message-error"
|
|
1841
|
+
data-cms-invalid-class="cms-form-field-invalid"
|
|
1842
|
+
data-cms-working-class="cms-form-submitting"
|
|
1843
|
+
>
|
|
1844
|
+
<!-- Honeypot (optional, used by helper if present) -->
|
|
1845
|
+
<div class="pointer-events-none absolute -left-[9999px] top-auto h-px w-px overflow-hidden opacity-0" aria-hidden="true">
|
|
1846
|
+
<label for="cms-company">Company</label>
|
|
1847
|
+
<input id="cms-company" name="company" type="text" tabindex="-1" autocomplete="off" />
|
|
1848
|
+
</div>
|
|
1849
|
+
|
|
1850
|
+
<div class="space-y-4">
|
|
1851
|
+
{{{#array {"field":"formFields","schema":[{"field":"fieldName","type":"text","title":"Field Label"},{"field":"fieldType","type":"option","title":"Field Type","option":{"optionsKey":"title","optionsValue":"value","options":[{"title":"Text","value":"text"},{"title":"Email","value":"email"},{"title":"Phone","value":"tel"},{"title":"Textarea","value":"textarea"}]},"value":"text"},{"field":"fieldRequired","type":"option","title":"Required","option":{"optionsKey":"title","optionsValue":"value","options":[{"title":"Yes","value":"true"},{"title":"No","value":"false"}]},"value":"true"}],"value":[{"fieldName":"Name","fieldType":"text","fieldRequired":"true"},{"fieldName":"Email","fieldType":"email","fieldRequired":"true"},{"fieldName":"Message","fieldType":"textarea","fieldRequired":"true"}]}}}}
|
|
1852
|
+
<div class="space-y-1">
|
|
1853
|
+
<label class="text-xs font-medium uppercase tracking-wide text-slate-600">
|
|
1854
|
+
{{item.fieldName}}
|
|
1855
|
+
</label>
|
|
1856
|
+
|
|
1857
|
+
{{{#if {"cond":"item.fieldType == 'textarea'"}}}}
|
|
1858
|
+
<textarea
|
|
1859
|
+
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900"
|
|
1860
|
+
data-cms-required="{{item.fieldRequired}}"
|
|
1861
|
+
name="{{item.fieldName}}"
|
|
1862
|
+
placeholder="{{item.fieldName}}"
|
|
1863
|
+
rows="6"
|
|
1864
|
+
></textarea>
|
|
1865
|
+
{{{#else}}}
|
|
1866
|
+
<input
|
|
1867
|
+
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900"
|
|
1868
|
+
data-cms-required="{{item.fieldRequired}}"
|
|
1869
|
+
type="{{item.fieldType}}"
|
|
1870
|
+
name="{{item.fieldName}}"
|
|
1871
|
+
placeholder="{{item.fieldName}}"
|
|
1872
|
+
/>
|
|
1873
|
+
{{{/if}}}
|
|
1874
|
+
</div>
|
|
1875
|
+
{{{/array}}}
|
|
1876
|
+
</div>
|
|
1877
|
+
|
|
1878
|
+
<div class="mt-6">
|
|
1879
|
+
<button
|
|
1880
|
+
type="submit"
|
|
1881
|
+
class="cms-form-submit inline-flex w-full items-center justify-center rounded-lg bg-slate-900 px-4 py-2.5 text-sm font-semibold text-white shadow-sm disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
|
|
1882
|
+
data-cms-form-submit
|
|
1883
|
+
>
|
|
1884
|
+
{{{#text {"field":"buttonText","title":"Button Text","value":"Send Message"}}}}
|
|
1885
|
+
</button>
|
|
1886
|
+
</div>
|
|
1887
|
+
|
|
1888
|
+
<p class="cms-form-message hidden text-sm" data-cms-form-message></p>
|
|
1889
|
+
</form>
|
|
1890
|
+
|
|
1891
|
+
<div class="hidden">
|
|
1892
|
+
{{{#text {"field":"emailTo","title":"Email To","value":"test@testing.com"}}}}
|
|
1893
|
+
</div>
|
|
1894
|
+
</div>
|
|
1895
|
+
</section></code></pre>
|
|
1896
|
+
</section>
|
|
1897
|
+
</div>
|
|
1898
|
+
</div>
|
|
1899
|
+
</TabsContent>
|
|
1900
|
+
|
|
1299
1901
|
<TabsContent value="scroll-reveals">
|
|
1300
1902
|
<div class="h-[calc(100vh-190px)] overflow-y-auto pr-1 pb-6">
|
|
1301
1903
|
<div class="space-y-6">
|