@edgedev/create-edge-app 1.2.33 → 1.2.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deploy.sh +77 -0
- package/edge/components/cms/block.vue +228 -18
- package/edge/components/cms/blockApi.vue +3 -3
- package/edge/components/cms/blockEditor.vue +374 -85
- package/edge/components/cms/blockPicker.vue +29 -3
- package/edge/components/cms/blockRender.vue +3 -3
- package/edge/components/cms/blocksManager.vue +755 -82
- package/edge/components/cms/codeEditor.vue +15 -6
- package/edge/components/cms/fontUpload.vue +318 -2
- package/edge/components/cms/htmlContent.vue +230 -89
- package/edge/components/cms/menu.vue +5 -4
- package/edge/components/cms/page.vue +750 -21
- package/edge/components/cms/site.vue +624 -84
- package/edge/components/cms/sitesManager.vue +5 -4
- package/edge/components/cms/themeEditor.vue +196 -162
- package/edge/components/editor.vue +5 -1
- package/edge/composables/global.ts +37 -5
- package/edge/composables/useCmsNewDocs.js +100 -0
- package/edge/composables/useEdgeCmsDialogPositionFix.js +19 -0
- package/edge/routes/cms/dashboard/blocks/[block].vue +5 -0
- package/edge/routes/cms/dashboard/blocks/index.vue +12 -1
- package/edge/routes/cms/dashboard/media/index.vue +5 -0
- package/edge/routes/cms/dashboard/sites/[site]/[[page]].vue +4 -0
- package/edge/routes/cms/dashboard/sites/[site].vue +4 -0
- package/edge/routes/cms/dashboard/sites/index.vue +4 -0
- package/edge/routes/cms/dashboard/templates/[page].vue +4 -0
- package/edge/routes/cms/dashboard/templates/index.vue +4 -0
- package/edge/routes/cms/dashboard/themes/[theme].vue +5 -0
- package/edge/routes/cms/dashboard/themes/index.vue +330 -1
- package/firebase.json +4 -0
- package/nuxt.config.ts +1 -1
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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:
|
|
470
|
-
formatOnPaste:
|
|
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:
|
|
487
|
-
formatOnPaste:
|
|
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 =
|
|
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"
|