@edgedev/create-edge-app 1.2.32 → 1.2.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/deploy.sh +77 -0
  2. package/edge/components/cms/block.vue +228 -18
  3. package/edge/components/cms/blockApi.vue +3 -3
  4. package/edge/components/cms/blockEditor.vue +374 -85
  5. package/edge/components/cms/blockPicker.vue +29 -3
  6. package/edge/components/cms/blockRender.vue +3 -3
  7. package/edge/components/cms/blocksManager.vue +755 -82
  8. package/edge/components/cms/codeEditor.vue +15 -6
  9. package/edge/components/cms/fontUpload.vue +318 -2
  10. package/edge/components/cms/htmlContent.vue +230 -89
  11. package/edge/components/cms/menu.vue +5 -4
  12. package/edge/components/cms/page.vue +750 -21
  13. package/edge/components/cms/site.vue +624 -84
  14. package/edge/components/cms/sitesManager.vue +5 -4
  15. package/edge/components/cms/themeEditor.vue +196 -162
  16. package/edge/components/editor.vue +5 -1
  17. package/edge/composables/global.ts +37 -5
  18. package/edge/composables/useCmsNewDocs.js +100 -0
  19. package/edge/composables/useEdgeCmsDialogPositionFix.js +19 -0
  20. package/edge/routes/cms/dashboard/blocks/[block].vue +5 -0
  21. package/edge/routes/cms/dashboard/blocks/index.vue +12 -1
  22. package/edge/routes/cms/dashboard/media/index.vue +5 -0
  23. package/edge/routes/cms/dashboard/sites/[site]/[[page]].vue +4 -0
  24. package/edge/routes/cms/dashboard/sites/[site].vue +4 -0
  25. package/edge/routes/cms/dashboard/sites/index.vue +4 -0
  26. package/edge/routes/cms/dashboard/templates/[page].vue +4 -0
  27. package/edge/routes/cms/dashboard/templates/index.vue +4 -0
  28. package/edge/routes/cms/dashboard/themes/[theme].vue +5 -0
  29. package/edge/routes/cms/dashboard/themes/index.vue +330 -1
  30. package/firebase.json +4 -0
  31. package/nuxt.config.ts +1 -1
  32. package/package.json +2 -2
  33. package/pages/app.vue +12 -12
@@ -60,6 +60,10 @@ const props = defineProps({
60
60
  type: Boolean,
61
61
  default: false,
62
62
  },
63
+ enableFormatting: {
64
+ type: Boolean,
65
+ default: true,
66
+ },
63
67
  })
64
68
 
65
69
  const emit = defineEmits(['update:modelValue', 'lineClick'])
