@edgedev/create-edge-app 1.1.25 → 1.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +55 -20
  2. package/{agent.md → agents.md} +2 -0
  3. package/bin/cli.js +6 -6
  4. package/edge/components/auth/login.vue +384 -0
  5. package/edge/components/auth/register.vue +396 -0
  6. package/edge/components/auth.vue +108 -0
  7. package/edge/components/autoFileUpload.vue +215 -0
  8. package/edge/components/billing.vue +8 -0
  9. package/edge/components/buttonDivider.vue +14 -0
  10. package/edge/components/chip.vue +34 -0
  11. package/edge/components/clipboardButton.vue +42 -0
  12. package/edge/components/cms/block.vue +529 -0
  13. package/edge/components/cms/blockApi.vue +212 -0
  14. package/edge/components/cms/blockEditor.vue +725 -0
  15. package/edge/components/cms/blockInput.vue +66 -0
  16. package/edge/components/cms/blockPicker.vue +486 -0
  17. package/edge/components/cms/blockRender.vue +78 -0
  18. package/edge/components/cms/blockSheetContent.vue +28 -0
  19. package/edge/components/cms/codeEditor.vue +466 -0
  20. package/edge/components/cms/fontUpload.vue +327 -0
  21. package/edge/components/cms/htmlContent.vue +807 -0
  22. package/edge/components/cms/init_blocks/api_with_subarrays.html +17 -0
  23. package/edge/components/cms/init_blocks/array_with_collection.html +7 -0
  24. package/edge/components/cms/init_blocks/array_with_objects.html +7 -0
  25. package/edge/components/cms/init_blocks/carousel.html +103 -0
  26. package/edge/components/cms/init_blocks/contact_us.html +69 -0
  27. package/edge/components/cms/init_blocks/content_with_left_image.html +27 -0
  28. package/edge/components/cms/init_blocks/footer.html +24 -0
  29. package/edge/components/cms/init_blocks/header_divider.html +7 -0
  30. package/edge/components/cms/init_blocks/hero.html +35 -0
  31. package/edge/components/cms/init_blocks/hero_carousel.html +52 -0
  32. package/edge/components/cms/init_blocks/newsletter.html +117 -0
  33. package/edge/components/cms/init_blocks/post_content.html +7 -0
  34. package/edge/components/cms/init_blocks/post_title_header.html +21 -0
  35. package/edge/components/cms/init_blocks/posts_list.html +20 -0
  36. package/edge/components/cms/init_blocks/properties_showcase.html +100 -0
  37. package/edge/components/cms/init_blocks/property_carousel.html +59 -0
  38. package/edge/components/cms/init_blocks/property_detail.html +112 -0
  39. package/edge/components/cms/init_blocks/property_detail_header.html +34 -0
  40. package/edge/components/cms/init_blocks/property_results.html +137 -0
  41. package/edge/components/cms/init_blocks/property_search.html +75 -0
  42. package/edge/components/cms/init_blocks/simple_array.html +7 -0
  43. package/edge/components/cms/mediaCard.vue +116 -0
  44. package/edge/components/cms/mediaManager.vue +386 -0
  45. package/edge/components/cms/menu.vue +1103 -0
  46. package/edge/components/cms/optionsSelect.vue +107 -0
  47. package/edge/components/cms/page.vue +1785 -0
  48. package/edge/components/cms/posts.vue +1083 -0
  49. package/edge/components/cms/site.vue +1475 -0
  50. package/edge/components/cms/themeDefaultMenu.vue +548 -0
  51. package/edge/components/cms/themeEditor.vue +429 -0
  52. package/edge/components/dashboard.vue +776 -0
  53. package/edge/components/editor.vue +671 -0
  54. package/edge/components/fileTree.vue +72 -0
  55. package/edge/components/files.vue +89 -0
  56. package/edge/components/formSubtypes/myOrgs.vue +214 -0
  57. package/edge/components/formSubtypes/users.vue +336 -0
  58. package/edge/components/functionChips.vue +57 -0
  59. package/edge/components/gError.vue +98 -0
  60. package/edge/components/gHelper.vue +67 -0
  61. package/edge/components/gInput.vue +1331 -0
  62. package/edge/components/loggingIn.vue +41 -0
  63. package/edge/components/menu.vue +137 -0
  64. package/edge/components/menuContent.vue +132 -0
  65. package/edge/components/myAccount.vue +317 -0
  66. package/edge/components/myOrganizations.vue +75 -0
  67. package/edge/components/myProfile.vue +122 -0
  68. package/edge/components/orgSwitcher.vue +25 -0
  69. package/edge/components/organizationMembers.vue +522 -0
  70. package/edge/components/organizationSettings.vue +271 -0
  71. package/edge/components/shad/breadcrumbs.vue +35 -0
  72. package/edge/components/shad/button.vue +43 -0
  73. package/edge/components/shad/checkbox.vue +73 -0
  74. package/edge/components/shad/combobox.vue +238 -0
  75. package/edge/components/shad/datepicker.vue +184 -0
  76. package/edge/components/shad/dialog.vue +32 -0
  77. package/edge/components/shad/dropdownMenu.vue +54 -0
  78. package/edge/components/shad/dropdownMenuItem.vue +21 -0
  79. package/edge/components/shad/form.vue +59 -0
  80. package/edge/components/shad/html.vue +877 -0
  81. package/edge/components/shad/input.vue +139 -0
  82. package/edge/components/shad/number.vue +109 -0
  83. package/edge/components/shad/select.vue +151 -0
  84. package/edge/components/shad/selectTags.vue +278 -0
  85. package/edge/components/shad/switch.vue +67 -0
  86. package/edge/components/shad/tags.vue +137 -0
  87. package/edge/components/shad/textarea.vue +102 -0
  88. package/edge/components/shad/typeMoney.vue +167 -0
  89. package/edge/components/sideBar.vue +288 -0
  90. package/edge/components/sideBarContent.vue +268 -0
  91. package/edge/components/sidebarProvider.vue +33 -0
  92. package/edge/components/tooltip.vue +16 -0
  93. package/edge/components/userMenu.vue +148 -0
  94. package/edge/components/v/alert.vue +59 -0
  95. package/edge/components/v/alertTitle.vue +18 -0
  96. package/edge/components/v/card.vue +53 -0
  97. package/edge/components/v/cardActions.vue +18 -0
  98. package/edge/components/v/cardText.vue +18 -0
  99. package/edge/components/v/cardTitle.vue +20 -0
  100. package/edge/components/v/col.vue +56 -0
  101. package/edge/components/v/list.vue +46 -0
  102. package/edge/components/v/listItem.vue +26 -0
  103. package/edge/components/v/listItemTitle.vue +18 -0
  104. package/edge/components/v/row.vue +42 -0
  105. package/edge/components/v/toolbar.vue +24 -0
  106. package/edge/composables/global.ts +519 -0
  107. package/edge-pull.sh +2 -0
  108. package/edge-push.sh +1 -0
  109. package/edge-status.sh +14 -0
  110. package/package.json +1 -1
  111. package/edge-components-install.sh +0 -1
