@edgedev/create-edge-app 1.2.34 → 1.2.35

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.
@@ -22,6 +22,16 @@ const emit = defineEmits(['head'])
22
22
  const edgeFirebase = inject('edgeFirebase')
23
23
  const router = useRouter()
24
24
  const { buildPageStructuredData } = useStructuredDataTemplates()
25
+ const cmsMultiOrg = useState('cmsMultiOrg', () => true)
26
+ const isAdmin = computed(() => edgeGlobal.isAdminGlobal(edgeFirebase).value)
27
+ const isDevModeEnabled = computed(() => process.dev || Boolean(edgeGlobal.edgeState.devOverride))
28
+ const canOpenPreviewBlockContentEditor = computed(() => {
29
+ if (!isAdmin.value)
30
+ return false
31
+ if (cmsMultiOrg.value)
32
+ return true
33
+ return isDevModeEnabled.value
34
+ })
25
35
 
26
36
  const state = reactive({
27
37
  newDocs: {
@@ -50,6 +60,7 @@ const state = reactive({
50
60
  importErrorDialogOpen: false,
51
61
  importErrorMessage: '',
52
62
  previewViewport: 'full',
63
+ previewPageView: 'list',
53
64
  newRowLayout: '6',
54
65
  newPostRowLayout: '6',
55
66
  rowSettings: {
@@ -110,10 +121,8 @@ const previewViewportStyle = computed(() => {
110
121
  })
111
122
 
112
123
  const previewViewportContainStyle = computed(() => {
113
- const shouldContain = !state.editMode
114
124
  return {
115
125
  ...(previewViewportStyle.value || {}),
116
- ...(shouldContain ? { transform: 'translateZ(0)' } : {}),
117
126
  }
118
127
  })
119
128
 
@@ -121,6 +130,18 @@ const setPreviewViewport = (viewportId) => {
121
130
  state.previewViewport = viewportId
122
131
  }
123
132
 
133
+ const hasPostView = (workingDoc) => {
134
+ if (!workingDoc || typeof workingDoc !== 'object')
135
+ return false
136
+ return Boolean(workingDoc.post)
137
+ || (Array.isArray(workingDoc.postContent) && workingDoc.postContent.length > 0)
138
+ || (Array.isArray(workingDoc.postStructure) && workingDoc.postStructure.length > 0)
139
+ }
140
+
141
+ const setPreviewPageView = (view) => {
142
+ state.previewPageView = view === 'post' ? 'post' : 'list'
143
+ }
144
+
124
145
  const previewViewportMode = computed(() => {
125
146
  if (state.previewViewport === 'full')
126
147
  return 'auto'
@@ -162,6 +183,14 @@ const ROW_GAP_OPTIONS = [
162
183
  { name: '8', title: 'X-Large' },
163
184
  ]
164
185
 
186
+ const ROW_GAP_CLASS_MAP = {
187
+ 0: 'gap-0 sm:gap-0',
188
+ 2: 'gap-0 sm:gap-2',
189
+ 4: 'gap-0 sm:gap-4',
190
+ 6: 'gap-0 sm:gap-6',
191
+ 8: 'gap-0 sm:gap-8',
192
+ }
193
+
165
194
  const ROW_MOBILE_STACK_OPTIONS = [
166
195
  { name: 'normal', title: 'Left first' },
167
196
  { name: 'reverse', title: 'Right first' },
@@ -467,17 +496,51 @@ const blockPick = (block, index, slotProps, post = false) => {
467
496
  }
468
497
 
469
498
  const applyCollectionUniqueKeys = (workingDoc) => {
499
+ const hasTemplateToken = (value) => {
500
+ if (typeof value === 'string')
501
+ return value.includes('{orgId}') || value.includes('{siteId}')
502
+ if (Array.isArray(value))
503
+ return value.some(entry => hasTemplateToken(entry))
504
+ if (value && typeof value === 'object')
505
+ return Object.values(value).some(entry => hasTemplateToken(entry))
506
+ return false
507
+ }
508
+
509
+ const resolveTokens = (value) => {
510
+ if (typeof value === 'string') {
511
+ let resolved = value
512
+ const orgId = edgeGlobal.edgeState.currentOrganization || ''
513
+ const siteId = props.site || ''
514
+ if (resolved.includes('{orgId}') && orgId)
515
+ resolved = resolved.replaceAll('{orgId}', orgId)
516
+ if (resolved.includes('{siteId}') && siteId)
517
+ resolved = resolved.replaceAll('{siteId}', siteId)
518
+ return resolved
519
+ }
520
+ if (Array.isArray(value))
521
+ return value.map(entry => resolveTokens(entry))
522
+ if (value && typeof value === 'object') {
523
+ const out = {}
524
+ Object.keys(value).forEach((key) => {
525
+ out[key] = resolveTokens(value[key])
526
+ })
527
+ return out
528
+ }
529
+ return value
530
+ }
531
+
532
+ const isEmptyQueryItem = (value) => {
533
+ if (value === undefined || value === null || value === '')
534
+ return true
535
+ if (Array.isArray(value))
536
+ return value.length === 0
537
+ return false
538
+ }
539
+
470
540
  const resolveUniqueKey = (template) => {
471
541
  if (!template || typeof template !== 'string')
472
542
  return ''
473
- let resolved = template
474
- const orgId = edgeGlobal.edgeState.currentOrganization || ''
475
- const siteId = props.site || ''
476
- if (resolved.includes('{orgId}') && orgId)
477
- resolved = resolved.replaceAll('{orgId}', orgId)
478
- if (resolved.includes('{siteId}') && siteId)
479
- resolved = resolved.replaceAll('{siteId}', siteId)
480
- return resolved
543
+ return resolveTokens(template)
481
544
  }
482
545
 
483
546
  const applyToBlocks = (blocks) => {
@@ -489,6 +552,27 @@ const applyCollectionUniqueKeys = (workingDoc) => {
489
552
  return
490
553
  Object.keys(meta).forEach((fieldKey) => {
491
554
  const cfg = meta[fieldKey]
555
+ if (!cfg || typeof cfg !== 'object')
556
+ return
557
+
558
+ // Materialize tokenized collection.query filters (e.g. {siteId}) into queryItems
559
+ // so frontend hydration receives concrete runtime filter selections.
560
+ if (Array.isArray(cfg?.collection?.query)) {
561
+ if (!cfg.queryItems || typeof cfg.queryItems !== 'object')
562
+ cfg.queryItems = {}
563
+ for (const queryFilter of cfg.collection.query) {
564
+ const queryField = queryFilter?.field
565
+ if (!queryField || typeof queryField !== 'string')
566
+ continue
567
+ const rawValue = queryFilter?.value
568
+ if (!hasTemplateToken(rawValue))
569
+ continue
570
+ if (!isEmptyQueryItem(cfg.queryItems[queryField]))
571
+ continue
572
+ cfg.queryItems[queryField] = resolveTokens(rawValue)
573
+ }
574
+ }
575
+
492
576
  if (!cfg?.collection?.uniqueKey)
493
577
  return
494
578
  const resolved = resolveUniqueKey(cfg.collection.uniqueKey)
@@ -560,6 +644,8 @@ const editorDocUpdates = (workingDoc) => {
560
644
  if (workingDoc?.post || (Array.isArray(workingDoc?.postContent) && workingDoc.postContent.length > 0) || Array.isArray(workingDoc?.postStructure))
561
645
  ensureStructureDefaults(workingDoc, true)
562
646
  applyCollectionUniqueKeys(workingDoc)
647
+ if (!hasPostView(workingDoc) && state.previewPageView === 'post')
648
+ state.previewPageView = 'list'
563
649
  const blockIds = (workingDoc.content || []).map(block => block.blockId).filter(id => id)
564
650
  const postBlockIds = workingDoc.postContent ? workingDoc.postContent.map(block => block.blockId).filter(id => id) : []
565
651
  blockIds.push(...postBlockIds)
@@ -859,11 +945,7 @@ const rowUsesSpans = row => (row?.columns || []).some(col => Number.isFinite(col
859
945
 
860
946
  const rowGapClass = (row) => {
861
947
  const gap = Number(row?.gap)
862
- const allowed = new Set([0, 2, 4, 6, 8])
863
- const safeGap = allowed.has(gap) ? gap : 4
864
- if (safeGap === 0)
865
- return 'gap-0'
866
- return ['gap-0', `sm:gap-${safeGap}`].join(' ')
948
+ return ROW_GAP_CLASS_MAP[gap] || ROW_GAP_CLASS_MAP[4]
867
949
  }
868
950
 
869
951
  const rowGridClass = (row) => {
@@ -1329,6 +1411,11 @@ const slugifyMenuPageName = (value) => {
1329
1411
  .replace(/[^a-z0-9]+/g, '-')
1330
1412
  .replace(/(^-|-$)+/g, '') || 'page'
1331
1413
  }
1414
+ const titleFromSlug = (slug) => {
1415
+ if (!slug)
1416
+ return ''
1417
+ return String(slug).replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
1418
+ }
1332
1419
 
1333
1420
  const makeUniqueMenuPageName = (value, existingNames = new Set()) => {
1334
1421
  const base = slugifyMenuPageName(value)
@@ -1357,7 +1444,8 @@ const addImportedPageToSiteMenu = async (docId, pageName = '') => {
1357
1444
 
1358
1445
  const existingNames = collectMenuPageNames(menus)
1359
1446
  const menuName = makeUniqueMenuPageName(pageName || nextDocId, existingNames)
1360
- menus['Site Root'].push({ name: menuName, item: nextDocId })
1447
+ const menuTitle = String(pageName || '').trim() || titleFromSlug(menuName)
1448
+ menus['Site Root'].push({ name: menuName, menuTitle, item: nextDocId })
1361
1449
 
1362
1450
  const results = await edgeFirebase.changeDoc(sitesCollectionPath, siteId, { menus })
1363
1451
  if (results?.success === false)
@@ -1776,7 +1864,7 @@ const hasUnsavedChanges = (changes) => {
1776
1864
  :doc-id="page"
1777
1865
  :schema="schemas.pages"
1778
1866
  :new-doc-schema="state.newDocs.pages"
1779
- class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none pt-0 px-0"
1867
+ class="w-full mx-auto flex-1 bg-transparent flex flex-col border-none shadow-none pt-0 px-0" :class="[!state.editMode ? 'cms-page-preview-mode' : '']"
1780
1868
  :show-footer="false"
1781
1869
  :save-redirect-override="`/app/dashboard/sites/${site}`"
1782
1870
  :no-close-after-save="true"
@@ -1785,7 +1873,7 @@ const hasUnsavedChanges = (changes) => {
1785
1873
  @unsaved-changes="hasUnsavedChanges"
1786
1874
  >
1787
1875
  <template #header="slotProps">
1788
- <div class="relative flex items-center p-2 justify-between sticky top-0 z-50 bg-gray-100 rounded h-[50px]">
1876
+ <div class="relative flex items-center p-2 justify-between top-0 z-50 bg-gray-100 rounded h-[50px]">
1789
1877
  <span class="text-lg font-semibold whitespace-nowrap pr-1">{{ pageName }}</span>
1790
1878
 
1791
1879
  <div class="flex w-full items-center">
@@ -1847,20 +1935,45 @@ const hasUnsavedChanges = (changes) => {
1847
1935
  </edge-shad-button>
1848
1936
  </div>
1849
1937
 
1850
- <div class="flex items-center gap-1 pr-3">
1851
- <span class="text-[11px] uppercase tracking-wide text-muted-foreground">Viewport</span>
1852
- <edge-shad-button
1853
- v-for="option in previewViewportOptions"
1854
- :key="option.id"
1855
- type="button"
1856
- variant="ghost"
1857
- size="icon"
1858
- class="h-[26px] w-[26px] text-xs gap-1 border transition-colors"
1859
- :class="state.previewViewport === option.id ? 'bg-primary text-primary-foreground border-primary shadow-sm' : 'bg-muted text-foreground border-border hover:bg-muted/80'"
1860
- @click="setPreviewViewport(option.id)"
1861
- >
1862
- <component :is="option.icon" class="w-3.5 h-3.5" />
1863
- </edge-shad-button>
1938
+ <div class="flex flex-col items-center gap-1 px-2">
1939
+ <div class="flex items-center gap-1">
1940
+ <edge-shad-button
1941
+ v-for="option in previewViewportOptions"
1942
+ :key="option.id"
1943
+ type="button"
1944
+ variant="ghost"
1945
+ size="icon"
1946
+ class="h-[26px] w-[26px] text-xs gap-1 border transition-colors"
1947
+ :class="state.previewViewport === option.id ? 'bg-primary text-primary-foreground border-primary shadow-sm' : 'bg-muted text-foreground border-border hover:bg-muted/80'"
1948
+ @click="setPreviewViewport(option.id)"
1949
+ >
1950
+ <component :is="option.icon" class="w-3.5 h-3.5" />
1951
+ </edge-shad-button>
1952
+ </div>
1953
+ <span class="text-[10px] leading-tight text-muted-foreground">Viewport</span>
1954
+ </div>
1955
+ <div v-if="hasPostView(slotProps.workingDoc)" class="flex flex-col items-center gap-1 px-2">
1956
+ <div class="flex items-center gap-1">
1957
+ <edge-shad-button
1958
+ type="button"
1959
+ variant="ghost"
1960
+ class="h-[26px] px-2 text-xs border transition-colors"
1961
+ :class="state.previewPageView === 'list' ? 'bg-primary text-primary-foreground border-primary shadow-sm' : 'bg-muted text-foreground border-border hover:bg-muted/80'"
1962
+ @click="setPreviewPageView('list')"
1963
+ >
1964
+ Index
1965
+ </edge-shad-button>
1966
+ <edge-shad-button
1967
+ type="button"
1968
+ variant="ghost"
1969
+ class="h-[26px] px-2 text-xs border transition-colors"
1970
+ :class="state.previewPageView === 'post' ? 'bg-primary text-primary-foreground border-primary shadow-sm' : 'bg-muted text-foreground border-border hover:bg-muted/80'"
1971
+ @click="setPreviewPageView('post')"
1972
+ >
1973
+ Detail
1974
+ </edge-shad-button>
1975
+ </div>
1976
+ <span class="text-[10px] leading-tight text-muted-foreground">View</span>
1864
1977
  </div>
1865
1978
 
1866
1979
  <edge-shad-button variant="text" class="hover:text-primary/50 text-xs h-[26px] text-primary" @click="state.editMode = !state.editMode">
@@ -1906,7 +2019,7 @@ const hasUnsavedChanges = (changes) => {
1906
2019
  </div>
1907
2020
  </template>
1908
2021
  <template #success-alert>
1909
- <div v-if="!props.isTemplateSite" class="mt-2 flex flex-wrap items-center gap-2">
2022
+ <div v-if="state.editMode && !props.isTemplateSite" class="mt-2 flex flex-wrap items-center gap-2">
1910
2023
  <edge-shad-button
1911
2024
  variant="outline"
1912
2025
  class="text-xs h-[28px] gap-1"
@@ -1923,20 +2036,13 @@ const hasUnsavedChanges = (changes) => {
1923
2036
  </div>
1924
2037
  </template>
1925
2038
  <template #main="slotProps">
1926
- <Tabs class="w-full" default-value="list">
1927
- <TabsList v-if="slotProps.workingDoc?.post" class="w-full mt-3 bg-primary rounded-sm">
1928
- <TabsTrigger value="list">
1929
- Index Page
1930
- </TabsTrigger>
1931
- <TabsTrigger value="post">
1932
- Detail Page
1933
- </TabsTrigger>
1934
- </TabsList>
1935
- <TabsContent value="list">
1936
- <Separator class="my-4" />
2039
+ <Tabs class="w-full" :model-value="hasPostView(slotProps.workingDoc) ? state.previewPageView : 'list'">
2040
+ <TabsContent value="list" class="mt-0">
1937
2041
  <div
1938
2042
  :key="`${pagePreviewRenderKey}:list`"
1939
- class="w-full mx-auto bg-card border border-border shadow-sm md:shadow-md p-0 space-y-6"
2043
+ data-cms-preview-surface="page"
2044
+ :data-cms-preview-mode="state.editMode ? 'edit' : 'preview'"
2045
+ class="w-full h-[calc(100vh-180px)] mt-2 overflow-y-auto mx-auto bg-card border border-border shadow-sm md:shadow-md p-0 space-y-6"
1940
2046
  :class="[{ 'transition-all duration-300': !state.editMode }, state.editMode ? 'rounded-lg' : 'rounded-none']"
1941
2047
  :style="previewViewportContainStyle"
1942
2048
  >
@@ -2082,7 +2188,9 @@ const hasUnsavedChanges = (changes) => {
2082
2188
  v-model="slotProps.workingDoc.content[blockIndex(slotProps.workingDoc, blockId, false)]"
2083
2189
  :site-id="props.site"
2084
2190
  :edit-mode="state.editMode"
2085
- :contain-fixed="true"
2191
+ :override-clicks-in-edit-mode="state.editMode"
2192
+ :allow-preview-content-edit="!state.editMode && canOpenPreviewBlockContentEditor"
2193
+ :contain-fixed="state.editMode"
2086
2194
  :viewport-mode="previewViewportMode"
2087
2195
  :block-id="blockId"
2088
2196
  :theme="theme"
@@ -2185,11 +2293,12 @@ const hasUnsavedChanges = (changes) => {
2185
2293
  </edge-button-divider>
2186
2294
  </div>
2187
2295
  </TabsContent>
2188
- <TabsContent value="post">
2189
- <Separator class="my-4" />
2296
+ <TabsContent v-if="hasPostView(slotProps.workingDoc)" value="post" class="mt-0">
2190
2297
  <div
2191
2298
  :key="`${pagePreviewRenderKey}:post`"
2192
- class="w-full mx-auto bg-card border border-border shadow-sm md:shadow-md p-4 space-y-6"
2299
+ data-cms-preview-surface="page"
2300
+ :data-cms-preview-mode="state.editMode ? 'edit' : 'preview'"
2301
+ class="w-full h-[calc(100vh-180px)] mt-2 overflow-y-auto mx-auto bg-card border border-border shadow-sm md:shadow-md p-0 space-y-6"
2193
2302
  :class="[{ 'transition-all duration-300': !state.editMode }, state.editMode ? 'rounded-lg' : 'rounded-none']"
2194
2303
  :style="previewViewportContainStyle"
2195
2304
  >
@@ -2334,7 +2443,9 @@ const hasUnsavedChanges = (changes) => {
2334
2443
  :key="`${pagePreviewRenderKey}:${blockId}:${effectiveThemeId}:post`"
2335
2444
  v-model="slotProps.workingDoc.postContent[blockIndex(slotProps.workingDoc, blockId, true)]"
2336
2445
  :edit-mode="state.editMode"
2337
- :contain-fixed="true"
2446
+ :override-clicks-in-edit-mode="state.editMode"
2447
+ :allow-preview-content-edit="!state.editMode && canOpenPreviewBlockContentEditor"
2448
+ :contain-fixed="state.editMode"
2338
2449
  :viewport-mode="previewViewportMode"
2339
2450
  :block-id="blockId"
2340
2451
  :theme="theme"
@@ -2653,4 +2764,8 @@ const hasUnsavedChanges = (changes) => {
2653
2764
  .block-drag-handle:active {
2654
2765
  cursor: grabbing;
2655
2766
  }
2767
+
2768
+ .cms-page-preview-mode :deep(.border-emerald-200.bg-emerald-50) {
2769
+ display: none !important;
2770
+ }
2656
2771
  </style>
@@ -542,7 +542,10 @@ const unPublishPost = async (postId) => {
542
542
  </script>
543
543
 
544
544
  <template>
545
- <div v-if="props.mode !== 'editor'" class="space-y-4">
545
+ <div
546
+ v-if="props.mode !== 'editor'"
547
+ :class="isFullList ? 'h-full min-h-0 flex flex-col gap-4 overflow-hidden' : 'space-y-4 h-full min-h-0 overflow-y-auto'"
548
+ >
546
549
  <edge-shad-button
547
550
  variant="outline"
548
551
  :class="isFullList ? 'h-8 px-3' : 'w-full mt-2 py-0 h-[28px]'"
@@ -552,7 +555,10 @@ const unPublishPost = async (postId) => {
552
555
  New Post
553
556
  </edge-shad-button>
554
557
 
555
- <div v-if="isFullList" class="rounded-lg border bg-card overflow-hidden">
558
+ <div
559
+ v-if="isFullList"
560
+ class="rounded-lg border bg-card overflow-hidden flex flex-col h-[calc(100vh-180px)] max-h-[calc(100vh-180px)]"
561
+ >
556
562
  <div class="flex items-center justify-between px-4 py-3 border-b bg-muted/40">
557
563
  <div class="text-sm font-semibold">
558
564
  Posts
@@ -561,7 +567,10 @@ const unPublishPost = async (postId) => {
561
567
  {{ postsList.length }} total
562
568
  </div>
563
569
  </div>
564
- <div v-if="hasPosts" class="divide-y">
570
+ <div
571
+ v-if="hasPosts"
572
+ class="divide-y overflow-y-auto h-[calc(100vh-260px)] max-h-[calc(100vh-260px)]"
573
+ >
565
574
  <div
566
575
  v-for="post in postsList"
567
576
  :key="post.id"
@@ -646,7 +655,7 @@ const unPublishPost = async (postId) => {
646
655
  </div>
647
656
  <div
648
657
  v-else
649
- class="flex flex-col items-center justify-center gap-3 px-6 py-12 text-center"
658
+ class="flex-1 flex flex-col items-center justify-center gap-3 px-6 py-12 text-center"
650
659
  >
651
660
  <File class="h-8 w-8 text-muted-foreground/60" />
652
661
  <div class="space-y-1">
@@ -145,6 +145,7 @@ const schemas = {
145
145
  trackingFacebookPixel: z.string().optional(),
146
146
  trackingGoogleAnalytics: z.string().optional(),
147
147
  trackingAdroll: z.string().optional(),
148
+ sureFeedURL: z.string().optional(),
148
149
  socialFacebook: z.string().optional(),
149
150
  socialInstagram: z.string().optional(),
150
151
  socialTwitter: z.string().optional(),
@@ -639,8 +640,10 @@ const buildMenusFromDefaultPages = (defaultPages = []) => {
639
640
  if (!entry?.pageId)
640
641
  continue
641
642
  const slug = ensureUniqueSlug(entry?.name || '', null, usedSlugs)
643
+ const menuTitle = String(entry?.menuTitle || entry?.name || '').trim() || titleFromSlug(slug)
642
644
  menus['Site Root'].push({
643
645
  name: slug,
646
+ menuTitle,
644
647
  item: entry.pageId,
645
648
  disableRename: !!entry?.disableRename,
646
649
  disableDelete: !!entry?.disableDelete,
@@ -710,7 +713,9 @@ const duplicateEntriesWithPages = async (entries = [], options) => {
710
713
  }
711
714
  if (typeof entry.item === 'string' || entry.item === '') {
712
715
  const templateDoc = templatePages?.[entry.item] || null
713
- const slug = ensureUniqueSlug(entry.name || '', templateDoc, usedSlugs)
716
+ const entryMenuTitle = String(entry?.menuTitle || '').trim()
717
+ const slugSource = entry.name || entryMenuTitle
718
+ const slug = ensureUniqueSlug(slugSource || '', templateDoc, usedSlugs)
714
719
  const payload = buildPagePayloadFromTemplateDoc(templateDoc, slug, entry.name || '')
715
720
  try {
716
721
  const result = await edgeFirebase.storeDoc(`${edgeGlobal.edgeState.organizationDocPath}/sites/${siteId}/pages`, payload)
@@ -719,6 +724,7 @@ const duplicateEntriesWithPages = async (entries = [], options) => {
719
724
  next.push({
720
725
  ...entry,
721
726
  name: slug,
727
+ menuTitle: entryMenuTitle || titleFromSlug(slug),
722
728
  item: docId,
723
729
  })
724
730
  }
@@ -852,6 +858,7 @@ const isSiteDiff = computed(() => {
852
858
  trackingFacebookPixel: publishedSite.trackingFacebookPixel,
853
859
  trackingGoogleAnalytics: publishedSite.trackingGoogleAnalytics,
854
860
  trackingAdroll: publishedSite.trackingAdroll,
861
+ sureFeedURL: publishedSite.sureFeedURL,
855
862
  socialFacebook: publishedSite.socialFacebook,
856
863
  socialInstagram: publishedSite.socialInstagram,
857
864
  socialTwitter: publishedSite.socialTwitter,
@@ -880,6 +887,7 @@ const isSiteDiff = computed(() => {
880
887
  trackingFacebookPixel: siteData.value.trackingFacebookPixel,
881
888
  trackingGoogleAnalytics: siteData.value.trackingGoogleAnalytics,
882
889
  trackingAdroll: siteData.value.trackingAdroll,
890
+ sureFeedURL: siteData.value.sureFeedURL,
883
891
  socialFacebook: siteData.value.socialFacebook,
884
892
  socialInstagram: siteData.value.socialInstagram,
885
893
  socialTwitter: siteData.value.socialTwitter,
@@ -922,6 +930,7 @@ const discardSiteSettings = async () => {
922
930
  trackingFacebookPixel: publishedSite.trackingFacebookPixel || '',
923
931
  trackingGoogleAnalytics: publishedSite.trackingGoogleAnalytics || '',
924
932
  trackingAdroll: publishedSite.trackingAdroll || '',
933
+ sureFeedURL: publishedSite.sureFeedURL || '',
925
934
  socialFacebook: publishedSite.socialFacebook || '',
926
935
  socialInstagram: publishedSite.socialInstagram || '',
927
936
  socialTwitter: publishedSite.socialTwitter || '',
@@ -1154,7 +1163,8 @@ const addImportedPageToSiteMenu = (docId, pageName = '') => {
1154
1163
 
1155
1164
  const existingNames = collectMenuPageNames(menus)
1156
1165
  const menuName = makeUniqueMenuPageName(pageName || nextDocId, existingNames)
1157
- menus['Site Root'].push({ name: menuName, item: nextDocId })
1166
+ const menuTitle = String(pageName || '').trim() || titleFromSlug(menuName)
1167
+ menus['Site Root'].push({ name: menuName, menuTitle, item: nextDocId })
1158
1168
  state.menus = menus
1159
1169
  }
1160
1170
 
@@ -2132,11 +2142,12 @@ const pageSettingsUpdated = async (pageData) => {
2132
2142
  </Transition>
2133
2143
  </ResizablePanel>
2134
2144
  </ResizablePanelGroup>
2135
- <div v-else class="flex-1 overflow-y-auto p-6">
2136
- <div class="mx-auto w-full max-w-5xl space-y-6">
2145
+ <div v-else class="flex-1 min-h-0 overflow-hidden p-6">
2146
+ <div class="mx-auto w-full max-w-5xl h-full min-h-0">
2137
2147
  <edge-cms-posts
2138
2148
  mode="list"
2139
2149
  list-variant="full"
2150
+ class="h-full min-h-0"
2140
2151
  :site="props.site"
2141
2152
  @updating="isUpdating => state.updating = isUpdating"
2142
2153
  @update:selected-post-id="handlePostSelect"
@@ -480,13 +480,17 @@ watch(() => props.settings?.forwardApex, (value) => {
480
480
  </div>
481
481
  <div class="space-y-2 text-sm">
482
482
  <div class="grid grid-cols-[70px_1fr] gap-3">
483
- <div class="text-muted-foreground">CNAME</div>
483
+ <div class="text-muted-foreground">
484
+ CNAME
485
+ </div>
484
486
  <div class="font-mono">
485
487
  {{ entry?.dnsRecords?.www?.name || 'www' }} → {{ entry?.dnsRecords?.www?.value || pagesDomain }}
486
488
  </div>
487
489
  </div>
488
490
  <div class="grid grid-cols-[70px_1fr] gap-3">
489
- <div class="text-muted-foreground">CNAME</div>
491
+ <div class="text-muted-foreground">
492
+ CNAME
493
+ </div>
490
494
  <div class="font-mono">
491
495
  {{ entry?.dnsRecords?.apex?.name || '@' }} → {{ entry?.dnsRecords?.apex?.value || pagesDomain }}
492
496
  </div>
@@ -532,7 +536,7 @@ watch(() => props.settings?.forwardApex, (value) => {
532
536
  item-value="value"
533
537
  @update:model-value="value => (props.settings.theme = value || '')"
534
538
  />
535
- <edge-shad-select
539
+ <!-- <edge-shad-select
536
540
  :model-value="props.settings.menuPosition || ''"
537
541
  name="menuPosition"
538
542
  label="Menu Position"
@@ -542,7 +546,7 @@ watch(() => props.settings?.forwardApex, (value) => {
542
546
  item-title="label"
543
547
  item-value="value"
544
548
  @update:model-value="value => (props.settings.menuPosition = value || '')"
545
- />
549
+ /> -->
546
550
  </TabsContent>
547
551
  <TabsContent value="branding" class="pt-4 space-y-4">
548
552
  <div v-if="props.enableMediaPicker && props.siteId" class="space-y-2">
@@ -562,7 +566,7 @@ watch(() => props.settings?.forwardApex, (value) => {
562
566
  <img
563
567
  :src="props.settings.logo"
564
568
  alt="Logo preview"
565
- :class="['max-h-16 max-w-[220px] h-auto w-auto rounded-md border border-border object-contain', previewBackgroundClass(props.settings.logo)]"
569
+ class="max-h-16 max-w-[220px] h-auto w-auto rounded-md border border-border object-contain" :class="[previewBackgroundClass(props.settings.logo)]"
566
570
  >
567
571
  <edge-shad-button
568
572
  type="button"
@@ -612,7 +616,7 @@ watch(() => props.settings?.forwardApex, (value) => {
612
616
  <img
613
617
  :src="props.settings.logoLight"
614
618
  alt="Light logo preview"
615
- :class="['max-h-16 max-w-[220px] h-auto w-auto rounded-md border border-border object-contain', previewBackgroundClass(props.settings.logoLight)]"
619
+ class="max-h-16 max-w-[220px] h-auto w-auto rounded-md border border-border object-contain" :class="[previewBackgroundClass(props.settings.logoLight)]"
616
620
  >
617
621
  <edge-shad-button
618
622
  type="button"
@@ -666,7 +670,7 @@ watch(() => props.settings?.forwardApex, (value) => {
666
670
  <img
667
671
  :src="props.settings.brandLogoDark"
668
672
  alt="Brand dark logo preview"
669
- :class="['max-h-16 max-w-[220px] h-auto w-auto rounded-md border border-border object-contain', previewBackgroundClass(props.settings.brandLogoDark)]"
673
+ class="max-h-16 max-w-[220px] h-auto w-auto rounded-md border border-border object-contain" :class="[previewBackgroundClass(props.settings.brandLogoDark)]"
670
674
  >
671
675
  <edge-shad-button
672
676
  type="button"
@@ -716,7 +720,7 @@ watch(() => props.settings?.forwardApex, (value) => {
716
720
  <img
717
721
  :src="props.settings.brandLogoLight"
718
722
  alt="Brand light logo preview"
719
- :class="['max-h-16 max-w-[220px] h-auto w-auto rounded-md border border-border object-contain', previewBackgroundClass(props.settings.brandLogoLight)]"
723
+ class="max-h-16 max-w-[220px] h-auto w-auto rounded-md border border-border object-contain" :class="[previewBackgroundClass(props.settings.brandLogoLight)]"
720
724
  >
721
725
  <edge-shad-button
722
726
  type="button"
@@ -767,7 +771,7 @@ watch(() => props.settings?.forwardApex, (value) => {
767
771
  <img
768
772
  :src="props.settings.favicon"
769
773
  alt="Favicon preview"
770
- :class="['max-h-12 max-w-12 h-auto w-auto rounded-md border border-border object-contain', previewBackgroundClass(props.settings.favicon)]"
774
+ class="max-h-12 max-w-12 h-auto w-auto rounded-md border border-border object-contain" :class="[previewBackgroundClass(props.settings.favicon)]"
771
775
  >
772
776
  <edge-shad-button
773
777
  type="button"
@@ -856,6 +860,12 @@ watch(() => props.settings?.forwardApex, (value) => {
856
860
  name="trackingAdroll"
857
861
  placeholder="ADROLL-ID"
858
862
  />
863
+ <edge-shad-input
864
+ v-model="props.settings.sureFeedURL"
865
+ label="Sure Feedback"
866
+ name="sureFeedURL"
867
+ placeholder=""
868
+ />
859
869
  </div>
860
870
  </TabsContent>
861
871
  <TabsContent value="social" class="pt-4">
@@ -118,6 +118,17 @@ const getFolderName = (entry) => {
118
118
  return ''
119
119
  return Object.keys(entry.item || {})[0] || ''
120
120
  }
121
+ const getFolderTitle = (entry) => {
122
+ if (!isFolder(entry))
123
+ return ''
124
+ const title = String(entry?.menuTitle || entry?.folderTitle || '').trim()
125
+ if (title)
126
+ return title
127
+ const folderName = getFolderName(entry)
128
+ if (!folderName)
129
+ return ''
130
+ return folderName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
131
+ }
121
132
 
122
133
  const getFolderList = (menuName, folderName) => {
123
134
  const targetMenu = modelValue.value[menuName] || []
@@ -228,6 +239,7 @@ const submitFolderDialog = () => {
228
239
  return
229
240
  const slug = uniqueFolderSlug(value, state.folderDialog.menu)
230
241
  modelValue.value[state.folderDialog.menu].push({
242
+ menuTitle: value,
231
243
  item: { [slug]: [] },
232
244
  })
233
245
  state.folderDialog.open = false
@@ -262,13 +274,16 @@ const openRenameDialogForPage = (menuName, index, folderName = null) => {
262
274
  }
263
275
 
264
276
  const openRenameDialogForFolder = (menuName, folderName, index) => {
277
+ const folderList = modelValue.value[menuName] || []
278
+ const target = folderList[index]
279
+ const currentTitle = String(target?.menuTitle || target?.folderTitle || '').trim()
265
280
  state.renameDialog = {
266
281
  open: true,
267
282
  type: 'folder',
268
283
  menu: menuName,
269
284
  folder: folderName,
270
285
  index,
271
- value: folderName,
286
+ value: currentTitle || folderName,
272
287
  }
273
288
  }
274
289
 
@@ -347,6 +362,9 @@ const submitRenameDialog = () => {
347
362
  return
348
363
  const currentName = getFolderName(target)
349
364
  const slug = uniqueFolderSlug(value || currentName, state.renameDialog.menu, currentName)
365
+ target.menuTitle = value || target.menuTitle || target.folderTitle || currentName
366
+ if (Object.prototype.hasOwnProperty.call(target, 'folderTitle'))
367
+ delete target.folderTitle
350
368
  if (slug !== currentName) {
351
369
  target.item[slug] = target.item[currentName]
352
370
  delete target.item[currentName]
@@ -438,7 +456,7 @@ const hasEntries = computed(() => {
438
456
  <div>
439
457
  <div class="text-sm font-semibold flex items-center gap-1">
440
458
  <Folder class="w-4 h-4" />
441
- {{ getFolderName(element) }}
459
+ {{ getFolderTitle(element) }}
442
460
  </div>
443
461
  <div class="text-[11px] text-muted-foreground">
444
462
  Folder
@@ -24,6 +24,7 @@ export const useSiteSettingsTemplate = () => {
24
24
  trackingFacebookPixel: '',
25
25
  trackingGoogleAnalytics: '',
26
26
  trackingAdroll: '',
27
+ sureFeedURL: '',
27
28
  socialFacebook: '',
28
29
  socialInstagram: '',
29
30
  socialTwitter: '',
@@ -59,6 +60,7 @@ export const useSiteSettingsTemplate = () => {
59
60
  trackingFacebookPixel: { bindings: { 'field-type': 'text', 'label': 'Facebook Pixel ID' }, cols: '12', value: defaults.trackingFacebookPixel },
60
61
  trackingGoogleAnalytics: { bindings: { 'field-type': 'text', 'label': 'Google Analytics ID' }, cols: '12', value: defaults.trackingGoogleAnalytics },
61
62
  trackingAdroll: { bindings: { 'field-type': 'text', 'label': 'AdRoll ID' }, cols: '12', value: defaults.trackingAdroll },
63
+ sureFeedURL: { bindings: { 'field-type': 'text', 'label': 'Sure Feedback' }, cols: '12', value: defaults.sureFeedURL },
62
64
  socialFacebook: { bindings: { 'field-type': 'text', 'label': 'Facebook URL' }, cols: '12', value: defaults.socialFacebook },
63
65
  socialInstagram: { bindings: { 'field-type': 'text', 'label': 'Instagram URL' }, cols: '12', value: defaults.socialInstagram },
64
66
  socialTwitter: { bindings: { 'field-type': 'text', 'label': 'X (Twitter) URL' }, cols: '12', value: defaults.socialTwitter },