@@ -253,7 +257,8 @@ const runChatGpt = async () => {
253
257
 
254
258
  const handleMount = (editor) => {
255
259
  editorInstanceRef.value = editor
256
- editorInstanceRef.value?.getAction('editor.action.formatDocument').run()
260
+ if (props.enableFormatting)
261
+ editorInstanceRef.value?.getAction('editor.action.formatDocument').run()
257
262
  editorDomNode = editor.getDomNode?.()
258
263
  if (editorDomNode)
259
264
  editorDomNode.addEventListener('keydown', stopEnterPropagation)
@@ -274,6 +279,9 @@ const handleMount = (editor) => {
274
279
  }
275
280
 
276
281
  const formatCode = () => {
282
+ if (!props.enableFormatting)
283
+ return
284
+
277
285
  const editorInstance = editorInstanceRef.value
278
286
  if (!editorInstance)
279
287
  return console.error('Editor instance not found')
@@ -411,7 +419,7 @@ onBeforeUnmount(() => {
411
419
  </div>
412
420
  </template>
413
421
  <template #end>
414
- <edge-tooltip>
422
+ <edge-tooltip v-if="props.enableFormatting">
415
423
  <edge-shad-button
416
424
  size="icon"
417
425
  class="bg-slate-400 w-8 h-8"
@@ -423,6 +431,7 @@ onBeforeUnmount(() => {
423
431
  Format Code
424
432
  </template>
425
433
  </edge-tooltip>
434
+ <slot name="end-actions" />
426
435
  <insert-menu v-if="props.siteVars && props.siteVars.length" :items="props.siteVars" var-prefix="siteVar" insert-type="vars" @value-sent="insertBlock">
427
436
  <Code class="w-4 h-4" /> Site Variables
428
437
  </insert-menu>
@@ -466,8 +475,8 @@ onBeforeUnmount(() => {
466
475
  :language="props.language"
467
476
  :options="{
468
477
  automaticLayout: true,
469
- formatOnType: true,
470
- formatOnPaste: true,
478
+ formatOnType: props.enableFormatting,
479
+ formatOnPaste: props.enableFormatting,
471
480
  }"
472
481
  style="height: calc(100vh - 120px)"
473
482
  @mount="handleMountDiff"
@@ -483,8 +492,8 @@ onBeforeUnmount(() => {
483
492
  :language="props.language"
484
493
  :options="{
485
494
  automaticLayout: true,
486
- formatOnType: true,
487
- formatOnPaste: true,
495
+ formatOnType: props.enableFormatting,
496
+ formatOnPaste: props.enableFormatting,
488
497
  }"
489
498
  @mount="handleMount"
490
499
  />
@@ -28,13 +28,17 @@ const state = reactive({
28
28
  fontFamily: '',
29
29
  fontDisplay: 'swap',
30
30
  fileMeta: {},
31
+ renamingDocId: null,
32
+ renameDrafts: {},
33
+ busyDocId: null,
34
+ loadingFonts: false,
31
35
  })
32
36
 
33
37
  const acceptList = ['.woff', '.woff2', '.ttf', '.otf', 'font/woff', 'font/woff2', 'application/font-woff', 'application/font-woff2', 'application/x-font-ttf', 'application/x-font-otf']
34
38
  const normalizedAccept = computed(() => acceptList.join(','))
35
- const collectionPath = computed(() => `${edgeGlobal.edgeState.organizationDocPath}/files`)
39
+ const collectionPath = computed(() => edgeGlobal.edgeState.organizationDocPath ? `${edgeGlobal.edgeState.organizationDocPath}/files` : '')
36
40
 
37
- const slugify = (value) => String(value || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, '')
41
+ const slugify = value => String(value || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, '')
38
42
 
39
43
  const parseHead = () => {
40
44
  try {
@@ -46,6 +50,15 @@ const parseHead = () => {
46
50
  }
47
51
  }
48
52
 
53
+ const parsedHead = computed(() => {
54
+ try {
55
+ return JSON.parse(headJson.value || '{}') || {}
56
+ }
57
+ catch {
58
+ return {}
59
+ }
60
+ })
61
+
49
62
  const setHeadJson = (head) => {
50
63
  if (!head || typeof head !== 'object')
51
64
  return
@@ -54,6 +67,208 @@ const setHeadJson = (head) => {
54
67
 
55
68
  const fileKey = file => file?.name || file?.file?.name || crypto.randomUUID()
56
69
 
70
+ const fileCollection = computed(() => edgeFirebase.data?.[collectionPath.value] || {})
71
+
72
+ const uploadedFonts = computed(() => {
73
+ return Object.values(fileCollection.value || {})
74
+ .filter((doc) => {
75
+ const meta = doc?.meta || {}
76
+ return !!meta.cmsFont && meta.themeId === props.themeId
77
+ })
78
+ .sort((a, b) => (b?.uploadTime || 0) - (a?.uploadTime || 0))
79
+ })
80
+
81
+ const fontLabel = (doc) => {
82
+ return doc?.name || doc?.fileName || doc?.docId || 'Untitled font'
83
+ }
84
+
85
+ const styleEntryContent = (entry = {}) => String(entry?.children || entry?.innerHTML || '')
86
+
87
+ const urlVariants = (url = '') => {
88
+ return [
89
+ String(url || ''),
90
+ encodeURI(String(url || '')),
91
+ decodeURI(String(url || '')),
92
+ ].filter(Boolean)
93
+ }
94
+
95
+ const entryMatchesUrl = (entry, url = '') => {
96
+ const content = styleEntryContent(entry)
97
+ if (!content)
98
+ return false
99
+ const variants = urlVariants(url)
100
+ return variants.some(v => content.includes(v))
101
+ }
102
+
103
+ const extractFontFamilyFromCss = (css = '') => {
104
+ const match = String(css || '').match(/font-family:\s*["']?([^;"'\n]+)["']?\s*;/i)
105
+ return (match?.[1] || '').trim()
106
+ }
107
+
108
+ const formatUploadedAt = (value) => {
109
+ if (!value)
110
+ return ''
111
+
112
+ let date = null
113
+ if (typeof value?.toDate === 'function')
114
+ date = value.toDate()
115
+ else if (typeof value?.seconds === 'number')
116
+ date = new Date(value.seconds * 1000)
117
+ else
118
+ date = new Date(value)
119
+
120
+ if (!date || Number.isNaN(date.getTime()))
121
+ return ''
122
+
123
+ return date.toLocaleDateString()
124
+ }
125
+
126
+ const relatedDocsForRename = (doc) => {
127
+ const groupId = doc?.meta?.fontGroupId
128
+ if (!groupId)
129
+ return [doc]
130
+ const related = uploadedFonts.value.filter(item => item?.meta?.fontGroupId === groupId)
131
+ return related.length ? related : [doc]
132
+ }
133
+
134
+ const currentFamilyForDoc = (doc) => {
135
+ const head = parsedHead.value
136
+ if (!head || !Array.isArray(head.style))
137
+ return ''
138
+
139
+ const docs = relatedDocsForRename(doc)
140
+ for (const item of docs) {
141
+ const url = item?.r2URL
142
+ if (!url)
143
+ continue
144
+ const match = head.style.find(entry => entryMatchesUrl(entry, url))
145
+ if (match) {
146
+ const family = extractFontFamilyFromCss(styleEntryContent(match))
147
+ if (family)
148
+ return family
149
+ }
150
+ }
151
+ return ''
152
+ }
153
+
154
+ const startRename = (doc) => {
155
+ if (!doc?.docId)
156
+ return
157
+ const family = currentFamilyForDoc(doc)
158
+ state.renamingDocId = doc.docId
159
+ state.renameDrafts[doc.docId] = family || doc?.name || doc?.fileName || ''
160
+ }
161
+
162
+ const cancelRename = () => {
163
+ if (state.renamingDocId)
164
+ delete state.renameDrafts[state.renamingDocId]
165
+ state.renamingDocId = null
166
+ }
167
+
168
+ const saveRename = async (doc) => {
169
+ if (!doc?.docId)
170
+ return
171
+ const nextName = (state.renameDrafts[doc.docId] || '').trim()
172
+ if (!nextName) {
173
+ state.errors.push('Font family is required.')
174
+ return
175
+ }
176
+
177
+ state.busyDocId = doc.docId
178
+ try {
179
+ const { ok, head } = parseHead()
180
+ if (!ok || !head || !Array.isArray(head.style)) {
181
+ state.errors.push('Head JSON is invalid or missing style entries.')
182
+ return
183
+ }
184
+
185
+ const docs = relatedDocsForRename(doc)
186
+ const targets = docs.flatMap(item => urlVariants(item?.r2URL || ''))
187
+ const hasTarget = value => targets.some(url => value.includes(url))
188
+
189
+ let updated = false
190
+ const nextStyle = head.style.map((entry) => {
191
+ const content = styleEntryContent(entry)
192
+ if (!content || !hasTarget(content))
193
+ return entry
194
+
195
+ const replaced = content.replace(
196
+ /font-family:\s*("[^"]*"|'[^']*'|[^;]+)\s*;/i,
197
+ `font-family: "${nextName}";`,
198
+ )
199
+ if (replaced !== content)
200
+ updated = true
201
+
202
+ return {
203
+ ...entry,
204
+ children: replaced,
205
+ innerHTML: replaced,
206
+ }
207
+ })
208
+
209
+ if (!updated) {
210
+ state.errors.push(`No @font-face entries were found in Head JSON for "${fontLabel(doc)}".`)
211
+ return
212
+ }
213
+
214
+ head.style = nextStyle
215
+ setHeadJson(head)
216
+ cancelRename()
217
+ }
218
+ catch (error) {
219
+ state.errors.push(`Font family update failed for "${fontLabel(doc)}": ${error?.message || error}`)
220
+ }
221
+ finally {
222
+ state.busyDocId = null
223
+ }
224
+ }
225
+
226
+ const stripFontFromHead = (doc) => {
227
+ if (!doc?.r2URL)
228
+ return
229
+
230
+ const { ok, head } = parseHead()
231
+ if (!ok || !head)
232
+ return
233
+
234
+ const hasVariant = (value = '') => urlVariants(doc.r2URL).some(v => value.includes(v))
235
+
236
+ if (Array.isArray(head.link)) {
237
+ head.link = head.link.filter((entry) => {
238
+ const href = String(entry?.href || '')
239
+ return !hasVariant(href)
240
+ })
241
+ }
242
+
243
+ if (Array.isArray(head.style)) {
244
+ head.style = head.style.filter((entry) => {
245
+ const content = String(entry?.children || entry?.innerHTML || '')
246
+ return !hasVariant(content)
247
+ })
248
+ }
249
+
250
+ setHeadJson(head)
251
+ }
252
+
253
+ const deleteFont = async (doc) => {
254
+ if (!doc?.docId)
255
+ return
256
+
257
+ state.busyDocId = doc.docId
258
+ try {
259
+ await edgeFirebase.removeDoc(collectionPath.value, doc.docId)
260
+ stripFontFromHead(doc)
261
+ if (state.renamingDocId === doc.docId)
262
+ cancelRename()
263
+ }
264
+ catch (error) {
265
+ state.errors.push(`Delete failed for "${fontLabel(doc)}": ${error?.message || error}`)
266
+ }
267
+ finally {
268
+ state.busyDocId = null
269
+ }
270
+ }
271
+
57
272
  const formatFromName = (name = '', contentType = '') => {
58
273
  const lower = (name || '').toLowerCase()
59
274
  const ext = lower.split('.').pop() || ''
@@ -125,6 +340,25 @@ const ensureMetaForFiles = () => {
125
340
 
126
341
  watch(() => state.files, () => ensureMetaForFiles(), { deep: true })
127
342
 
343
+ onBeforeMount(async () => {
344
+ if (!collectionPath.value)
345
+ return
346
+
347
+ state.loadingFonts = true
348
+ try {
349
+ await edgeFirebase.startSnapshot(collectionPath.value)
350
+ }
351
+ finally {
352
+ state.loadingFonts = false
353
+ }
354
+ })
355
+
356
+ onBeforeUnmount(() => {
357
+ if (!collectionPath.value)
358
+ return
359
+ edgeFirebase.stopSnapshot(collectionPath.value)
360
+ })
361
+
128
362
  const waitForR2 = async (docId) => {
129
363
  if (!docId || !collectionPath.value)
130
364
  return null
@@ -308,6 +542,88 @@ const processFonts = async () => {
308
542
  {{ state.uploading ? 'Processing…' : 'Process fonts' }}
309
543
  </edge-shad-button>
310
544
  </div>
545
+ <div class="rounded-md border border-border/70 bg-muted/20 p-2 space-y-2">
546
+ <div class="flex items-center justify-between text-xs text-muted-foreground">
547
+ <span>Uploaded custom fonts</span>
548
+ <span>{{ uploadedFonts.length }}</span>
549
+ </div>
550
+
551
+ <div v-if="state.loadingFonts" class="text-xs text-muted-foreground">
552
+ Loading fonts...
553
+ </div>
554
+
555
+ <div v-else-if="uploadedFonts.length === 0" class="text-xs text-muted-foreground">
556
+ No custom fonts uploaded for this theme yet.
557
+ </div>
558
+
559
+ <div v-else class="space-y-1">
560
+ <div
561
+ v-for="font in uploadedFonts"
562
+ :key="font.docId"
563
+ class="flex items-start gap-2 rounded border border-border/60 bg-background/60 px-2 py-2"
564
+ >
565
+ <div class="flex-1 min-w-0">
566
+ <div v-if="state.renamingDocId === font.docId">
567
+ <edge-shad-input
568
+ v-model="state.renameDrafts[font.docId]"
569
+ label="Font family"
570
+ name="fontFamilyName"
571
+ placeholder="Font family"
572
+ class="w-full"
573
+ />
574
+ </div>
575
+ <div v-else class="space-y-0.5">
576
+ <div class="truncate text-sm font-medium text-foreground">
577
+ {{ currentFamilyForDoc(font) || fontLabel(font) }}
578
+ </div>
579
+ <div class="truncate text-[11px] text-muted-foreground">
580
+ {{ font.fileName || font.docId }}
581
+ </div>
582
+ <div v-if="formatUploadedAt(font.uploadTime)" class="text-[11px] text-muted-foreground">
583
+ Uploaded {{ formatUploadedAt(font.uploadTime) }}
584
+ </div>
585
+ </div>
586
+ </div>
587
+ <div class="flex items-center gap-1">
588
+ <template v-if="state.renamingDocId === font.docId">
589
+ <edge-shad-button
590
+ size="sm"
591
+ :disabled="state.busyDocId === font.docId"
592
+ @click="saveRename(font)"
593
+ >
594
+ Save
595
+ </edge-shad-button>
596
+ <edge-shad-button
597
+ size="sm"
598
+ variant="outline"
599
+ :disabled="state.busyDocId === font.docId"
600
+ @click="cancelRename"
601
+ >
602
+ Cancel
603
+ </edge-shad-button>
604
+ </template>
605
+ <template v-else>
606
+ <edge-shad-button
607
+ size="sm"
608
+ variant="outline"
609
+ :disabled="state.busyDocId === font.docId"
610
+ @click="startRename(font)"
611
+ >
612
+ Edit family
613
+ </edge-shad-button>
614
+ <edge-shad-button
615
+ size="sm"
616
+ variant="destructive"
617
+ :disabled="state.busyDocId === font.docId"
618
+ @click="deleteFont(font)"
619
+ >
620
+ Delete
621
+ </edge-shad-button>
622
+ </template>
623
+ </div>
624
+ </div>
625
+ </div>
626
+ </div>
311
627
  <template v-if="state.errors.length">
312
628
  <Alert
313
629
  v-for="(error, idx) in state.errors"