@edgedev/create-edge-app 1.2.34 → 1.2.36

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.
@@ -34,6 +34,18 @@ const props = defineProps({
34
34
  type: Boolean,
35
35
  default: false,
36
36
  },
37
+ disableInteractivePreviewInEdit: {
38
+ type: Boolean,
39
+ default: true,
40
+ },
41
+ overrideClicksInEditMode: {
42
+ type: Boolean,
43
+ default: false,
44
+ },
45
+ allowPreviewContentEdit: {
46
+ type: Boolean,
47
+ default: false,
48
+ },
37
49
  })
38
50
  const emit = defineEmits(['update:modelValue', 'delete'])
39
51
  const edgeFirebase = inject('edgeFirebase')
@@ -143,9 +155,11 @@ function extractFieldsInOrder(template) {
143
155
 
144
156
  const modelValue = useVModel(props, 'modelValue', emit)
145
157
  const blockFormRef = ref(null)
158
+ const previewContentEditorRef = ref(null)
146
159
 
147
160
  const state = reactive({
148
161
  open: false,
162
+ editorMode: 'fields',
149
163
  draft: {},
150
164
  delete: false,
151
165
  meta: {},
@@ -162,6 +176,10 @@ const state = reactive({
162
176
  aiGenerating: false,
163
177
  aiError: '',
164
178
  validationErrors: [],
179
+ blockContentDraft: '',
180
+ blockContentDocId: '',
181
+ blockContentUpdating: false,
182
+ blockContentError: '',
165
183
  })
166
184
 
167
185
  const INTERACTIVE_CLICK_SELECTOR = [
@@ -172,6 +190,14 @@ const INTERACTIVE_CLICK_SELECTOR = [
172
190
  '.cms-nav-panel',
173
191
  '.cms-nav-close',
174
192
  '.cms-nav-link',
193
+ '.cms-nav-folder-toggle',
194
+ '.cms-nav-folder-menu',
195
+ '[data-cms-nav-folder-toggle]',
196
+ '[data-cms-nav-folder-menu]',
197
+ ].join(', ')
198
+ const EDITOR_CONTROL_CLICK_SELECTOR = [
199
+ '[data-cms-block-control]',
200
+ '[data-cms-block-ignore-editor-click]',
175
201
  ].join(', ')
176
202
 
177
203
  const hasFixedPositionInContent = computed(() => {
@@ -207,17 +233,26 @@ const effectivePreviewType = computed(() => {
207
233
  return normalizePreviewType(inheritedPreviewType.value ?? resolvedPreviewType.value)
208
234
  })
209
235
 
236
+ const canOpenFieldEditor = computed(() => props.editMode)
237
+ const canOpenPreviewContentEditor = computed(() => !props.editMode && props.allowPreviewContentEdit)
238
+ const canOpenEditor = computed(() => canOpenFieldEditor.value || canOpenPreviewContentEditor.value)
239
+
210
240
  const shouldContainFixedPreview = computed(() => {
211
241
  return (props.editMode || props.containFixed) && hasFixedPositionInContent.value
212
242
  })
213
243
 
244
+ const shouldDisableInteractivePreview = computed(() => {
245
+ return props.editMode && props.disableInteractivePreviewInEdit
246
+ })
247
+
214
248
  const blockWrapperClass = computed(() => ({
215
249
  'overflow-visible': shouldContainFixedPreview.value,
216
- 'min-h-[88px]': props.editMode && shouldContainFixedPreview.value,
250
+ 'min-h-[88px]': props.editMode && shouldContainFixedPreview.value && shouldDisableInteractivePreview.value,
251
+ 'min-h-[calc(100vh-360px)]': props.editMode && shouldContainFixedPreview.value && !shouldDisableInteractivePreview.value,
217
252
  'z-30': shouldContainFixedPreview.value,
218
253
  'bg-white text-black': props.editMode && effectivePreviewType.value === 'light',
219
254
  'bg-neutral-950 text-neutral-50': props.editMode && effectivePreviewType.value === 'dark',
220
- 'cms-nav-edit-static': props.editMode,
255
+ 'cms-nav-edit-static': shouldDisableInteractivePreview.value,
221
256
  }))
222
257
 
223
258
  const blockWrapperStyle = computed(() => {
@@ -236,6 +271,94 @@ const isLightName = (value) => {
236
271
 
237
272
  const previewBackgroundClass = value => (isLightName(value) ? 'bg-neutral-900/90' : 'bg-neutral-100')
238
273
 
274
+ const PLACEHOLDERS = {
275
+ text: 'Lorem ipsum dolor sit amet.',
276
+ textarea: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
277
+ richtext: '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>',
278
+ arrayItem: [
279
+ 'Lorem ipsum dolor sit amet.',
280
+ 'Consectetur adipiscing elit.',
281
+ 'Sed do eiusmod tempor incididunt.',
282
+ ],
283
+ image: 'https://imagedelivery.net/h7EjKG0X9kOxmLp41mxOng/f1f7f610-dfa9-4011-08a3-7a98d95e7500/thumbnail',
284
+ }
285
+
286
+ const BLOCK_CONTENT_SNIPPETS = [
287
+ {
288
+ label: 'Text Field',
289
+ snippet: '{{{#text {"field": "fieldName", "value": "" }}}}',
290
+ description: 'Simple text field placeholder',
291
+ },
292
+ {
293
+ label: 'Text with Options',
294
+ 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"}}}}',
295
+ description: 'Text field with selectable options',
296
+ },
297
+ {
298
+ label: 'Text Area',
299
+ snippet: '{{{#textarea {"field": "fieldName", "value": "" }}}}',
300
+ description: 'Textarea field placeholder',
301
+ },
302
+ {
303
+ label: 'Rich Text',
304
+ snippet: '{{{#richtext {"field": "content", "value": "" }}}}',
305
+ description: 'Rich text field placeholder',
306
+ },
307
+ {
308
+ label: 'Image',
309
+ snippet: '{{{#image {"field": "imageField", "value": "", "tags": ["Backgrounds"] }}}}',
310
+ description: 'Image field placeholder',
311
+ },
312
+ {
313
+ label: 'Array (Basic)',
314
+ snippet: `{{{#array {"field": "items", "value": [] }}}}
315
+ <!-- iterate with {{item}} -->
316
+ {{{/array}}}`,
317
+ description: 'Basic repeating array block',
318
+ },
319
+ {
320
+ label: 'Subarray',
321
+ snippet: `{{{#subarray:child {"field": "item.children", "limit": 0 }}}}
322
+ {{child}}
323
+ {{{/subarray}}}`,
324
+ description: 'Nested array inside an array item',
325
+ },
326
+ {
327
+ label: 'If / Else',
328
+ snippet: `{{{#if {"cond": "condition" }}}}
329
+ <!-- content when condition is true -->
330
+ {{{#else}}}
331
+ <!-- content when condition is false -->
332
+ {{{/if}}}`,
333
+ description: 'Conditional block with optional else',
334
+ },
335
+ ]
336
+
337
+ const insertPreviewSnippet = (snippet) => {
338
+ if (!snippet)
339
+ return
340
+ const editor = previewContentEditorRef.value
341
+ if (!editor || typeof editor.insertSnippet !== 'function')
342
+ return
343
+ editor.insertSnippet(snippet)
344
+ }
345
+
346
+ const previewBlockDisplayName = computed(() => {
347
+ const blockDocId = String(state.blockContentDocId || sourceBlockDocId.value || '').trim()
348
+ const blockDoc = blockDocId
349
+ ? edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/blocks`]?.[blockDocId]
350
+ : null
351
+
352
+ const candidates = [
353
+ blockDoc?.name,
354
+ modelValue.value?.name,
355
+ state.blockContentDocId,
356
+ sourceBlockDocId.value,
357
+ ]
358
+ const found = candidates.find(value => String(value || '').trim())
359
+ return String(found || '').trim() || 'Block'
360
+ })
361
+
239
362
  const ensureQueryItemsDefaults = (meta) => {
240
363
  Object.keys(meta || {}).forEach((key) => {
241
364
  const cfg = meta[key]
@@ -304,11 +427,241 @@ const resetArrayItems = (field, metaSource = null) => {
304
427
  }
305
428
  }
306
429
 
430
+ const TAG_START_RE = /\{\{\{\#([A-Za-z0-9_-]+)\s*\{/g
431
+
432
+ function* iterateTags(html) {
433
+ TAG_START_RE.lastIndex = 0
434
+ for (;;) {
435
+ const m = TAG_START_RE.exec(html)
436
+ if (!m)
437
+ break
438
+
439
+ const type = m[1]
440
+ const configStart = TAG_START_RE.lastIndex - 1
441
+ if (configStart < 0 || html[configStart] !== '{')
442
+ continue
443
+
444
+ const configEnd = findMatchingBrace(html, configStart)
445
+ if (configEnd === -1)
446
+ continue
447
+
448
+ const rawCfg = html.slice(configStart, configEnd + 1)
449
+ const closeTriple = html.indexOf('}}}', configEnd)
450
+ const tagEnd = closeTriple !== -1 ? closeTriple + 3 : configEnd + 1
451
+
452
+ yield { type, rawCfg, tagEnd }
453
+
454
+ TAG_START_RE.lastIndex = tagEnd
455
+ }
456
+ }
457
+
458
+ const parseBlockContentModel = (html) => {
459
+ const values = {}
460
+ const meta = {}
461
+ if (!html)
462
+ return { values, meta }
463
+
464
+ for (const { type, rawCfg } of iterateTags(html)) {
465
+ const cfg = safeParseTagConfig(rawCfg)
466
+ if (!cfg || !cfg.field)
467
+ continue
468
+
469
+ const field = String(cfg.field)
470
+ const title = cfg.title != null ? String(cfg.title) : ''
471
+ const { value: _omitValue, field: _omitField, ...rest } = cfg
472
+ meta[field] = { type, ...rest, title }
473
+
474
+ let val = cfg.value
475
+ if (type === 'image')
476
+ val = !val ? PLACEHOLDERS.image : String(val)
477
+ else if (type === 'text')
478
+ val = !val ? PLACEHOLDERS.text : String(val)
479
+ else if (type === 'array') {
480
+ if (meta[field]?.limit > 0) {
481
+ val = Array(meta[field].limit).fill('placeholder')
482
+ }
483
+ else {
484
+ if (Array.isArray(val)) {
485
+ if (val.length === 0)
486
+ val = PLACEHOLDERS.arrayItem
487
+ }
488
+ else {
489
+ val = PLACEHOLDERS.arrayItem
490
+ }
491
+ }
492
+ }
493
+ else if (type === 'textarea')
494
+ val = !val ? PLACEHOLDERS.textarea : String(val)
495
+ else if (type === 'richtext')
496
+ val = !val ? PLACEHOLDERS.richtext : String(val)
497
+
498
+ values[field] = val
499
+ }
500
+
501
+ return { values, meta }
502
+ }
503
+
504
+ const buildUpdatedBlockDocFromContent = (content, sourceDoc = {}) => {
505
+ const parsed = parseBlockContentModel(content)
506
+ const previousValues = sourceDoc?.values || {}
507
+ const previousMeta = sourceDoc?.meta || {}
508
+ const nextValues = {}
509
+ const nextMeta = {}
510
+
511
+ Object.keys(parsed.values || {}).forEach((field) => {
512
+ if (previousValues[field] !== undefined)
513
+ nextValues[field] = previousValues[field]
514
+ else
515
+ nextValues[field] = parsed.values[field]
516
+ })
517
+
518
+ Object.keys(parsed.meta || {}).forEach((field) => {
519
+ if (previousMeta[field]) {
520
+ nextMeta[field] = {
521
+ ...previousMeta[field],
522
+ ...parsed.meta[field],
523
+ }
524
+ }
525
+ else {
526
+ nextMeta[field] = parsed.meta[field]
527
+ }
528
+ })
529
+
530
+ return { values: nextValues, meta: nextMeta }
531
+ }
532
+
533
+ const blockContentSourceDoc = computed(() => {
534
+ const blockDocId = String(state.blockContentDocId || sourceBlockDocId.value || '').trim()
535
+ if (!blockDocId)
536
+ return null
537
+ return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/blocks`]?.[blockDocId] || null
538
+ })
539
+
540
+ const blockContentPreviewBlock = computed(() => {
541
+ const content = String(state.blockContentDraft ?? '')
542
+ const sourceDoc = modelValue.value || blockContentSourceDoc.value || {}
543
+ const { values, meta } = buildUpdatedBlockDocFromContent(content, sourceDoc)
544
+ const previewType = modelValue.value?.previewType ?? blockContentSourceDoc.value?.previewType
545
+ return {
546
+ id: modelValue.value?.id || 'preview-content',
547
+ blockId: String(state.blockContentDocId || sourceBlockDocId.value || '').trim(),
548
+ name: previewBlockDisplayName.value,
549
+ previewType: normalizePreviewType(previewType),
550
+ content,
551
+ values,
552
+ meta,
553
+ synced: !!sourceDoc?.synced,
554
+ }
555
+ })
556
+
557
+ const previewContentSurfaceClass = computed(() => {
558
+ const previewType = normalizePreviewType(blockContentPreviewBlock.value?.previewType)
559
+ return previewType === 'light'
560
+ ? 'bg-white text-black'
561
+ : 'bg-neutral-950 text-neutral-50'
562
+ })
563
+
564
+ const previewContentCanvasClass = computed(() => {
565
+ const content = String(blockContentPreviewBlock.value?.content || '')
566
+ const hasFixedContent = /\bfixed\b/.test(content)
567
+ return hasFixedContent ? 'min-h-[calc(100vh-380px)]' : 'min-h-[220px]'
568
+ })
569
+
570
+ const openPreviewContentEditor = async () => {
571
+ const blockDocId = sourceBlockDocId.value
572
+ if (!blockDocId)
573
+ return
574
+
575
+ const blocksPath = `${edgeGlobal.edgeState.organizationDocPath}/blocks`
576
+ if (!edgeFirebase.data?.[blocksPath])
577
+ await edgeFirebase.startSnapshot(blocksPath)
578
+
579
+ const blockData = edgeFirebase.data?.[blocksPath]?.[blockDocId]
580
+ if (!blockData) {
581
+ state.blockContentError = 'Unable to load block content.'
582
+ edgeFirebase?.toast?.error?.('Unable to load block content.')
583
+ return
584
+ }
585
+
586
+ state.editorMode = 'content'
587
+ state.blockContentDocId = blockDocId
588
+ state.blockContentDraft = String(modelValue.value?.content || blockData.content || '')
589
+ state.blockContentError = ''
590
+ state.blockContentUpdating = false
591
+ state.validationErrors = []
592
+ state.open = true
593
+ state.afterLoad = true
594
+ }
595
+
596
+ const updateBlockContent = async () => {
597
+ if (state.blockContentUpdating)
598
+ return
599
+ const blockDocId = String(state.blockContentDocId || sourceBlockDocId.value || '').trim()
600
+ if (!blockDocId)
601
+ return
602
+
603
+ const blocksPath = `${edgeGlobal.edgeState.organizationDocPath}/blocks`
604
+ const blockData = edgeFirebase.data?.[blocksPath]?.[blockDocId] || {}
605
+ const nextContent = String(state.blockContentDraft || '')
606
+ // Update shared block defaults from the source block doc.
607
+ const { values: blockValues, meta: blockMeta } = buildUpdatedBlockDocFromContent(nextContent, blockData)
608
+ // Preserve page/block-instance values when editing block content from preview mode.
609
+ const { values: instanceValues, meta: instanceMeta } = buildUpdatedBlockDocFromContent(nextContent, modelValue.value || {})
610
+ const blockUpdatedAt = new Date().toISOString()
611
+
612
+ const previousModelValue = edgeGlobal.dupObject(modelValue.value || {})
613
+ modelValue.value = {
614
+ ...(modelValue.value || {}),
615
+ content: nextContent,
616
+ values: instanceValues,
617
+ meta: instanceMeta,
618
+ blockUpdatedAt,
619
+ blockId: blockData?.docId || blockDocId,
620
+ }
621
+
622
+ state.blockContentError = ''
623
+ state.blockContentUpdating = true
624
+ try {
625
+ const results = await edgeFirebase.changeDoc(blocksPath, blockDocId, {
626
+ content: nextContent,
627
+ values: blockValues,
628
+ meta: blockMeta,
629
+ blockUpdatedAt,
630
+ })
631
+ if (results?.success === false) {
632
+ throw new Error(results?.error || 'Failed to update block content.')
633
+ }
634
+ edgeFirebase?.toast?.success?.('Block content updated.')
635
+ state.open = false
636
+ }
637
+ catch (error) {
638
+ modelValue.value = previousModelValue
639
+ state.blockContentError = error?.message || 'Unable to save block content.'
640
+ edgeFirebase?.toast?.error?.(state.blockContentError)
641
+ }
642
+ finally {
643
+ state.blockContentUpdating = false
644
+ }
645
+ }
646
+
307
647
  const openEditor = async (event) => {
308
- if (!props.editMode)
648
+ if (!canOpenEditor.value)
309
649
  return
310
650
  const target = event?.target
311
- if (target?.closest?.(INTERACTIVE_CLICK_SELECTOR))
651
+ if (target?.closest?.(EDITOR_CONTROL_CLICK_SELECTOR))
652
+ return
653
+ const shouldOverrideEditClicks = props.editMode && props.overrideClicksInEditMode
654
+ if (shouldOverrideEditClicks) {
655
+ event?.preventDefault?.()
656
+ event?.stopPropagation?.()
657
+ }
658
+ if (canOpenPreviewContentEditor.value) {
659
+ event?.preventDefault?.()
660
+ event?.stopPropagation?.()
661
+ await openPreviewContentEditor()
662
+ return
663
+ }
664
+ if (!shouldOverrideEditClicks && target?.closest?.(INTERACTIVE_CLICK_SELECTOR))
312
665
  return
313
666
  const blockData = edgeFirebase.data[`${edgeGlobal.edgeState.organizationDocPath}/blocks`]?.[modelValue.value.blockId]
314
667
  const templateMeta = blockData?.meta || modelValue.value?.meta || {}
@@ -348,6 +701,7 @@ const openEditor = async (event) => {
348
701
  }
349
702
  modelValue.value.blockUpdatedAt = new Date().toISOString()
350
703
  state.validationErrors = []
704
+ state.editorMode = 'fields'
351
705
  state.open = true
352
706
  state.afterLoad = true
353
707
  }
@@ -669,28 +1023,33 @@ const getTagsFromPosts = computed(() => {
669
1023
  <template>
670
1024
  <div>
671
1025
  <div
672
- :class="[{ 'cursor-pointer': props.editMode }, blockWrapperClass]"
1026
+ :class="[{ 'cursor-pointer': canOpenEditor }, blockWrapperClass]"
673
1027
  :style="blockWrapperStyle"
674
1028
  class="relative group"
675
- @click="openEditor($event)"
1029
+ @click.capture="openEditor($event)"
676
1030
  >
677
1031
  <!-- Content -->
678
- <edge-cms-block-api :site-id="props.siteId" :theme="props.theme" :content="modelValue?.content" :values="modelValue?.values" :meta="modelValue?.meta" :viewport-mode="props.viewportMode" @pending="state.loading = $event" />
679
- <edge-cms-block-render
680
- v-if="state.loading"
681
- :content="loadingRender(modelValue?.content)"
682
- :values="modelValue?.values"
683
- :meta="modelValue?.meta"
684
- :theme="props.theme"
685
- :viewport-mode="props.viewportMode"
686
- />
1032
+ <div :class="props.editMode && props.overrideClicksInEditMode ? 'pointer-events-none' : ''">
1033
+ <edge-cms-block-api :site-id="props.siteId" :theme="props.theme" :content="modelValue?.content" :values="modelValue?.values" :meta="modelValue?.meta" :viewport-mode="props.viewportMode" @pending="state.loading = $event" />
1034
+ <edge-cms-block-render
1035
+ v-if="state.loading"
1036
+ :content="loadingRender(modelValue?.content)"
1037
+ :values="modelValue?.values"
1038
+ :meta="modelValue?.meta"
1039
+ :theme="props.theme"
1040
+ :viewport-mode="props.viewportMode"
1041
+ />
1042
+ </div>
687
1043
  <!-- Darken overlay on hover -->
688
- <div v-if="props.editMode" class="pointer-events-none absolute inset-0 bg-black/50 opacity-0 transition-opacity duration-200 group-hover:opacity-100 z-10" />
1044
+ <div v-if="props.editMode" class="pointer-events-none absolute inset-0 bg-black/50 opacity-0 transition-opacity duration-200 group-hover:opacity-100 z-[10000]" />
689
1045
 
690
1046
  <!-- Hover controls -->
691
- <div v-if="props.editMode" class="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-20">
1047
+ <div
1048
+ v-if="props.editMode"
1049
+ class="pointer-events-none absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-[10001]"
1050
+ >
692
1051
  <!-- Delete button top right -->
693
- <div v-if="props.allowDelete" class="absolute top-2 right-2">
1052
+ <div v-if="props.allowDelete" data-cms-block-control class="pointer-events-auto absolute top-2 right-2">
694
1053
  <edge-shad-button
695
1054
  variant="destructive"
696
1055
  size="icon"
@@ -729,317 +1088,420 @@ const getTagsFromPosts = computed(() => {
729
1088
  </edge-shad-dialog>
730
1089
 
731
1090
  <Sheet v-model:open="state.open">
732
- <edge-cms-block-sheet-content v-if="state.afterLoad" class="w-full md:w-1/2 max-w-none sm:max-w-none max-w-2xl">
733
- <SheetHeader>
734
- <div class="flex flex-col gap-3 pr-10 md:flex-row md:items-start md:justify-between">
735
- <div class="min-w-0">
736
- <SheetTitle>Edit Block</SheetTitle>
737
- <SheetDescription v-if="modelValue.synced" class="text-sm text-red-500">
738
- This is a synced block. Changes made here will be reflected across all instances of this block on your site.
739
- </SheetDescription>
1091
+ <edge-cms-block-sheet-content
1092
+ v-if="state.afterLoad"
1093
+ :side="state.editorMode === 'content' ? 'left' : 'right'"
1094
+ :class="state.editorMode === 'content'
1095
+ ? 'w-full max-w-none sm:max-w-none md:max-w-none'
1096
+ : 'w-full md:w-1/2 max-w-none sm:max-w-none max-w-2xl'"
1097
+ >
1098
+ <template v-if="state.editorMode === 'content'">
1099
+ <SheetHeader>
1100
+ <div class="flex flex-col gap-2 pr-10">
1101
+ <div class="min-w-0">
1102
+ <SheetTitle>Edit Block Content: {{ previewBlockDisplayName }}</SheetTitle>
1103
+ <SheetDescription class="text-sm text-muted-foreground">
1104
+ Update this block template and save it globally. Changes will sync to every page using this block.
1105
+ </SheetDescription>
1106
+ </div>
740
1107
  </div>
741
- <edge-shad-button
742
- type="button"
743
- size="sm"
744
- class="h-8 gap-2 md:self-start"
745
- variant="outline"
746
- :disabled="!aiFieldOptions.length"
747
- @click="openAiDialog"
748
- >
749
- <Sparkles class="w-4 h-4" />
750
- Generate with AI
751
- </edge-shad-button>
752
- </div>
753
- </SheetHeader>
754
-
755
- <edge-shad-form ref="blockFormRef">
756
- <div v-if="editableMetaEntries.length === 0">
757
- <Alert variant="info" class="mt-4 mb-4">
758
- <AlertTitle>No editable fields found</AlertTitle>
1108
+ </SheetHeader>
1109
+ <div class="px-6 pb-1 pt-3 flex h-[calc(100vh-116px)] flex-col gap-2 overflow-hidden">
1110
+ <div class="flex-1 min-h-0 grid grid-cols-1 xl:grid-cols-2 gap-3">
1111
+ <div class="min-h-0">
1112
+ <edge-cms-code-editor
1113
+ ref="previewContentEditorRef"
1114
+ v-model="state.blockContentDraft"
1115
+ title="Block Content"
1116
+ language="handlebars"
1117
+ name="preview-block-content"
1118
+ :enable-formatting="false"
1119
+ height="calc(100vh - 295px)"
1120
+ class="h-full min-h-0"
1121
+ >
1122
+ <template #end-actions>
1123
+ <DropdownMenu>
1124
+ <DropdownMenuTrigger as-child>
1125
+ <edge-shad-button
1126
+ type="button"
1127
+ size="sm"
1128
+ variant="outline"
1129
+ class="h-8 px-2 text-[11px] uppercase tracking-wide"
1130
+ >
1131
+ Dynamic Content
1132
+ </edge-shad-button>
1133
+ </DropdownMenuTrigger>
1134
+ <DropdownMenuContent align="end" class="w-72">
1135
+ <DropdownMenuItem
1136
+ v-for="snippet in BLOCK_CONTENT_SNIPPETS"
1137
+ :key="snippet.label"
1138
+ class="cursor-pointer flex-col items-start gap-0.5"
1139
+ @click="insertPreviewSnippet(snippet.snippet)"
1140
+ >
1141
+ <span class="text-sm font-medium">{{ snippet.label }}</span>
1142
+ <span class="text-xs text-muted-foreground whitespace-normal">{{ snippet.description }}</span>
1143
+ </DropdownMenuItem>
1144
+ </DropdownMenuContent>
1145
+ </DropdownMenu>
1146
+ </template>
1147
+ </edge-cms-code-editor>
1148
+ </div>
1149
+ <div class="min-h-0 rounded-md border border-border bg-card overflow-hidden flex flex-col">
1150
+ <div class="px-3 py-2 border-b border-border text-xs font-semibold uppercase tracking-wide text-muted-foreground bg-muted/40">
1151
+ Live Preview
1152
+ </div>
1153
+ <div class="flex-1 min-h-0 overflow-y-auto p-3">
1154
+ <div class="relative overflow-visible rounded-none" :class="[previewContentSurfaceClass, previewContentCanvasClass]" style="transform: translateZ(0);">
1155
+ <edge-cms-block-api
1156
+ :site-id="props.siteId"
1157
+ :theme="props.theme"
1158
+ :content="blockContentPreviewBlock.content"
1159
+ :values="blockContentPreviewBlock.values"
1160
+ :meta="blockContentPreviewBlock.meta"
1161
+ :viewport-mode="props.viewportMode"
1162
+ />
1163
+ </div>
1164
+ </div>
1165
+ </div>
1166
+ </div>
1167
+ <Alert v-if="state.blockContentError" variant="destructive" class="mb-2">
1168
+ <AlertTitle>Save failed</AlertTitle>
759
1169
  <AlertDescription class="text-sm">
760
- This block does not have any editable fields defined.
1170
+ {{ state.blockContentError }}
761
1171
  </AlertDescription>
762
1172
  </Alert>
1173
+ <SheetFooter class="flex justify-between pt-1 pb-0">
1174
+ <edge-shad-button
1175
+ variant="destructive"
1176
+ class="text-white"
1177
+ :disabled="state.blockContentUpdating"
1178
+ @click="state.open = false"
1179
+ >
1180
+ Cancel
1181
+ </edge-shad-button>
1182
+ <edge-shad-button
1183
+ class="bg-slate-800 hover:bg-slate-400 w-full"
1184
+ :disabled="state.blockContentUpdating"
1185
+ @click="updateBlockContent"
1186
+ >
1187
+ <Loader2 v-if="state.blockContentUpdating" class="w-4 h-4 mr-2 animate-spin" />
1188
+ Update
1189
+ </edge-shad-button>
1190
+ </SheetFooter>
763
1191
  </div>
764
- <div :class="modelValue.synced ? 'h-[calc(100vh-160px)]' : 'h-[calc(100vh-130px)]'" class="p-6 space-y-4 overflow-y-auto">
765
- <template v-for="entry in editableMetaEntries" :key="entry.field">
766
- <div v-if="entry.meta.type === 'array'">
767
- <div v-if="!entry.meta?.api && !entry.meta?.collection">
768
- <div v-if="entry.meta?.schema">
769
- <Card v-if="!state.reload" class="mb-4 bg-white shadow-sm border border-gray-200 p-4">
770
- <CardHeader class="p-0 mb-2">
771
- <div class="relative flex items-center bg-secondary p-2 justify-between sticky top-0 z-10 bg-primary rounded">
772
- <span class="text-lg font-semibold whitespace-nowrap pr-1"> {{ genTitleFromField(entry) }}</span>
773
- <div class="flex w-full items-center">
774
- <div class="w-full border-t border-gray-300 dark:border-white/15" aria-hidden="true" />
775
- <edge-shad-button variant="text" class="hover:text-primary/50 text-xs h-[26px] text-primary" @click="state.editMode = !state.editMode">
776
- <Popover>
777
- <PopoverTrigger as-child>
778
- <edge-shad-button
779
- variant="text"
780
- type="submit"
781
- class="bg-secondary hover:text-primary/50 text-xs h-[26px] text-primary"
782
- >
783
- <Plus class="w-4 h-4" />
784
- </edge-shad-button>
785
- </PopoverTrigger>
786
- <PopoverContent class="!w-80 mr-20">
787
- <Card class="border-none shadow-none p-4">
788
- <template v-for="schemaItem in entry.meta.schema" :key="schemaItem.field">
789
- <edge-cms-block-input
790
- v-model="state.arrayItems[entry.field][schemaItem.field]"
791
- :type="schemaItem.type"
792
- :field="schemaItem.field"
793
- :schema="schemaItem"
794
- :label="genTitleFromField(schemaItem)"
795
- />
796
- </template>
797
- <CardFooter class="mt-2 flex justify-end">
798
- <edge-shad-button
799
- class="bg-secondary hover:text-white text-xs h-[26px] text-primary"
800
- @click="addToArray(entry.field)"
801
- >
802
- Add Entry
803
- </edge-shad-button>
804
- </CardFooter>
805
- </Card>
806
- </PopoverContent>
807
- </Popover>
808
- </edge-shad-button>
809
- </div>
810
- </div>
811
- </CardHeader>
812
- <draggable
813
- v-if="state.draft?.[entry.field] && state.draft[entry.field].length > 0"
814
- v-model="state.draft[entry.field]"
815
- handle=".handle"
816
- item-key="index"
817
- >
818
- <template #item="{ element, index }">
819
- <div :key="index" class="">
820
- <div class="flex gap-2 w-full items-center w-full border-1 border-dotted py-1 mb-1">
821
- <div class="text-left px-2">
822
- <Grip class="handle pointer" />
823
- </div>
824
- <div class="px-2 py-2 w-[98%] flex gap-1">
825
- <template v-for="schemaItem in entry.meta.schema" :key="schemaItem.field">
826
- <Popover>
827
- <PopoverTrigger as-child>
828
- <Alert class="w-[200px] text-xs py-1 px-2 cursor-pointer hover:bg-primary hover:text-white">
829
- <AlertTitle> {{ genTitleFromField(schemaItem) }}</AlertTitle>
830
- <AlertDescription class="text-sm truncate max-w-[200px]">
831
- {{ element[schemaItem.field] }}
832
- </AlertDescription>
833
- </Alert>
834
- </PopoverTrigger>
835
- <PopoverContent class="!w-80 mr-20">
836
- <Card class="border-none shadow-none p-4">
1192
+ </template>
1193
+ <template v-else>
1194
+ <SheetHeader>
1195
+ <div class="flex flex-col gap-3 pr-10 md:flex-row md:items-start md:justify-between">
1196
+ <div class="min-w-0">
1197
+ <SheetTitle>Edit Block</SheetTitle>
1198
+ <SheetDescription v-if="modelValue.synced" class="text-sm text-red-500">
1199
+ This is a synced block. Changes made here will be reflected across all instances of this block on your site.
1200
+ </SheetDescription>
1201
+ </div>
1202
+ <edge-shad-button
1203
+ type="button"
1204
+ size="sm"
1205
+ class="h-8 gap-2 md:self-start"
1206
+ variant="outline"
1207
+ :disabled="!aiFieldOptions.length"
1208
+ @click="openAiDialog"
1209
+ >
1210
+ <Sparkles class="w-4 h-4" />
1211
+ Generate with AI
1212
+ </edge-shad-button>
1213
+ </div>
1214
+ </SheetHeader>
1215
+
1216
+ <edge-shad-form ref="blockFormRef">
1217
+ <div v-if="editableMetaEntries.length === 0">
1218
+ <Alert variant="info" class="mt-4 mb-4">
1219
+ <AlertTitle>No editable fields found</AlertTitle>
1220
+ <AlertDescription class="text-sm">
1221
+ This block does not have any editable fields defined.
1222
+ </AlertDescription>
1223
+ </Alert>
1224
+ </div>
1225
+ <div :class="modelValue.synced ? 'h-[calc(100vh-160px)]' : 'h-[calc(100vh-130px)]'" class="p-6 space-y-4 overflow-y-auto">
1226
+ <template v-for="entry in editableMetaEntries" :key="entry.field">
1227
+ <div v-if="entry.meta.type === 'array'">
1228
+ <div v-if="!entry.meta?.api && !entry.meta?.collection">
1229
+ <div v-if="entry.meta?.schema">
1230
+ <Card v-if="!state.reload" class="mb-4 bg-white shadow-sm border border-gray-200 p-4">
1231
+ <CardHeader class="p-0 mb-2">
1232
+ <div class="relative flex items-center bg-secondary p-2 justify-between sticky top-0 z-10 bg-primary rounded">
1233
+ <span class="text-lg font-semibold whitespace-nowrap pr-1"> {{ genTitleFromField(entry) }}</span>
1234
+ <div class="flex w-full items-center">
1235
+ <div class="w-full border-t border-gray-300 dark:border-white/15" aria-hidden="true" />
1236
+ <edge-shad-button variant="text" class="hover:text-primary/50 text-xs h-[26px] text-primary" @click="state.editMode = !state.editMode">
1237
+ <Popover>
1238
+ <PopoverTrigger as-child>
1239
+ <edge-shad-button
1240
+ variant="text"
1241
+ type="submit"
1242
+ class="bg-secondary hover:text-primary/50 text-xs h-[26px] text-primary"
1243
+ >
1244
+ <Plus class="w-4 h-4" />
1245
+ </edge-shad-button>
1246
+ </PopoverTrigger>
1247
+ <PopoverContent class="!w-80 mr-20">
1248
+ <Card class="border-none shadow-none p-4">
1249
+ <template v-for="schemaItem in entry.meta.schema" :key="schemaItem.field">
837
1250
  <edge-cms-block-input
838
- v-model="element[schemaItem.field]"
1251
+ v-model="state.arrayItems[entry.field][schemaItem.field]"
839
1252
  :type="schemaItem.type"
1253
+ :field="schemaItem.field"
840
1254
  :schema="schemaItem"
841
- :field="`${schemaItem.field}-${index}-entry`"
842
1255
  :label="genTitleFromField(schemaItem)"
843
1256
  />
844
- </Card>
845
- </PopoverContent>
846
- </Popover>
847
- </template>
848
- </div>
849
- <div class="pr-2">
850
- <edge-shad-button
851
- variant="destructive"
852
- size="icon"
853
- @click="state.draft[entry.field].splice(index, 1)"
854
- >
855
- <Trash class="h-4 w-4" />
856
- </edge-shad-button>
857
- </div>
1257
+ </template>
1258
+ <CardFooter class="mt-2 flex justify-end">
1259
+ <edge-shad-button
1260
+ class="bg-secondary hover:text-white text-xs h-[26px] text-primary"
1261
+ @click="addToArray(entry.field)"
1262
+ >
1263
+ Add Entry
1264
+ </edge-shad-button>
1265
+ </CardFooter>
1266
+ </Card>
1267
+ </PopoverContent>
1268
+ </Popover>
1269
+ </edge-shad-button>
858
1270
  </div>
859
1271
  </div>
860
- </template>
861
- </draggable>
862
- </Card>
1272
+ </CardHeader>
1273
+ <draggable
1274
+ v-if="state.draft?.[entry.field] && state.draft[entry.field].length > 0"
1275
+ v-model="state.draft[entry.field]"
1276
+ handle=".handle"
1277
+ item-key="index"
1278
+ >
1279
+ <template #item="{ element, index }">
1280
+ <div :key="index" class="">
1281
+ <div class="flex gap-2 w-full items-center w-full border-1 border-dotted py-1 mb-1">
1282
+ <div class="text-left px-2">
1283
+ <Grip class="handle pointer" />
1284
+ </div>
1285
+ <div class="px-2 py-2 w-[98%] flex gap-1">
1286
+ <template v-for="schemaItem in entry.meta.schema" :key="schemaItem.field">
1287
+ <Popover>
1288
+ <PopoverTrigger as-child>
1289
+ <Alert class="w-[200px] text-xs py-1 px-2 cursor-pointer hover:bg-primary hover:text-white">
1290
+ <AlertTitle> {{ genTitleFromField(schemaItem) }}</AlertTitle>
1291
+ <AlertDescription class="text-sm truncate max-w-[200px]">
1292
+ {{ element[schemaItem.field] }}
1293
+ </AlertDescription>
1294
+ </Alert>
1295
+ </PopoverTrigger>
1296
+ <PopoverContent class="!w-80 mr-20">
1297
+ <Card class="border-none shadow-none p-4">
1298
+ <edge-cms-block-input
1299
+ v-model="element[schemaItem.field]"
1300
+ :type="schemaItem.type"
1301
+ :schema="schemaItem"
1302
+ :field="`${schemaItem.field}-${index}-entry`"
1303
+ :label="genTitleFromField(schemaItem)"
1304
+ />
1305
+ </Card>
1306
+ </PopoverContent>
1307
+ </Popover>
1308
+ </template>
1309
+ </div>
1310
+ <div class="pr-2">
1311
+ <edge-shad-button
1312
+ variant="destructive"
1313
+ size="icon"
1314
+ @click="state.draft[entry.field].splice(index, 1)"
1315
+ >
1316
+ <Trash class="h-4 w-4" />
1317
+ </edge-shad-button>
1318
+ </div>
1319
+ </div>
1320
+ </div>
1321
+ </template>
1322
+ </draggable>
1323
+ </Card>
1324
+ </div>
1325
+ <edge-cms-block-input
1326
+ v-else
1327
+ v-model="state.draft[entry.field]"
1328
+ :type="entry.meta.type"
1329
+ :field="entry.field"
1330
+ :label="genTitleFromField(entry)"
1331
+ />
863
1332
  </div>
864
- <edge-cms-block-input
865
- v-else
1333
+ <div v-else>
1334
+ <template v-if="entry.meta?.queryOptions">
1335
+ <div v-for="option in entry.meta.queryOptions" :key="option.field" class="mb-2">
1336
+ <edge-shad-select-tags
1337
+ v-if="entry.meta?.collection?.path === 'posts' && option.field === 'tags'"
1338
+ v-model="state.meta[entry.field].queryItems[option.field]"
1339
+ :items="getTagsFromPosts"
1340
+ :label="`${genTitleFromField(option)}`"
1341
+ :name="option.field"
1342
+ :placeholder="`Select ${genTitleFromField(option)}`"
1343
+ />
1344
+ <edge-cms-options-select
1345
+ v-else-if="entry.meta?.collection?.path !== 'post'"
1346
+ v-model="state.meta[entry.field].queryItems[option.field]"
1347
+ :option="option"
1348
+ :label="genTitleFromField(option)"
1349
+ :multiple="option?.multiple || false"
1350
+ />
1351
+ </div>
1352
+ </template>
1353
+ <edge-shad-number
1354
+ v-if="entry.meta?.collection?.path !== 'post' && !isLimitOne(entry.field)"
1355
+ v-model="state.meta[entry.field].limit"
1356
+ name="limit"
1357
+ label="Limit"
1358
+ />
1359
+ </div>
1360
+ </div>
1361
+ <div v-else-if="entry.meta?.type === 'image'" class="w-full">
1362
+ <div class="mb-2 text-sm font-medium text-foreground">
1363
+ {{ genTitleFromField(entry) }}
1364
+ </div>
1365
+ <div class="relative py-2 rounded-md flex items-center justify-center" :class="previewBackgroundClass(state.draft[entry.field])">
1366
+ <div class="bg-black/80 absolute left-0 top-0 w-full h-full opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center z-10 cursor-pointer">
1367
+ <Dialog v-model:open="state.imageOpenByField[entry.field]">
1368
+ <DialogTrigger as-child>
1369
+ <edge-shad-button variant="outline" class="bg-white text-black hover:bg-gray-200">
1370
+ <ImagePlus class="h-5 w-5 mr-2" />
1371
+ Select Image
1372
+ </edge-shad-button>
1373
+ </DialogTrigger>
1374
+ <DialogContent class="w-full max-w-[1200px] max-h-[80vh] overflow-y-auto">
1375
+ <DialogHeader>
1376
+ <DialogTitle>Select Image</DialogTitle>
1377
+ <DialogDescription />
1378
+ </DialogHeader>
1379
+ <edge-cms-media-manager
1380
+ v-if="entry.meta?.tags && entry.meta.tags.length > 0"
1381
+ :site="props.siteId"
1382
+ :select-mode="true"
1383
+ :default-tags="entry.meta.tags"
1384
+ @select="(url) => { state.draft[entry.field] = url; state.imageOpenByField[entry.field] = false }"
1385
+ />
1386
+ <edge-cms-media-manager
1387
+ v-else
1388
+ :site="props.siteId"
1389
+ :select-mode="true"
1390
+ @select="(url) => { state.draft[entry.field] = url; state.imageOpenByField[entry.field] = false }"
1391
+ />
1392
+ </DialogContent>
1393
+ </Dialog>
1394
+ </div>
1395
+ <img
1396
+ v-if="state.draft[entry.field]"
1397
+ :src="state.draft[entry.field]"
1398
+ class="max-h-40 max-w-full h-auto w-auto object-contain"
1399
+ >
1400
+ </div>
1401
+ </div>
1402
+ <div v-else-if="entry.meta?.option">
1403
+ <edge-cms-options-select
866
1404
  v-model="state.draft[entry.field]"
867
- :type="entry.meta.type"
868
- :field="entry.field"
1405
+ :option="entry.meta.option"
869
1406
  :label="genTitleFromField(entry)"
870
1407
  />
871
1408
  </div>
872
1409
  <div v-else>
873
- <template v-if="entry.meta?.queryOptions">
874
- <div v-for="option in entry.meta.queryOptions" :key="option.field" class="mb-2">
875
- <edge-shad-select-tags
876
- v-if="entry.meta?.collection?.path === 'posts' && option.field === 'tags'"
877
- v-model="state.meta[entry.field].queryItems[option.field]"
878
- :items="getTagsFromPosts"
879
- :label="`${genTitleFromField(option)}`"
880
- :name="option.field"
881
- :placeholder="`Select ${genTitleFromField(option)}`"
882
- />
883
- <edge-cms-options-select
884
- v-else-if="entry.meta?.collection?.path !== 'post'"
885
- v-model="state.meta[entry.field].queryItems[option.field]"
886
- :option="option"
887
- :label="genTitleFromField(option)"
888
- :multiple="option?.multiple || false"
889
- />
890
- </div>
891
- </template>
892
- <edge-shad-number
893
- v-if="entry.meta?.collection?.path !== 'post' && !isLimitOne(entry.field)"
894
- v-model="state.meta[entry.field].limit"
895
- name="limit"
896
- label="Limit"
1410
+ <edge-cms-block-input
1411
+ v-model="state.draft[entry.field]"
1412
+ :type="entry.meta.type"
1413
+ :field="entry.field"
1414
+ :label="genTitleFromField(entry)"
897
1415
  />
898
1416
  </div>
899
- </div>
900
- <div v-else-if="entry.meta?.type === 'image'" class="w-full">
901
- <div class="mb-2 text-sm font-medium text-foreground">
902
- {{ genTitleFromField(entry) }}
903
- </div>
904
- <div class="relative py-2 rounded-md flex items-center justify-center" :class="previewBackgroundClass(state.draft[entry.field])">
905
- <div class="bg-black/80 absolute left-0 top-0 w-full h-full opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center z-10 cursor-pointer">
906
- <Dialog v-model:open="state.imageOpenByField[entry.field]">
907
- <DialogTrigger as-child>
908
- <edge-shad-button variant="outline" class="bg-white text-black hover:bg-gray-200">
909
- <ImagePlus class="h-5 w-5 mr-2" />
910
- Select Image
911
- </edge-shad-button>
912
- </DialogTrigger>
913
- <DialogContent class="w-full max-w-[1200px] max-h-[80vh] overflow-y-auto">
914
- <DialogHeader>
915
- <DialogTitle>Select Image</DialogTitle>
916
- <DialogDescription />
917
- </DialogHeader>
918
- <edge-cms-media-manager
919
- v-if="entry.meta?.tags && entry.meta.tags.length > 0"
920
- :site="props.siteId"
921
- :select-mode="true"
922
- :default-tags="entry.meta.tags"
923
- @select="(url) => { state.draft[entry.field] = url; state.imageOpenByField[entry.field] = false }"
924
- />
925
- <edge-cms-media-manager
926
- v-else
927
- :site="props.siteId"
928
- :select-mode="true"
929
- @select="(url) => { state.draft[entry.field] = url; state.imageOpenByField[entry.field] = false }"
930
- />
931
- </DialogContent>
932
- </Dialog>
1417
+ </template>
1418
+ </div>
1419
+
1420
+ <div class="sticky bottom-0 bg-background px-6 pb-4 pt-2">
1421
+ <Alert v-if="state.validationErrors.length" variant="destructive" class="mb-3">
1422
+ <AlertTitle>Fix the highlighted fields</AlertTitle>
1423
+ <AlertDescription class="text-sm">
1424
+ <div v-for="(error, index) in state.validationErrors" :key="`${error}-${index}`">
1425
+ {{ error }}
933
1426
  </div>
934
- <img
935
- v-if="state.draft[entry.field]"
936
- :src="state.draft[entry.field]"
937
- class="max-h-40 max-w-full h-auto w-auto object-contain"
938
- >
939
- </div>
940
- </div>
941
- <div v-else-if="entry.meta?.option">
942
- <edge-cms-options-select
943
- v-model="state.draft[entry.field]"
944
- :option="entry.meta.option"
945
- :label="genTitleFromField(entry)"
946
- />
947
- </div>
948
- <div v-else>
949
- <edge-cms-block-input
950
- v-model="state.draft[entry.field]"
951
- :type="entry.meta.type"
952
- :field="entry.field"
953
- :label="genTitleFromField(entry)"
1427
+ </AlertDescription>
1428
+ </Alert>
1429
+ <SheetFooter class="flex justify-between">
1430
+ <edge-shad-button variant="destructive" class="text-white" @click="state.open = false">
1431
+ Cancel
1432
+ </edge-shad-button>
1433
+ <edge-shad-button class=" bg-slate-800 hover:bg-slate-400 w-full" @click="save">
1434
+ Save changes
1435
+ </edge-shad-button>
1436
+ </SheetFooter>
1437
+ </div>
1438
+ </edge-shad-form>
1439
+
1440
+ <edge-shad-dialog v-model="state.aiDialogOpen">
1441
+ <DialogContent class="max-w-[640px]">
1442
+ <DialogHeader>
1443
+ <DialogTitle>Generate with AI</DialogTitle>
1444
+ <DialogDescription>
1445
+ Choose which fields the AI should fill and add any optional instructions.
1446
+ </DialogDescription>
1447
+ </DialogHeader>
1448
+ <div class="space-y-4">
1449
+ <edge-shad-textarea
1450
+ v-model="state.aiInstructions"
1451
+ name="aiInstructions"
1452
+ label="Instructions (Optional)"
1453
+ placeholder="Share tone, audience, and any details the AI should include."
954
1454
  />
955
- </div>
956
- </template>
957
- </div>
958
-
959
- <div class="sticky bottom-0 bg-background px-6 pb-4 pt-2">
960
- <Alert v-if="state.validationErrors.length" variant="destructive" class="mb-3">
961
- <AlertTitle>Fix the highlighted fields</AlertTitle>
962
- <AlertDescription class="text-sm">
963
- <div v-for="(error, index) in state.validationErrors" :key="`${error}-${index}`">
964
- {{ error }}
965
- </div>
966
- </AlertDescription>
967
- </Alert>
968
- <SheetFooter class="flex justify-between">
969
- <edge-shad-button variant="destructive" class="text-white" @click="state.open = false">
970
- Cancel
971
- </edge-shad-button>
972
- <edge-shad-button class=" bg-slate-800 hover:bg-slate-400 w-full" @click="save">
973
- Save changes
974
- </edge-shad-button>
975
- </SheetFooter>
976
- </div>
977
- </edge-shad-form>
978
-
979
- <edge-shad-dialog v-model="state.aiDialogOpen">
980
- <DialogContent class="max-w-[640px]">
981
- <DialogHeader>
982
- <DialogTitle>Generate with AI</DialogTitle>
983
- <DialogDescription>
984
- Choose which fields the AI should fill and add any optional instructions.
985
- </DialogDescription>
986
- </DialogHeader>
987
- <div class="space-y-4">
988
- <edge-shad-textarea
989
- v-model="state.aiInstructions"
990
- name="aiInstructions"
991
- label="Instructions (Optional)"
992
- placeholder="Share tone, audience, and any details the AI should include."
993
- />
994
- <div class="space-y-2">
995
- <div class="flex items-center justify-between text-xs font-semibold uppercase tracking-wide text-muted-foreground">
996
- <span>Fields</span>
997
- <span>{{ selectedAiFieldIds.length }} selected</span>
998
- </div>
999
- <edge-shad-checkbox v-model="allAiFieldsSelected" name="aiSelectAll">
1000
- Select all fields
1001
- </edge-shad-checkbox>
1002
- <div v-if="aiFieldOptions.length" class="grid gap-2 md:grid-cols-2">
1003
- <edge-shad-checkbox
1004
- v-for="option in aiFieldOptions"
1005
- :key="option.id"
1006
- v-model="state.aiSelectedFields[option.id]"
1007
- :name="`ai-field-${option.id}`"
1008
- >
1009
- {{ option.label }}
1010
- <span class="ml-2 text-xs text-muted-foreground">({{ option.type }})</span>
1455
+ <div class="space-y-2">
1456
+ <div class="flex items-center justify-between text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1457
+ <span>Fields</span>
1458
+ <span>{{ selectedAiFieldIds.length }} selected</span>
1459
+ </div>
1460
+ <edge-shad-checkbox v-model="allAiFieldsSelected" name="aiSelectAll">
1461
+ Select all fields
1011
1462
  </edge-shad-checkbox>
1463
+ <div v-if="aiFieldOptions.length" class="grid gap-2 md:grid-cols-2">
1464
+ <edge-shad-checkbox
1465
+ v-for="option in aiFieldOptions"
1466
+ :key="option.id"
1467
+ v-model="state.aiSelectedFields[option.id]"
1468
+ :name="`ai-field-${option.id}`"
1469
+ >
1470
+ {{ option.label }}
1471
+ <span class="ml-2 text-xs text-muted-foreground">({{ option.type }})</span>
1472
+ </edge-shad-checkbox>
1473
+ </div>
1474
+ <Alert v-else variant="info">
1475
+ <AlertTitle>No editable fields</AlertTitle>
1476
+ <AlertDescription class="text-sm">
1477
+ Add editable fields to this block to enable AI generation.
1478
+ </AlertDescription>
1479
+ </Alert>
1012
1480
  </div>
1013
- <Alert v-else variant="info">
1014
- <AlertTitle>No editable fields</AlertTitle>
1481
+ <Alert v-if="state.aiError" variant="destructive">
1482
+ <AlertTitle>AI generation failed</AlertTitle>
1015
1483
  <AlertDescription class="text-sm">
1016
- Add editable fields to this block to enable AI generation.
1484
+ {{ state.aiError }}
1017
1485
  </AlertDescription>
1018
1486
  </Alert>
1019
1487
  </div>
1020
- <Alert v-if="state.aiError" variant="destructive">
1021
- <AlertTitle>AI generation failed</AlertTitle>
1022
- <AlertDescription class="text-sm">
1023
- {{ state.aiError }}
1024
- </AlertDescription>
1025
- </Alert>
1026
- </div>
1027
- <DialogFooter class="pt-4 flex justify-between">
1028
- <edge-shad-button type="button" variant="destructive" class="text-white" @click="closeAiDialog">
1029
- Cancel
1030
- </edge-shad-button>
1031
- <edge-shad-button
1032
- type="button"
1033
- class="w-full"
1034
- :disabled="state.aiGenerating || !selectedAiFieldIds.length"
1035
- @click="generateWithAi"
1036
- >
1037
- <Loader2 v-if="state.aiGenerating" class="w-4 h-4 mr-2 animate-spin" />
1038
- Generate
1039
- </edge-shad-button>
1040
- </DialogFooter>
1041
- </DialogContent>
1042
- </edge-shad-dialog>
1488
+ <DialogFooter class="pt-4 flex justify-between">
1489
+ <edge-shad-button type="button" variant="destructive" class="text-white" @click="closeAiDialog">
1490
+ Cancel
1491
+ </edge-shad-button>
1492
+ <edge-shad-button
1493
+ type="button"
1494
+ class="w-full"
1495
+ :disabled="state.aiGenerating || !selectedAiFieldIds.length"
1496
+ @click="generateWithAi"
1497
+ >
1498
+ <Loader2 v-if="state.aiGenerating" class="w-4 h-4 mr-2 animate-spin" />
1499
+ Generate
1500
+ </edge-shad-button>
1501
+ </DialogFooter>
1502
+ </DialogContent>
1503
+ </edge-shad-dialog>
1504
+ </template>
1043
1505
  </edge-cms-block-sheet-content>
1044
1506
  </Sheet>
1045
1507
  </div>