@edgedev/create-edge-app 1.1.27 → 1.1.29

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 (35) hide show
  1. package/edge/components/auth/register.vue +51 -0
  2. package/edge/components/cms/block.vue +363 -42
  3. package/edge/components/cms/blockEditor.vue +50 -3
  4. package/edge/components/cms/codeEditor.vue +39 -2
  5. package/edge/components/cms/htmlContent.vue +10 -2
  6. package/edge/components/cms/init_blocks/footer.html +111 -19
  7. package/edge/components/cms/init_blocks/image.html +8 -0
  8. package/edge/components/cms/init_blocks/post_content.html +3 -2
  9. package/edge/components/cms/init_blocks/post_title_header.html +8 -6
  10. package/edge/components/cms/init_blocks/posts_list.html +6 -5
  11. package/edge/components/cms/mediaCard.vue +13 -2
  12. package/edge/components/cms/mediaManager.vue +35 -5
  13. package/edge/components/cms/menu.vue +384 -61
  14. package/edge/components/cms/optionsSelect.vue +20 -3
  15. package/edge/components/cms/page.vue +160 -18
  16. package/edge/components/cms/site.vue +548 -374
  17. package/edge/components/cms/siteSettingsForm.vue +623 -0
  18. package/edge/components/cms/themeDefaultMenu.vue +258 -22
  19. package/edge/components/cms/themeEditor.vue +95 -11
  20. package/edge/components/editor.vue +1 -0
  21. package/edge/components/formSubtypes/myOrgs.vue +112 -1
  22. package/edge/components/imagePicker.vue +126 -0
  23. package/edge/components/myAccount.vue +1 -0
  24. package/edge/components/myProfile.vue +345 -61
  25. package/edge/components/orgSwitcher.vue +1 -1
  26. package/edge/components/organizationMembers.vue +620 -235
  27. package/edge/components/shad/html.vue +6 -0
  28. package/edge/components/shad/number.vue +2 -2
  29. package/edge/components/sideBar.vue +7 -4
  30. package/edge/components/sideBarContent.vue +1 -1
  31. package/edge/components/userMenu.vue +50 -14
  32. package/edge/composables/global.ts +4 -1
  33. package/edge/composables/siteSettingsTemplate.js +79 -0
  34. package/edge/composables/structuredDataTemplates.js +36 -0
  35. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  <script setup>
2
- import { computed, reactive, watchEffect } from 'vue'
2
+ import { computed, reactive, watch, watchEffect } from 'vue'
3
3
  import { useVModel } from '@vueuse/core'
4
- import { File as FileIcon, FileMinus2, FilePen, Folder, FolderMinus, FolderPen, FolderPlus, GripVertical, Plus } from 'lucide-vue-next'
4
+ import { FileCog, File as FileIcon, FileMinus2, FilePen, Folder, FolderMinus, FolderPen, FolderPlus, GripVertical, Link, Plus } from 'lucide-vue-next'
5
5
 