@@ -0,0 +1,725 @@
1
+ <script setup>
2
+ import { Maximize2, Monitor, Smartphone, Tablet } from 'lucide-vue-next'
3
+ import { toTypedSchema } from '@vee-validate/zod'
4
+ import * as z from 'zod'
5
+ const props = defineProps({
6
+ blockId: {
7
+ type: String,
8
+ required: true,
9
+ },
10
+ })
11
+
12
+ const emit = defineEmits(['head'])
13
+
14
+ const edgeFirebase = inject('edgeFirebase')
15
+
16
+ const route = useRoute()
17
+
18
+ const state = reactive({
19
+ filter: '',
20
+ newDocs: {
21
+ blocks: {
22
+ name: { value: '' },
23
+ content: { value: '' },
24
+ tags: { value: [] },
25
+ themes: { value: [] },
26
+ synced: { value: false },
27
+ version: 1,
28
+ },
29
+ },
30
+ mounted: false,
31
+ workingDoc: {},
32
+ loading: false,
33
+ jsonEditorOpen: false,
34
+ jsonEditorContent: '',
35
+ jsonEditorError: '',
36
+ editingContext: null,
37
+ renderSite: '',
38
+ initialBlocksSeeded: false,
39
+ seedingInitialBlocks: false,
40
+ previewViewport: 'full',
41
+ })
42
+
43
+ const blockSchema = toTypedSchema(z.object({
44
+ name: z.string({
45
+ required_error: 'Name is required',
46
+ }).min(1, { message: 'Name is required' }),
47
+ }))
48
+
49
+ const previewViewportOptions = [
50
+ { id: 'full', label: 'Wild Width', width: '100%', icon: Maximize2 },
51
+ { id: 'large', label: 'Large Screen', width: '1280px', icon: Monitor },
52
+ { id: 'medium', label: 'Medium', width: '992px', icon: Tablet },
53
+ { id: 'mobile', label: 'Mobile', width: '420px', icon: Smartphone },
54
+ ]
55
+
56
+ const selectedPreviewViewport = computed(() => previewViewportOptions.find(option => option.id === state.previewViewport) || previewViewportOptions[0])
57
+
58
+ const previewViewportStyle = computed(() => {
59
+ const selected = selectedPreviewViewport.value
60
+ if (!selected || selected.id === 'full')
61
+ return { maxWidth: '100%' }
62
+ return {
63
+ width: '100%',
64
+ maxWidth: selected.width,
65
+ marginLeft: 'auto',
66
+ marginRight: 'auto',
67
+ }
68
+ })
69
+
70
+ const setPreviewViewport = (viewportId) => {
71
+ state.previewViewport = viewportId
72
+ }
73
+
74
+ const previewViewportMode = computed(() => {
75
+ if (state.previewViewport === 'full')
76
+ return 'auto'
77
+ return state.previewViewport
78
+ })
79
+
80
+ onMounted(() => {
81
+ // state.mounted = true
82
+ })
83
+
84
+ const PLACEHOLDERS = {
85
+ text: 'Lorem ipsum dolor sit amet.',
86
+ textarea: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
87
+ richtext: '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>',
88
+ arrayItem: [
89
+ 'Lorem ipsum dolor sit amet.',
90
+ 'Consectetur adipiscing elit.',
91
+ 'Sed do eiusmod tempor incididunt.',
92
+ ],
93
+ image: '/images/filler.png',
94
+ }
95
+
96
+ const contentEditorRef = ref(null)
97
+
98
+ const BLOCK_CONTENT_SNIPPETS = [
99
+ {
100
+ label: 'Text Field',
101
+ snippet: '{{{#text {"field": "fieldName", "value": "" }}}}',
102
+ description: 'Simple text field placeholder',
103
+ },
104
+ {
105
+ label: 'Text Area',
106
+ snippet: '{{{#textarea {"field": "fieldName", "value": "" }}}}',
107
+ description: 'Textarea field placeholder',
108
+ },
109
+ {
110
+ label: 'Rich Text',
111
+ snippet: '{{{#richtext {"field": "content", "value": "" }}}}',
112
+ description: 'Rich text field placeholder',
113
+ },
114
+ {
115
+ label: 'Image',
116
+ snippet: '{{{#image {"field": "imageField", "value": "", "tags": ["Backgrounds"] }}}}',
117
+ description: 'Image field placeholder',
118
+ },
119
+ {
120
+ label: 'Array (Basic)',
121
+ snippet: `{{{#array {"field": "items", "value": [] }}}}
122
+ <!-- iterate with {{item}} -->
123
+ {{{/array}}}`,
124
+ description: 'Basic repeating array block',
125
+ },
126
+ {
127
+ label: 'Array (API)',
128
+ snippet: `{{{#array {"field":"List","schema":{"listing_price":"money","square_feet":"number","acres":"number"},"api":"https://api.clearwaterproperties.com/api/front/properties","apiField":"data","apiQuery":"?limit=20&filter_scope[agent][]=mt_nmar-mt.545000478","queryOptions":[{"field":"sort","optionsKey":"label","optionsValue":"value","options":[{"label":"Highest Price","value":"listing_price"},{"label":"Lowest Price","value":"-listing_price"},{"label":"Newest","value":"-list_date"}]},{"field":"filter_scope[agent][]","title":"Agent","optionsKey":"name","optionsValue":"mls.primary","options":"users"}],"limit":10,"value":[]}}}}
129
+ <!-- iterate with {{item}} -->
130
+ {{{/array}}}`,
131
+ description: 'Array pulling data from an API',
132
+ },
133
+ {
134
+ label: 'Array (Collection)',
135
+ snippet: `{{{#array {"field":"list","schema":[{"field":"name","value":"text"}],"collection":{"path":"users","query":[{"field":"name","operator":">","value":""}],"order":[{"field":"name","direction":"asc"}]},"queryOptions":[{"field":"office_id","title":"Office","optionsKey":"label","optionsValue":"value","options":[{"label":"Office 1","value":"7"},{"label":"Office 2","value":"39"},{"label":"Office 3","value":"32"}]},{"field":"userId","title":"Agent","optionsKey":"name","optionsValue":"userId","options":"users"}],"limit":100,"value":[]}}}}
136
+ <h1 class="text-4xl">
137
+ {{item.name}}
138
+ </h1>
139
+ {{{/array}}}`,
140
+ description: 'Array pulling data from a collection',
141
+ },
142
+ {
143
+ label: 'Subarray',
144
+ snippet: `{{{#subarray:child {"field": "item.children", "limit": 0 }}}}
145
+ {{child}}
146
+ {{{/subarray}}}`,
147
+ description: 'Nested array inside an array item',
148
+ },
149
+ {
150
+ label: 'If / Else',
151
+ snippet: `{{{#if {"cond": "condition" }}}}
152
+ <!-- content when condition is true -->
153
+ {{{#else}}}
154
+ <!-- content when condition is false -->
155
+ {{{/if}}}`,
156
+ description: 'Conditional block with optional else',
157
+ },
158
+ ]
159
+
160
+ function insertBlockContentSnippet(snippet) {
161
+ if (!snippet)
162
+ return
163
+ const editor = contentEditorRef.value
164
+ if (!editor || typeof editor.insertSnippet !== 'function') {
165
+ console.warn('Block content editor is not ready for snippet insertion')
166
+ return
167
+ }
168
+ editor.insertSnippet(snippet)
169
+ }
170
+
171
+ function normalizeConfigLiteral(str) {
172
+ // ensure keys are quoted: { title: "x", field: "y" } -> { "title": "x", "field": "y" }
173
+ return str
174
+ .replace(/(\{|,)\s*([A-Za-z_][\w-]*)\s*:/g, '$1"$2":')
175
+ // allow single quotes too
176
+ .replace(/'/g, '"')
177
+ }
178
+
179
+ function safeParseConfig(raw) {
180
+ try {
181
+ return JSON.parse(normalizeConfigLiteral(raw))
182
+ }
183
+ catch {
184
+ return null
185
+ }
186
+ }
187
+
188
+ // --- Robust tag parsing: supports nested objects/arrays in the config ---
189
+ // Matches `{{{#<type> { ... }}}}` and extracts a *balanced* `{ ... }` blob.
190
+ const TAG_START_RE = /\{\{\{\#([A-Za-z0-9_-]+)\s*\{/g
191
+
192
+ function findMatchingBrace(str, startIdx) {
193
+ // startIdx points at the opening '{' of the config
194
+ let depth = 0
195
+ let inString = false
196
+ let quote = null
197
+ let escape = false
198
+ for (let i = startIdx; i < str.length; i++) {
199
+ const ch = str[i]
200
+ if (inString) {
201
+ if (escape) {
202
+ escape = false
203
+ continue
204
+ }
205
+ if (ch === '\\') {
206
+ escape = true
207
+ continue
208
+ }
209
+ if (ch === quote) {
210
+ inString = false
211
+ quote = null
212
+ }
213
+ continue
214
+ }
215
+ if (ch === '"' || ch === '\'') {
216
+ inString = true
217
+ quote = ch
218
+ continue
219
+ }
220
+ if (ch === '{')
221
+ depth++
222
+ else if (ch === '}') {
223
+ depth--
224
+ if (depth === 0)
225
+ return i
226
+ }
227
+ }
228
+ return -1
229
+ }
230
+
231
+ function* iterateTags(html) {
232
+ TAG_START_RE.lastIndex = 0
233
+ for (;;) {
234
+ const m = TAG_START_RE.exec(html)
235
+ if (!m)
236
+ break
237
+
238
+ const type = m[1]
239
+ const configStart = TAG_START_RE.lastIndex - 1
240
+ if (configStart < 0 || html[configStart] !== '{')
241
+ continue
242
+
243
+ const configEnd = findMatchingBrace(html, configStart)
244
+ if (configEnd === -1)
245
+ continue
246
+
247
+ const rawCfg = html.slice(configStart, configEnd + 1)
248
+ const tagStart = m.index
249
+ const closeTriple = html.indexOf('}}}', configEnd)
250
+ const tagEnd = closeTriple !== -1 ? closeTriple + 3 : configEnd + 1
251
+
252
+ yield { type, rawCfg, tagStart, tagEnd, configStart, configEnd }
253
+
254
+ TAG_START_RE.lastIndex = tagEnd
255
+ }
256
+ }
257
+
258
+ function findTagAtOffset(html, offset) {
259
+ for (const tag of iterateTags(html)) {
260
+ if (offset >= tag.tagStart && offset <= tag.tagEnd)
261
+ return tag
262
+ }
263
+ return null
264
+ }
265
+
266
+ const blockModel = (html) => {
267
+ const values = {}
268
+ const meta = {}
269
+
270
+ if (!html)
271
+ return { values, meta }
272
+
273
+ for (const { type, rawCfg } of iterateTags(html)) {
274
+ const cfg = safeParseConfig(rawCfg)
275
+ if (!cfg || !cfg.field)
276
+ continue
277
+
278
+ const field = String(cfg.field)
279
+ const title = cfg.title != null ? String(cfg.title) : ''
280
+
281
+ const { value: _omitValue, field: _omitField, ...rest } = cfg
282
+ meta[field] = { type, ...rest, title }
283
+
284
+ let val = cfg.value
285
+
286
+ if (type === 'image') {
287
+ val = !val ? PLACEHOLDERS.image : String(val)
288
+ }
289
+ else if (type === 'text') {
290
+ val = !val ? PLACEHOLDERS.text : String(val)
291
+ }
292
+ else if (type === 'array') {
293
+ if (meta[field]?.limit > 0) {
294
+ val = Array(meta[field].limit).fill('placeholder')
295
+ }
296
+ else {
297
+ if (Array.isArray(val)) {
298
+ console.log('Array value detected for field:', field, 'with value:', val)
299
+ if (val.length === 0) {
300
+ val = PLACEHOLDERS.arrayItem
301
+ }
302
+ }
303
+ else {
304
+ val = PLACEHOLDERS.arrayItem
305
+ }
306
+ }
307
+ }
308
+ else if (type === 'textarea') {
309
+ val = !val ? PLACEHOLDERS.textarea : String(val)
310
+ }
311
+ else if (type === 'richtext') {
312
+ val = !val ? PLACEHOLDERS.richtext : String(val)
313
+ }
314
+
315
+ values[field] = val
316
+ }
317
+ return { values, meta }
318
+ }
319
+
320
+ function resetJsonEditorState() {
321
+ state.jsonEditorContent = ''
322
+ state.jsonEditorError = ''
323
+ state.editingContext = null
324
+ }
325
+
326
+ function closeJsonEditor() {
327
+ state.jsonEditorOpen = false
328
+ resetJsonEditorState()
329
+ }
330
+
331
+ function handleEditorLineClick(payload, workingDoc) {
332
+ if (!workingDoc || !workingDoc.content)
333
+ return
334
+
335
+ const offset = typeof payload?.offset === 'number' ? payload.offset : null
336
+ if (offset == null)
337
+ return
338
+
339
+ const tag = findTagAtOffset(workingDoc.content, offset)
340
+ if (!tag)
341
+ return
342
+
343
+ const parsedCfg = safeParseConfig(tag.rawCfg)
344
+ state.jsonEditorError = ''
345
+ state.jsonEditorContent = parsedCfg ? JSON.stringify(parsedCfg, null, 2) : tag.rawCfg
346
+ state.jsonEditorOpen = true
347
+ state.editingContext = {
348
+ type: tag.type,
349
+ field: parsedCfg?.field != null ? String(parsedCfg.field) : null,
350
+ workingDoc,
351
+ originalTag: workingDoc.content.slice(tag.tagStart, tag.tagEnd),
352
+ configStartOffset: tag.configStart - tag.tagStart,
353
+ configEndOffset: tag.configEnd - tag.tagStart,
354
+ }
355
+ }
356
+
357
+ function handleJsonEditorSave() {
358
+ if (!state.editingContext)
359
+ return
360
+
361
+ let parsed
362
+ try {
363
+ parsed = JSON.parse(state.jsonEditorContent)
364
+ }
365
+ catch (error) {
366
+ state.jsonEditorError = `Unable to parse JSON: ${error.message}`
367
+ return
368
+ }
369
+
370
+ const serialized = JSON.stringify(parsed)
371
+ const { workingDoc, type, field, originalTag, configStartOffset, configEndOffset } = state.editingContext
372
+ const content = workingDoc?.content ?? ''
373
+ if (!content) {
374
+ state.jsonEditorError = 'Block content is empty.'
375
+ return
376
+ }
377
+
378
+ let target = null
379
+ for (const tag of iterateTags(content)) {
380
+ if (tag.type !== type)
381
+ continue
382
+ if (!field) {
383
+ target = tag
384
+ break
385
+ }
386
+ const cfg = safeParseConfig(tag.rawCfg)
387
+ if (cfg && String(cfg.field) === field) {
388
+ target = tag
389
+ break
390
+ }
391
+ }
392
+
393
+ if (!target && originalTag) {
394
+ const idx = content.indexOf(originalTag)
395
+ if (idx !== -1) {
396
+ const startOffset = typeof configStartOffset === 'number' ? configStartOffset : originalTag.indexOf('{')
397
+ const endOffset = typeof configEndOffset === 'number' ? configEndOffset : originalTag.lastIndexOf('}')
398
+ if (startOffset != null && endOffset != null && startOffset >= 0 && endOffset >= startOffset) {
399
+ target = {
400
+ configStart: idx + startOffset,
401
+ configEnd: idx + endOffset,
402
+ }
403
+ }
404
+ }
405
+ }
406
+
407
+ if (!target) {
408
+ state.jsonEditorError = 'Unable to locate the original block field in the current content.'
409
+ return
410
+ }
411
+
412
+ const prefix = content.slice(0, target.configStart)
413
+ const suffix = content.slice(target.configEnd + 1)
414
+ workingDoc.content = `${prefix}${serialized}${suffix}`
415
+
416
+ closeJsonEditor()
417
+ }
418
+
419
+ const theme = computed(() => {
420
+ const theme = edgeGlobal.edgeState.blockEditorTheme || ''
421
+ let themeContents = null
422
+ if (theme) {
423
+ themeContents = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[theme]?.theme || null
424
+ }
425
+ if (themeContents) {
426
+ return JSON.parse(themeContents)
427
+ }
428
+ return null
429
+ })
430
+
431
+ const headObject = computed(() => {
432
+ const theme = edgeGlobal.edgeState.blockEditorTheme || ''
433
+ try {
434
+ return JSON.parse(edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`]?.[theme]?.headJSON || '{}')
435
+ }
436
+ catch (e) {
437
+ return {}
438
+ }
439
+ })
440
+
441
+ watch(headObject, (newHeadElements) => {
442
+ emit('head', newHeadElements)
443
+ }, { immediate: true, deep: true })
444
+
445
+ const editorDocUpdates = (workingDoc) => {
446
+ state.workingDoc = blockModel(workingDoc.content)
447
+ console.log('Editor workingDoc update:', state.workingDoc)
448
+ }
449
+
450
+ onBeforeMount(async () => {
451
+ console.log('Block Editor mounting - starting snapshots if needed')
452
+ if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`]) {
453
+ await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`)
454
+ }
455
+ if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/sites`]) {
456
+ console.log('Starting sites snapshot for block editor')
457
+ await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/sites`)
458
+ }
459
+ else {
460
+ console.log('Themes and Sites snapshots already started')
461
+ }
462
+ state.mounted = true
463
+ })
464
+
465
+ const themes = computed(() => {
466
+ return Object.values(edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/themes`] || {})
467
+ })
468
+
469
+ watch (themes, async (newThemes) => {
470
+ state.loading = true
471
+ if (!edgeGlobal.edgeState.blockEditorTheme && newThemes.length > 0) {
472
+ edgeGlobal.edgeState.blockEditorTheme = newThemes[0].docId
473
+ }
474
+ await nextTick()
475
+ state.loading = false
476
+ }, { immediate: true, deep: true })
477
+
478
+ watch(() => state.jsonEditorOpen, (open) => {
479
+ if (!open)
480
+ resetJsonEditorState()
481
+ })
482
+ const sites = computed(() => {
483
+ return Object.values(edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/sites`] || {})
484
+ })
485
+
486
+ watch (sites, async (newSites) => {
487
+ state.loading = true
488
+ if (!edgeGlobal.edgeState.blockEditorSite && newSites.length > 0) {
489
+ edgeGlobal.edgeState.blockEditorSite = newSites[0].docId
490
+ }
491
+ await nextTick()
492
+ state.loading = false
493
+ }, { immediate: true, deep: true })
494
+
495
+ const blocks = computed(() => {
496
+ return edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/blocks`] || null
497
+ })
498
+
499
+ const getTagsFromBlocks = computed(() => {
500
+ const tagsSet = new Set()
501
+
502
+ Object.values(blocks.value || {}).forEach((block) => {
503
+ if (block.tags && Array.isArray(block.tags)) {
504
+ block.tags.forEach(tag => tagsSet.add(tag))
505
+ }
506
+ })
507
+
508
+ // Convert to array of objects
509
+ const tagsArray = Array.from(tagsSet).map(tag => ({ name: tag, title: tag }))
510
+
511
+ // Sort alphabetically
512
+ tagsArray.sort((a, b) => a.title.localeCompare(b.title))
513
+
514
+ // Remove "Quick Picks" if it exists
515
+ const filtered = tagsArray.filter(tag => tag.name !== 'Quick Picks')
516
+
517
+ // Always prepend it
518
+ return [{ name: 'Quick Picks', title: 'Quick Picks' }, ...filtered]
519
+ })
520
+ </script>
521
+
522
+ <template>
523
+ <div
524
+ v-if="edgeGlobal.edgeState.organizationDocPath && state.mounted"
525
+ >
526
+ <edge-editor
527
+ collection="blocks"
528
+ :doc-id="props.blockId"
529
+ :schema="blockSchema"
530
+ :new-doc-schema="state.newDocs.blocks"
531
+ class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none"
532
+ :show-footer="false"
533
+ :no-close-after-save="true"
534
+ :working-doc-overrides="state.workingDoc"
535
+ @working-doc="editorDocUpdates"
536
+ >
537
+ <template #header-start="slotProps">
538
+ <FilePenLine class="mr-2" />
539
+ {{ slotProps.title }}
540
+ </template>
541
+ <template #header-center>
542
+ <div class="w-full flex gap-1 px-4">
543
+ <div class="w-1/2">
544
+ <edge-shad-select
545
+ v-if="!state.loading"
546
+ v-model="edgeGlobal.edgeState.blockEditorTheme"
547
+ label="Theme Viewer Select"
548
+ name="theme"
549
+ :items="themes.map(t => ({ title: t.name, name: t.docId }))"
550
+ placeholder="Theme Viewer Select"
551
+ class="w-full"
552
+ />
553
+ </div>
554
+ <div class="w-1/2">
555
+ <edge-shad-select
556
+ v-if="!state.loading"
557
+ v-model="edgeGlobal.edgeState.blockEditorSite"
558
+ label="Site"
559
+ name="site"
560
+ :items="sites.map(s => ({ title: s.name, name: s.docId }))"
561
+ placeholder="Select Site"
562
+ class="w-full"
563
+ />
564
+ </div>
565
+ </div>
566
+ </template>
567
+
568
+ <template #main="slotProps">
569
+ <div class="pt-4">
570
+ <div class="flex w-full gap-2">
571
+ <div class="flex-auto">
572
+ <edge-shad-input
573
+ v-model="slotProps.workingDoc.name"
574
+ label="Block Name"
575
+ class="flex-auto"
576
+ name="name"
577
+ />
578
+ </div>
579
+ <div class="flex-auto">
580
+ <edge-shad-select-tags
581
+ v-model="slotProps.workingDoc.tags"
582
+ :items="getTagsFromBlocks"
583
+ name="tags"
584
+ placeholder="Select tags"
585
+ label="Tags"
586
+ :allow-additions="true"
587
+ class="w-full max-w-[800px] mx-auto mb-5 text-black"
588
+ />
589
+ </div>
590
+ <div class="flex-auto">
591
+ <edge-shad-select
592
+ v-model="slotProps.workingDoc.themes"
593
+ label="Allowed Themes"
594
+ name="themes"
595
+ :multiple="true"
596
+ :items="themes.map(t => ({ title: t.name, name: t.docId }))"
597
+ placeholder="Allowed Themes"
598
+ class="flex-auto"
599
+ />
600
+ </div>
601
+ <div class="flex-auto pt-2">
602
+ <edge-shad-checkbox
603
+ v-model="slotProps.workingDoc.synced"
604
+ name="synced"
605
+ label="Synced Block"
606
+ >
607
+ Synced Block
608
+ </edge-shad-checkbox>
609
+ </div>
610
+ </div>
611
+ <div class="flex gap-4">
612
+ <div class="w-1/2">
613
+ <div class="flex gap-2">
614
+ <div class="w-2/12 mb-3 rounded-md border border-slate-200 bg-white/80 p-3 shadow-sm shadow-slate-200/60 dark:border-slate-800 dark:bg-slate-900/60">
615
+ <div class="text-xs font-semibold uppercase tracking-wide text-slate-600 dark:text-slate-300">
616
+ Dynamic Content
617
+ </div>
618
+ <div class="mt-2 flex flex-wrap gap-2">
619
+ <edge-tooltip
620
+ v-for="snippet in BLOCK_CONTENT_SNIPPETS"
621
+ :key="snippet.label"
622
+ >
623
+ <edge-shad-button
624
+ size="sm"
625
+ variant="outline"
626
+ class="text-xs w-full"
627
+ @click="insertBlockContentSnippet(snippet.snippet)"
628
+ >
629
+ {{ snippet.label }}
630
+ </edge-shad-button>
631
+ <template #content>
632
+ <pre class="max-w-[320px] whitespace-pre-wrap break-words text-left text-xs font-mono leading-tight">{{ snippet.snippet }}</pre>
633
+ </template>
634
+ </edge-tooltip>
635
+ </div>
636
+ </div>
637
+ <div class="w-10/12">
638
+ <edge-cms-code-editor
639
+ ref="contentEditorRef"
640
+ v-model="slotProps.workingDoc.content"
641
+ title="Block Content"
642
+ language="html"
643
+ name="content"
644
+ height="calc(100vh - 300px)"
645
+ class="mb-4 flex-1"
646
+ @line-click="payload => handleEditorLineClick(payload, slotProps.workingDoc)"
647
+ />
648
+ </div>
649
+ </div>
650
+ </div>
651
+ <div class="w-1/2 space-y-2">
652
+ <div class="flex items-center justify-between">
653
+ <span class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Viewport</span>
654
+ <div class="flex items-center gap-1">
655
+ <edge-shad-button
656
+ v-for="option in previewViewportOptions"
657
+ :key="option.id"
658
+ type="button"
659
+ size="icon"
660
+ variant="ghost"
661
+ class="h-[28px] w-[28px] text-xs border transition-colors"
662
+ :class="state.previewViewport === option.id ? 'bg-primary text-primary-foreground border-primary shadow-sm' : 'bg-muted text-foreground border-border hover:bg-muted/80'"
663
+ @click="setPreviewViewport(option.id)"
664
+ >
665
+ <component :is="option.icon" class="w-3.5 h-3.5" />
666
+ </edge-shad-button>
667
+ </div>
668
+ </div>
669
+ <div
670
+ class="w-full mx-auto bg-card border border-border rounded-lg shadow-sm md:shadow-md"
671
+ :style="previewViewportStyle"
672
+ >
673
+ <edge-cms-block-picker
674
+ :site-id="edgeGlobal.edgeState.blockEditorSite"
675
+ :theme="theme"
676
+ :block-override="{ content: slotProps.workingDoc.content, values: state.workingDoc.values, meta: state.workingDoc.meta }"
677
+ :viewport-mode="previewViewportMode"
678
+ />
679
+ </div>
680
+ </div>
681
+ </div>
682
+ </div>
683
+ </template>
684
+ </edge-editor>
685
+ <Sheet
686
+ v-model:open="state.jsonEditorOpen"
687
+ >
688
+ <SheetContent side="left" class="w-full md:w-1/2 max-w-none sm:max-w-none max-w-2xl">
689
+ <SheetHeader>
690
+ <SheetTitle class="text-left">
691
+ Field Editor
692
+ </SheetTitle>
693
+ <SheetDescription v-if="state.jsonEditorError" class="text-left text-sm text-gray-500">
694
+ <Alert variant="destructive" class="mt-2">
695
+ <AlertCircle class="w-4 h-4" />
696
+ <AlertTitle>
697
+ JSON Error
698
+ </AlertTitle>
699
+ <AlertDescription>
700
+ {{ state.jsonEditorError }}
701
+ </AlertDescription>
702
+ </Alert>
703
+ </SheetDescription>
704
+ </SheetHeader>
705
+ <div :class="state.jsonEditorError ? 'h-[calc(100vh-200px)]' : 'h-[calc(100vh-120px)]'" class="p-6 space-y-4 overflow-y-auto">
706
+ <edge-cms-code-editor
707
+ v-model="state.jsonEditorContent"
708
+ title="Fields Configuration (JSON)"
709
+ language="json"
710
+ name="content"
711
+ height="calc(100vh - 200px)"
712
+ />
713
+ </div>
714
+ <SheetFooter class="pt-2 flex justify-between">
715
+ <edge-shad-button variant="destructive" class="text-white " @click="closeJsonEditor">
716
+ Cancel
717
+ </edge-shad-button>
718
+ <edge-shad-button class=" bg-slate-800 hover:bg-slate-400text-white w-full" @click="handleJsonEditorSave">
719
+ Save
720
+ </edge-shad-button>
721
+ </SheetFooter>
722
+ </SheetContent>
723
+ </Sheet>
724
+ </div>
725
+ </template>