6
6
  const props = defineProps({
7
7
  modelValue: {
@@ -31,6 +31,15 @@ const state = reactive({
31
31
  menu: '',
32
32
  value: '',
33
33
  },
34
+ linkDialog: {
35
+ open: false,
36
+ mode: 'add',
37
+ menu: '',
38
+ folder: '',
39
+ index: -1,
40
+ name: '',
41
+ url: '',
42
+ },
34
43
  renameDialog: {
35
44
  open: false,
36
45
  type: 'page',
@@ -84,7 +93,25 @@ const uniqueSlug = (value, siblings = [], current = '', options = {}) => {
84
93
  return candidate
85
94
  }
86
95
 
87
- const isFolder = entry => entry && typeof entry.item === 'object'
96
+ const uniqueDisplayName = (value, siblings = [], current = '') => {
97
+ const base = String(value || '').trim() || 'Link'
98
+ const siblingSet = new Set(siblings.filter(Boolean))
99
+ if (current)
100
+ siblingSet.delete(current)
101
+ if (!siblingSet.has(base))
102
+ return base
103
+ let suffix = 2
104
+ let candidate = `${base} (${suffix})`
105
+ while (siblingSet.has(candidate)) {
106
+ suffix += 1
107
+ candidate = `${base} (${suffix})`
108
+ }
109
+ return candidate
110
+ }
111
+
112
+ const isExternalLinkEntry = entry => entry?.item && typeof entry.item === 'object' && entry.item.type === 'external'
113
+ const isFolder = entry => entry && typeof entry.item === 'object' && !isExternalLinkEntry(entry)
114
+ const isPageEntry = entry => typeof entry?.item === 'string'
88
115
 
89
116
  const getFolderName = (entry) => {
90
117
  if (!isFolder(entry))
@@ -112,7 +139,13 @@ const resolveTemplateTitle = (pageId) => {
112
139
 
113
140
  const siblingSlugs = (list = [], excludeIndex = -1) => {
114
141
  return list
115
- .map((entry, idx) => (idx === excludeIndex || typeof entry?.item !== 'string') ? null : entry.name)
142
+ .map((entry, idx) => (idx === excludeIndex || !entry?.name) ? null : entry.name)
143
+ .filter(Boolean)
144
+ }
145
+
146
+ const siblingNames = (list = [], excludeIndex = -1) => {
147
+ return list
148
+ .map((entry, idx) => (idx === excludeIndex ? null : entry?.name))
116
149
  .filter(Boolean)
117
150
  }
118
151
 
@@ -124,6 +157,8 @@ const addPageToList = (list, pageId, nameHint) => {
124
157
  list.push({
125
158
  name: slug,
126
159
  item: pageId,
160
+ disableRename: false,
161
+ disableDelete: false,
127
162
  })
128
163
  }
129
164
 
@@ -136,6 +171,21 @@ const addPageToMenu = (menuName, pageId, folderName = null) => {
136
171
  addPageToList(targetList, pageId, resolveTemplateTitle(pageId))
137
172
  }
138
173
 
174
+ const addLinkToMenu = (menuName, label, url, folderName = null) => {
175
+ const targetList = getParentList(menuName, folderName)
176
+ if (!targetList)
177
+ return
178
+ const siblings = siblingNames(targetList)
179
+ const name = uniqueDisplayName(label, siblings)
180
+ targetList.push({
181
+ name,
182
+ item: {
183
+ type: 'external',
184
+ url,
185
+ },
186
+ })
187
+ }
188
+
139
189
  const removePage = (menuName, index, folderName = null) => {
140
190
  const targetList = getParentList(menuName, folderName)
141
191
  if (!targetList)
@@ -199,7 +249,7 @@ const deleteFolder = (menuName, index) => {
199
249
 
200
250
  const openRenameDialogForPage = (menuName, index, folderName = null) => {
201
251
  const parentList = getParentList(menuName, folderName)
202
- if (!parentList?.[index])
252
+ if (!parentList?.[index] || isExternalLinkEntry(parentList[index]))
203
253
  return
204
254
  state.renameDialog = {
205
255
  open: true,
@@ -222,6 +272,62 @@ const openRenameDialogForFolder = (menuName, folderName, index) => {
222
272
  }
223
273
  }
224
274
 
275
+ const resetLinkDialog = () => {
276
+ state.linkDialog.mode = 'add'
277
+ state.linkDialog.menu = ''
278
+ state.linkDialog.folder = ''
279
+ state.linkDialog.index = -1
280
+ state.linkDialog.name = ''
281
+ state.linkDialog.url = ''
282
+ }
283
+
284
+ watch(() => state.linkDialog.open, (open) => {
285
+ if (!open)
286
+ resetLinkDialog()
287
+ })
288
+
289
+ const openAddLinkDialog = (menuName, folderName = null) => {
290
+ state.linkDialog.mode = 'add'
291
+ state.linkDialog.menu = menuName
292
+ state.linkDialog.folder = folderName || ''
293
+ state.linkDialog.index = -1
294
+ state.linkDialog.name = ''
295
+ state.linkDialog.url = ''
296
+ state.linkDialog.open = true
297
+ }
298
+
299
+ const openEditLinkDialog = (menuName, index, entry, folderName = null) => {
300
+ state.linkDialog.mode = 'edit'
301
+ state.linkDialog.menu = menuName
302
+ state.linkDialog.folder = folderName || ''
303
+ state.linkDialog.index = index
304
+ state.linkDialog.name = entry?.name || ''
305
+ state.linkDialog.url = entry?.item?.url || ''
306
+ state.linkDialog.open = true
307
+ }
308
+
309
+ const submitLinkDialog = () => {
310
+ const label = state.linkDialog.name?.trim() || ''
311
+ const url = state.linkDialog.url?.trim() || ''
312
+ if (!label || !url)
313
+ return
314
+ const targetList = getParentList(state.linkDialog.menu, state.linkDialog.folder || null)
315
+ if (!targetList)
316
+ return
317
+ if (state.linkDialog.mode === 'edit') {
318
+ const target = targetList[state.linkDialog.index]
319
+ if (!target || !isExternalLinkEntry(target))
320
+ return
321
+ const siblings = siblingNames(targetList, state.linkDialog.index)
322
+ target.name = uniqueDisplayName(label, siblings, target.name)
323
+ target.item = { type: 'external', url }
324
+ }
325
+ else {
326
+ addLinkToMenu(state.linkDialog.menu, label, url, state.linkDialog.folder || null)
327
+ }
328
+ state.linkDialog.open = false
329
+ }
330
+
225
331
  const submitRenameDialog = () => {
226
332
  if (!state.renameDialog.open)
227
333
  return
@@ -298,6 +404,11 @@ const hasEntries = computed(() => {
298
404
  <div v-else class="px-3 py-2 text-xs text-muted-foreground">
299
405
  All template pages are already assigned.
300
406
  </div>
407
+ <DropdownMenuSeparator />
408
+ <DropdownMenuItem @click="openAddLinkDialog(menuName)">
409
+ <Link class="w-4 h-4" />
410
+ <span>External Link</span>
411
+ </DropdownMenuItem>
301
412
  </DropdownMenuContent>
302
413
  </DropdownMenu>
303
414
  <edge-shad-button
@@ -377,11 +488,16 @@ const hasEntries = computed(() => {
377
488
  <div v-else class="px-3 py-2 text-xs text-muted-foreground">
378
489
  All template pages are already assigned.
379
490
  </div>
491
+ <DropdownMenuSeparator />
492
+ <DropdownMenuItem @click="openAddLinkDialog(menuName, getFolderName(element))">
493
+ <Link class="w-4 h-4" />
494
+ <span>External Link</span>
495
+ </DropdownMenuItem>
380
496
  </DropdownMenuContent>
381
497
  </DropdownMenu>
382
498
  </div>
383
499
  </div>
384
- <div class="space-y-3 border-l border-dashed border-border/80 px-3 py-3 ml-4">
500
+ <div class="space-y-2 border-l border-dashed border-border/80 px-2 py-2 ml-3">
385
501
  <div class="flex justify-end" />
386
502
  <draggable
387
503
  :list="element.item[getFolderName(element)]"
@@ -391,21 +507,42 @@ const hasEntries = computed(() => {
391
507
  :group="dragGroup"
392
508
  >
393
509
  <template #item="{ element: child, index: childIndex }">
394
- <div class="flex items-center justify-between gap-3 rounded-md border border-border/70 bg-card px-3 py-2">
395
- <div class="flex items-center gap-2">
510
+ <div class="flex items-center justify-between gap-2 rounded-md border border-border/70 bg-card px-2 py-1.5">
511
+ <div class="flex items-center gap-2 flex-1 min-w-0">
396
512
  <GripVertical class="w-4 h-4 text-muted-foreground drag-handle" />
397
- <div>
398
- <div class="text-sm font-semibold flex items-center gap-1">
399
- <FileIcon class="w-4 h-4" />
400
- {{ element.item[getFolderName(element)][childIndex].name }}
513
+ <div class="min-w-0">
514
+ <div class="text-xs font-semibold flex items-center gap-1 leading-tight">
515
+ <template v-if="isExternalLinkEntry(child)">
516
+ <Link class="w-3.5 h-3.5" />
517
+ {{ child.name }}
518
+ </template>
519
+ <template v-else>
520
+ <FileIcon class="w-3.5 h-3.5" />
521
+ {{ child.name }}
522
+ </template>
401
523
  </div>
402
- <div class="text-[11px] text-muted-foreground">
403
- Template: {{ resolveTemplateTitle(element.item[getFolderName(element)][childIndex].item) }}
524
+ <div class="text-[10px] text-muted-foreground leading-tight">
525
+ <template v-if="isExternalLinkEntry(child)">
526
+ {{ child.item?.url || 'External link' }}
527
+ </template>
528
+ <template v-else>
529
+ {{ resolveTemplateTitle(child.item) }}
530
+ </template>
404
531
  </div>
405
532
  </div>
406
533
  </div>
407
534
  <div class="flex gap-1">
408
535
  <edge-shad-button
536
+ v-if="isExternalLinkEntry(child)"
537
+ variant="ghost"
538
+ size="icon"
539
+ class="h-7 w-7"
540
+ @click="openEditLinkDialog(menuName, childIndex, child, getFolderName(element))"
541
+ >
542
+ <Link class="w-3.5 h-3.5" />
543
+ </edge-shad-button>
544
+ <edge-shad-button
545
+ v-else
409
546
  variant="ghost"
410
547
  size="icon"
411
548
  class="h-7 w-7"
@@ -413,6 +550,25 @@ const hasEntries = computed(() => {
413
550
  >
414
551
  <FilePen class="w-3.5 h-3.5" />
415
552
  </edge-shad-button>
553
+ <DropdownMenu v-if="isPageEntry(child)">
554
+ <DropdownMenuTrigger as-child>
555
+ <edge-shad-button
556
+ variant="ghost"
557
+ size="icon"
558
+ class="h-7 w-7"
559
+ >
560
+ <FileCog class="w-3.5 h-3.5" />
561
+ </edge-shad-button>
562
+ </DropdownMenuTrigger>
563
+ <DropdownMenuContent align="end" class="w-48">
564
+ <DropdownMenuCheckboxItem v-model="child.disableRename">
565
+ Disable Rename
566
+ </DropdownMenuCheckboxItem>
567
+ <DropdownMenuCheckboxItem v-model="child.disableDelete">
568
+ Disable Delete
569
+ </DropdownMenuCheckboxItem>
570
+ </DropdownMenuContent>
571
+ </DropdownMenu>
416
572
  <edge-shad-button
417
573
  variant="ghost"
418
574
  size="icon"
@@ -435,21 +591,42 @@ const hasEntries = computed(() => {
435
591
  </draggable>
436
592
  </div>
437
593
  </div>
438
- <div v-else class="flex items-center justify-between gap-3 rounded-md border border-border/70 bg-card px-3 py-2">
439
- <div class="flex items-center gap-2">
594
+ <div v-else class="flex items-center justify-between gap-2 rounded-md border border-border/70 bg-card px-2 py-1.5">
595
+ <div class="flex items-center gap-2 flex-1 min-w-0">
440
596
  <GripVertical class="w-4 h-4 text-muted-foreground drag-handle" />
441
- <div>
442
- <div class="text-sm font-semibold flex items-center gap-1">
443
- <FileIcon class="w-4 h-4" />
444
- {{ element.name }}
597
+ <div class="min-w-0">
598
+ <div class="text-xs font-semibold flex items-center gap-1 leading-tight">
599
+ <template v-if="isExternalLinkEntry(element)">
600
+ <Link class="w-3.5 h-3.5" />
601
+ {{ element.name }}
602
+ </template>
603
+ <template v-else>
604
+ <FileIcon class="w-3.5 h-3.5" />
605
+ {{ element.name }}
606
+ </template>
445
607
  </div>
446
- <div class="text-[11px] text-muted-foreground">
447
- Template: {{ resolveTemplateTitle(element.item) }}
608
+ <div class="text-[10px] text-muted-foreground leading-tight">
609
+ <template v-if="isExternalLinkEntry(element)">
610
+ {{ element.item?.url || 'External link' }}
611
+ </template>
612
+ <template v-else>
613
+ {{ resolveTemplateTitle(element.item) }}
614
+ </template>
448
615
  </div>
449
616
  </div>
450
617
  </div>
451
618
  <div class="flex gap-1">
452
619
  <edge-shad-button
620
+ v-if="isExternalLinkEntry(element)"
621
+ variant="ghost"
622
+ size="icon"
623
+ class="h-7 w-7"
624
+ @click="openEditLinkDialog(menuName, index, element)"
625
+ >
626
+ <Link class="w-3.5 h-3.5" />
627
+ </edge-shad-button>
628
+ <edge-shad-button
629
+ v-else
453
630
  variant="ghost"
454
631
  size="icon"
455
632
  class="h-7 w-7"
@@ -457,6 +634,25 @@ const hasEntries = computed(() => {
457
634
  >
458
635
  <FilePen class="w-3.5 h-3.5" />
459
636
  </edge-shad-button>
637
+ <DropdownMenu v-if="isPageEntry(element)">
638
+ <DropdownMenuTrigger as-child>
639
+ <edge-shad-button
640
+ variant="ghost"
641
+ size="icon"
642
+ class="h-7 w-7"
643
+ >
644
+ <FileCog class="w-3.5 h-3.5" />
645
+ </edge-shad-button>
646
+ </DropdownMenuTrigger>
647
+ <DropdownMenuContent align="end" class="w-48">
648
+ <DropdownMenuCheckboxItem v-model="element.disableRename">
649
+ Disable Rename
650
+ </DropdownMenuCheckboxItem>
651
+ <DropdownMenuCheckboxItem v-model="element.disableDelete">
652
+ Disable Delete
653
+ </DropdownMenuCheckboxItem>
654
+ </DropdownMenuContent>
655
+ </DropdownMenu>
460
656
  <edge-shad-button
461
657
  variant="ghost"
462
658
  size="icon"
@@ -514,6 +710,46 @@ const hasEntries = computed(() => {
514
710
  </DialogContent>
515
711
  </edge-shad-dialog>
516
712
 
713
+ <edge-shad-dialog v-model="state.linkDialog.open">
714
+ <DialogContent class="pt-10">
715
+ <DialogHeader>
716
+ <DialogTitle>
717
+ {{ state.linkDialog.mode === 'edit' ? 'Edit External Link' : 'Add External Link' }}
718
+ </DialogTitle>
719
+ </DialogHeader>
720
+ <div class="space-y-4">
721
+ <edge-shad-input
722
+ v-model="state.linkDialog.name"
723
+ name="linkName"
724
+ label="Label"
725
+ placeholder="Link label"
726
+ />
727
+ <edge-shad-input
728
+ v-model="state.linkDialog.url"
729
+ name="linkUrl"
730
+ label="URL"
731
+ placeholder="https://example.com or tel:123-456-7890"
732
+ />
733
+ </div>
734
+ <DialogFooter class="pt-2">
735
+ <edge-shad-button
736
+ variant="destructive"
737
+ @click="state.linkDialog.open = false"
738
+ >
739
+ Cancel
740
+ </edge-shad-button>
741
+ <edge-shad-button
742
+ type="button"
743
+ class="bg-slate-800 hover:bg-slate-500 text-white"
744
+ :disabled="!state.linkDialog.name?.trim()?.length || !state.linkDialog.url?.trim()?.length"
745
+ @click="submitLinkDialog"
746
+ >
747
+ {{ state.linkDialog.mode === 'edit' ? 'Update Link' : 'Add Link' }}
748
+ </edge-shad-button>
749
+ </DialogFooter>
750
+ </DialogContent>
751
+ </edge-shad-dialog>
752
+
517
753
  <edge-shad-dialog v-model="state.renameDialog.open">
518
754
  <DialogContent class="pt-10">
519
755
  <DialogHeader>
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
2
  import { toTypedSchema } from '@vee-validate/zod'
3
+ import { FolderCog } from 'lucide-vue-next'
3
4
  import * as z from 'zod'
4
5
  const props = defineProps({
5
6
  themeId: {
@@ -10,6 +11,7 @@ const props = defineProps({
10
11
 
11
12
  const emit = defineEmits(['head'])
12
13
  const edgeFirebase = inject('edgeFirebase')
14
+ const { createDefaults: createSiteSettingsDefaults } = useSiteSettingsTemplate()
13
15
  const state = reactive({
14
16
  filter: '',
15
17
  workingDoc: {},
@@ -82,6 +84,9 @@ const state = reactive({
82
84
  }
83
85
  }`,
84
86
  },
87
+ extraCSS: {
88
+ value: '',
89
+ },
85
90
  version: 1,
86
91
  defaultPages: { value: [] },
87
92
  defaultMenus: {
@@ -90,10 +95,12 @@ const state = reactive({
90
95
  'Not In Menu': [],
91
96
  },
92
97
  },
98
+ defaultSiteSettings: { value: createSiteSettingsDefaults() },
93
99
  },
94
100
  },
95
101
  mounted: false,
96
102
  loading: false,
103
+ defaultSettingsOpen: false,
97
104
  })
98
105
 
99
106
  const blockSchema = toTypedSchema(z.object({
@@ -106,14 +113,19 @@ onMounted(() => {
106
113
  // state.mounted = true
107
114
  })
108
115
 
109
- const headObject = computed(() => {
116
+ const lastValidHead = ref({})
117
+ const lastValidTheme = ref({})
118
+
119
+ const parseJsonSafe = (value, fallback) => {
110
120
  try {
111
- return JSON.parse(state.workingDoc.headJSON || '{}')
121
+ return JSON.parse(value || '{}')
112
122
  }
113
- catch (e) {
114
- return {}
123
+ catch (error) {
124
+ return fallback
115
125
  }
116
- })
126
+ }
127
+
128
+ const headObject = computed(() => lastValidHead.value)
117
129
 
118
130
  watch(headObject, (newHeadElements) => {
119
131
  emit('head', newHeadElements)
@@ -153,6 +165,19 @@ const ensureDefaultMenusObject = (doc = state.workingDoc) => {
153
165
  return doc.defaultMenus
154
166
  }
155
167
 
168
+ const ensureDefaultSiteSettings = (doc = state.workingDoc) => {
169
+ if (!doc)
170
+ return {}
171
+ if (!doc.defaultSiteSettings || typeof doc.defaultSiteSettings !== 'object' || Array.isArray(doc.defaultSiteSettings))
172
+ doc.defaultSiteSettings = createSiteSettingsDefaults()
173
+ const defaults = createSiteSettingsDefaults()
174
+ for (const key of Object.keys(defaults)) {
175
+ if (doc.defaultSiteSettings[key] === undefined)
176
+ doc.defaultSiteSettings[key] = defaults[key]
177
+ }
178
+ return doc.defaultSiteSettings
179
+ }
180
+
156
181
  const collectTemplateIdsFromMenus = (menus = {}) => {
157
182
  const ids = new Set()
158
183
  const traverse = (items = []) => {
@@ -229,6 +254,8 @@ const hydrateMenusFromDefaultPages = (doc = state.workingDoc) => {
229
254
  return {
230
255
  name: slug,
231
256
  item: entry.pageId,
257
+ disableRename: !!entry?.disableRename,
258
+ disableDelete: !!entry?.disableDelete,
232
259
  }
233
260
  })
234
261
  }
@@ -245,6 +272,8 @@ const flattenMenusToDefaultPages = (menus = {}) => {
245
272
  collected.push({
246
273
  pageId: entry.item,
247
274
  name: templatePageName(entry.item, entry.name),
275
+ disableRename: !!entry?.disableRename,
276
+ disableDelete: !!entry?.disableDelete,
248
277
  })
249
278
  }
250
279
  else if (typeof entry.item === 'object') {
@@ -266,7 +295,11 @@ const syncDefaultPagesFromMenus = () => {
266
295
  const defaults = ensureDefaultPagesArray()
267
296
  const sameLength = defaults.length === normalized.length
268
297
  const sameOrder = sameLength && defaults.every((entry, index) => entry.pageId === normalized[index].pageId)
269
- if (sameLength && sameOrder)
298
+ const sameFlags = sameLength && defaults.every((entry, index) => (
299
+ !!entry.disableRename === !!normalized[index]?.disableRename
300
+ && !!entry.disableDelete === !!normalized[index]?.disableDelete
301
+ ))
302
+ if (sameLength && sameOrder && sameFlags)
270
303
  return
271
304
  defaults.splice(0, defaults.length, ...normalized)
272
305
  }
@@ -275,6 +308,7 @@ const editorDocUpdates = (workingDoc) => {
275
308
  state.workingDoc = workingDoc
276
309
  ensureDefaultPagesArray(state.workingDoc)
277
310
  ensureDefaultMenusObject(state.workingDoc)
311
+ ensureDefaultSiteSettings(state.workingDoc)
278
312
  hydrateMenusFromDefaultPages(state.workingDoc)
279
313
  syncDefaultPagesFromMenus()
280
314
  }
@@ -312,6 +346,14 @@ watch(() => state.workingDoc?.defaultMenus, () => {
312
346
  syncDefaultPagesFromMenus()
313
347
  }, { deep: true })
314
348
 
349
+ watch(() => state.workingDoc?.headJSON, (value) => {
350
+ lastValidHead.value = parseJsonSafe(value, lastValidHead.value)
351
+ }, { immediate: true })
352
+
353
+ watch(() => state.workingDoc?.theme, (value) => {
354
+ lastValidTheme.value = parseJsonSafe(value, lastValidTheme.value)
355
+ }, { immediate: true })
356
+
315
357
  onBeforeMount(async () => {
316
358
  if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/sites`]) {
317
359
  await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/sites`)
@@ -359,12 +401,21 @@ onBeforeMount(async () => {
359
401
  </template>
360
402
  <template #main="slotProps">
361
403
  <div class="pt-4 flex flex-col gap-6 lg:flex-row">
362
- <div class="lg:w-1/3 lg:max-w-sm w-full space-y-4">
404
+ <div class="lg:w-72 lg:max-w-xs w-full space-y-4">
363
405
  <Card class="h-full">
364
406
  <CardHeader class="pb-2">
365
- <CardTitle class="text-base">
366
- Default Template Pages
367
- </CardTitle>
407
+ <div class="flex items-center justify-between gap-2">
408
+ <CardTitle class="text-base">
409
+ Default Template Pages
410
+ </CardTitle>
411
+ <edge-shad-button
412
+ size="icon"
413
+ type="text"
414
+ @click="state.defaultSettingsOpen = true"
415
+ >
416
+ <FolderCog class="h-4 w-4" />
417
+ </edge-shad-button>
418
+ </div>
368
419
  <CardDescription class="text-xs">
369
420
  Choose which template pages are created for new sites and organize them into Site Menu or Not In Menu.
370
421
  </CardDescription>
@@ -409,6 +460,14 @@ onBeforeMount(async () => {
409
460
  height="400px"
410
461
  class="mb-4 w-full"
411
462
  />
463
+ <edge-cms-code-editor
464
+ v-model="slotProps.workingDoc.extraCSS"
465
+ title="Extra CSS"
466
+ language="css"
467
+ name="extraCSS"
468
+ height="300px"
469
+ class="mb-4 w-full"
470
+ />
412
471
  </div>
413
472
  <div class="w-1/2">
414
473
  <div class="w-full mx-auto bg-white drop-shadow-[4px_4px_6px_rgba(0,0,0,0.5)] shadow-lg shadow-black/30">
@@ -416,7 +475,7 @@ onBeforeMount(async () => {
416
475
  :site-id="edgeGlobal.edgeState.blockEditorSite"
417
476
  class="!h-[calc(100vh-220px)] overflow-y-auto"
418
477
  list-only
419
- :theme="JSON.parse(slotProps.workingDoc.theme)"
478
+ :theme="lastValidTheme"
420
479
  />
421
480
  </div>
422
481
  </div>
@@ -425,5 +484,30 @@ onBeforeMount(async () => {
425
484
  </div>
426
485
  </template>
427
486
  </edge-editor>
487
+ <Sheet v-model:open="state.defaultSettingsOpen">
488
+ <SheetContent side="left" class="w-full md:w-1/2 max-w-none sm:max-w-none max-w-2xl">
489
+ <SheetHeader>
490
+ <SheetTitle>Default Site Settings</SheetTitle>
491
+ <SheetDescription>
492
+ These values seed new sites created with this theme.
493
+ </SheetDescription>
494
+ </SheetHeader>
495
+ <div class="p-6 h-[calc(100vh-140px)] overflow-y-auto">
496
+ <edge-cms-site-settings-form
497
+ v-if="state.workingDoc"
498
+ :settings="state.workingDoc.defaultSiteSettings"
499
+ :show-users="false"
500
+ :show-theme-fields="false"
501
+ :is-admin="true"
502
+ :enable-media-picker="false"
503
+ />
504
+ </div>
505
+ <SheetFooter class="pt-2">
506
+ <edge-shad-button class="w-full" @click="state.defaultSettingsOpen = false">
507
+ Done
508
+ </edge-shad-button>
509
+ </SheetFooter>
510
+ </SheetContent>
511
+ </Sheet>
428
512
  </div>
429
513
  </template>
@@ -569,6 +569,7 @@ const onError = async () => {
569
569
  <AlertDescription>
570
570
  {{ state.successMessage }}
571
571
  </AlertDescription>
572
+ <slot name="success-alert" :message="state.successMessage" :working-doc="state.workingDoc" />
572
573
  </Alert>
573
574
  <slot name="main" :title="title" :on-cancel="onCancel" :submitting="state.submitting" :unsaved-changes="unsavedChanges" :on-submit="triggerSubmit" :working-doc="state.workingDoc">
574
575
  <div class="flex flex-wrap justify-between">