@edgedev/create-edge-app 1.2.33 → 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.
Files changed (45) hide show
  1. package/README.md +1 -0
  2. package/agents.md +95 -2
  3. package/deploy.sh +136 -0
  4. package/edge/components/cms/block.vue +977 -305
  5. package/edge/components/cms/blockApi.vue +3 -3
  6. package/edge/components/cms/blockEditor.vue +688 -86
  7. package/edge/components/cms/blockPicker.vue +31 -5
  8. package/edge/components/cms/blockRender.vue +3 -3
  9. package/edge/components/cms/blocksManager.vue +790 -82
  10. package/edge/components/cms/codeEditor.vue +15 -6
  11. package/edge/components/cms/fontUpload.vue +318 -2
  12. package/edge/components/cms/htmlContent.vue +825 -93
  13. package/edge/components/cms/init_blocks/contact_us.html +55 -47
  14. package/edge/components/cms/init_blocks/newsletter.html +56 -96
  15. package/edge/components/cms/menu.vue +96 -34
  16. package/edge/components/cms/page.vue +902 -58
  17. package/edge/components/cms/posts.vue +13 -4
  18. package/edge/components/cms/site.vue +638 -87
  19. package/edge/components/cms/siteSettingsForm.vue +19 -9
  20. package/edge/components/cms/sitesManager.vue +5 -4
  21. package/edge/components/cms/themeDefaultMenu.vue +20 -2
  22. package/edge/components/cms/themeEditor.vue +196 -162
  23. package/edge/components/editor.vue +5 -1
  24. package/edge/composables/global.ts +37 -5
  25. package/edge/composables/siteSettingsTemplate.js +2 -0
  26. package/edge/composables/useCmsNewDocs.js +100 -0
  27. package/edge/composables/useEdgeCmsDialogPositionFix.js +19 -0
  28. package/edge/routes/cms/dashboard/blocks/[block].vue +5 -0
  29. package/edge/routes/cms/dashboard/blocks/index.vue +12 -1
  30. package/edge/routes/cms/dashboard/media/index.vue +5 -0
  31. package/edge/routes/cms/dashboard/sites/[site]/[[page]].vue +4 -0
  32. package/edge/routes/cms/dashboard/sites/[site].vue +4 -0
  33. package/edge/routes/cms/dashboard/sites/index.vue +4 -0
  34. package/edge/routes/cms/dashboard/templates/[page].vue +4 -0
  35. package/edge/routes/cms/dashboard/templates/index.vue +4 -0
  36. package/edge/routes/cms/dashboard/themes/[theme].vue +5 -0
  37. package/edge/routes/cms/dashboard/themes/index.vue +330 -1
  38. package/edge-pull.sh +16 -2
  39. package/edge-push.sh +9 -1
  40. package/edge-remote.sh +20 -0
  41. package/edge-status.sh +9 -5
  42. package/edge-update-all.sh +127 -0
  43. package/firebase.json +4 -0
  44. package/nuxt.config.ts +1 -1
  45. package/package.json +2 -2
@@ -35,6 +35,44 @@ const emit = defineEmits(['loaded'])
35
35
 
36
36
  const scopeId = `hc-${Math.random().toString(36).slice(2)}`
37
37
 
38
+ const themeExtraCSS = computed(() => {
39
+ const value = props.theme?.extraCSS
40
+ return typeof value === 'string' ? value : ''
41
+ })
42
+
43
+ const toCssVarToken = (key) => {
44
+ return String(key || '')
45
+ .trim()
46
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
47
+ .replace(/[\s_]+/g, '-')
48
+ .replace(/[^a-zA-Z0-9-]/g, '-')
49
+ .replace(/-+/g, '-')
50
+ .replace(/^-|-$/g, '')
51
+ .toLowerCase()
52
+ }
53
+
54
+ const isSafeLegacyVarKey = key => /^[A-Za-z0-9_-]+$/.test(String(key || ''))
55
+
56
+ const pushVarDecl = (decls, prefix, key, value) => {
57
+ const token = toCssVarToken(key)
58
+ if (!token)
59
+ return
60
+
61
+ const normalizedName = `--${prefix}-${token}`
62
+ decls.push(`${normalizedName}: ${value};`)
63
+
64
+ const legacyKey = String(key || '')
65
+ if (!isSafeLegacyVarKey(legacyKey))
66
+ return
67
+
68
+ const legacyName = `--${prefix}-${legacyKey}`
69
+ if (legacyName !== normalizedName)
70
+ decls.push(`${legacyName}: ${value};`)
71
+ }
72
+
73
+ const cssVarRef = (prefix, key) => `var(--${prefix}-${toCssVarToken(key)})`
74
+ const escapeClassToken = value => String(value || '').replace(/([^a-zA-Z0-9_-])/g, '\\$1')
75
+
38
76
  // --- UnoCSS Runtime singleton (global, one init for the whole app) ---
39
77
  async function ensureUnoRuntime() {
40
78
  if (typeof window === 'undefined')
@@ -89,24 +127,24 @@ function buildGlobalThemeCSS(theme) {
89
127
  const t = normalizeTheme(theme || {})
90
128
  const { colors, fontFamily, fontSize, borderRadius, boxShadow } = t
91
129
  const decls = []
92
- Object.entries(colors).forEach(([k, v]) => decls.push(`--color-${k}: ${Array.isArray(v) ? v[0] : v};`))
130
+ Object.entries(colors).forEach(([k, v]) => pushVarDecl(decls, 'color', k, Array.isArray(v) ? v[0] : v))
93
131
  Object.entries(fontFamily).forEach(([k, v]) => {
94
132
  const val = Array.isArray(v) ? v.map(x => (x.includes(' ') ? `'${x}'` : x)).join(', ') : v
95
- decls.push(`--font-${k}: ${val};`)
133
+ pushVarDecl(decls, 'font', k, val)
96
134
  })
97
135
  Object.entries(fontSize).forEach(([k, v]) => {
98
136
  if (Array.isArray(v)) {
99
137
  const [size, opts] = v
100
- decls.push(`--font-size-${k}: ${size};`)
138
+ pushVarDecl(decls, 'font-size', k, size)
101
139
  if (opts && opts.lineHeight)
102
- decls.push(`--line-height-${k}: ${opts.lineHeight};`)
140
+ pushVarDecl(decls, 'line-height', k, opts.lineHeight)
103
141
  }
104
142
  else {
105
- decls.push(`--font-size-${k}: ${v};`)
143
+ pushVarDecl(decls, 'font-size', k, v)
106
144
  }
107
145
  })
108
- Object.entries(borderRadius).forEach(([k, v]) => decls.push(`--radius-${k}: ${v};`))
109
- Object.entries(boxShadow).forEach(([k, v]) => decls.push(`--shadow-${k}: ${v};`))
146
+ Object.entries(borderRadius).forEach(([k, v]) => pushVarDecl(decls, 'radius', k, v))
147
+ Object.entries(boxShadow).forEach(([k, v]) => pushVarDecl(decls, 'shadow', k, v))
110
148
  return `:root{${decls.join('')}}`
111
149
  }
112
150
 
@@ -114,25 +152,34 @@ function buildScopedThemeCSS(theme, scopeId) {
114
152
  const t = normalizeTheme(theme || {})
115
153
  const { colors, fontFamily, fontSize, borderRadius, boxShadow } = t
116
154
  const decls = []
117
- Object.entries(colors).forEach(([k, v]) => decls.push(`--color-${k}: ${Array.isArray(v) ? v[0] : v};`))
155
+ Object.entries(colors).forEach(([k, v]) => pushVarDecl(decls, 'color', k, Array.isArray(v) ? v[0] : v))
118
156
  Object.entries(fontFamily).forEach(([k, v]) => {
119
157
  const val = Array.isArray(v) ? v.map(x => (x.includes(' ') ? `'${x}'` : x)).join(', ') : v
120
- decls.push(`--font-${k}: ${val};`)
158
+ pushVarDecl(decls, 'font', k, val)
121
159
  })
122
160
  Object.entries(fontSize).forEach(([k, v]) => {
123
161
  if (Array.isArray(v)) {
124
162
  const [size, opts] = v
125
- decls.push(`--font-size-${k}: ${size};`)
163
+ pushVarDecl(decls, 'font-size', k, size)
126
164
  if (opts?.lineHeight)
127
- decls.push(`--line-height-${k}: ${opts.lineHeight};`)
165
+ pushVarDecl(decls, 'line-height', k, opts.lineHeight)
128
166
  }
129
167
  else {
130
- decls.push(`--font-size-${k}: ${v};`)
168
+ pushVarDecl(decls, 'font-size', k, v)
131
169
  }
132
170
  })
133
- Object.entries(borderRadius).forEach(([k, v]) => decls.push(`--radius-${k}: ${v};`))
134
- Object.entries(boxShadow).forEach(([k, v]) => decls.push(`--shadow-${k}: ${v};`))
135
- return `[data-theme-scope="${scopeId}"]{${decls.join('')}}`
171
+ Object.entries(borderRadius).forEach(([k, v]) => pushVarDecl(decls, 'radius', k, v))
172
+ Object.entries(boxShadow).forEach(([k, v]) => pushVarDecl(decls, 'shadow', k, v))
173
+
174
+ const rules = [`[data-theme-scope="${scopeId}"]{${decls.join('')}}`]
175
+ Object.keys(colors || {}).forEach((key) => {
176
+ const escapedKey = escapeClassToken(key)
177
+ const colorRef = cssVarRef('color', key)
178
+ rules.push(`[data-theme-scope="${scopeId}"] .bg-${escapedKey}{background-color:${colorRef} !important;}`)
179
+ rules.push(`[data-theme-scope="${scopeId}"] .text-${escapedKey}{color:${colorRef} !important;}`)
180
+ rules.push(`[data-theme-scope="${scopeId}"] .border-${escapedKey}{border-color:${colorRef} !important;}`)
181
+ })
182
+ return rules.join('')
136
183
  }
137
184
 
138
185
  function setGlobalThemeVars(theme) {
@@ -167,11 +214,18 @@ const safeHtml = computed(() => {
167
214
  })
168
215
 
169
216
  // Inject theme CSS variables into <head> for SSR + client
170
- useHead(() => ({
171
- style: [
172
- { id: 'htmlcontent-theme-global', children: buildScopedThemeCSS(props.theme, scopeId) },
173
- ],
174
- }))
217
+ useHead(() => {
218
+ const style = [
219
+ { id: `htmlcontent-theme-vars-${scopeId}`, children: buildScopedThemeCSS(props.theme, scopeId) },
220
+ ]
221
+ if (themeExtraCSS.value.trim()) {
222
+ style.push({
223
+ id: `htmlcontent-theme-extra-${scopeId}`,
224
+ children: themeExtraCSS.value,
225
+ })
226
+ }
227
+ return { style }
228
+ })
175
229
 
176
230
  // --- Embla initializer (runs client-side only) ---
177
231
  async function initEmblaCarousels(scope) {
@@ -339,10 +393,681 @@ async function initEmblaCarousels(scope) {
339
393
  })
340
394
  }
341
395
 
396
+ function initCmsNavHelpers(scope) {
397
+ if (!scope || !import.meta.client)
398
+ return
399
+
400
+ const existingCleanupFns = Array.isArray(scope.__cmsNavCleanupFns)
401
+ ? scope.__cmsNavCleanupFns
402
+ : []
403
+ existingCleanupFns.forEach((cleanup) => {
404
+ try {
405
+ cleanup()
406
+ }
407
+ catch {}
408
+ })
409
+ scope.__cmsNavCleanupFns = []
410
+
411
+ const roots = scope.querySelectorAll('.cms-nav-root, [data-cms-nav-root]')
412
+ roots.forEach((root) => {
413
+ const parseBoolean = (rawValue, fallback = false) => {
414
+ if (rawValue == null || rawValue === '')
415
+ return fallback
416
+ const normalized = String(rawValue).trim().toLowerCase()
417
+ if (['false', '0', 'off', 'no'].includes(normalized))
418
+ return false
419
+ if (['true', '1', 'on', 'yes'].includes(normalized))
420
+ return true
421
+ return fallback
422
+ }
423
+
424
+ const toClassTokens = (rawValue) => {
425
+ return String(rawValue || '')
426
+ .split(/\s+/)
427
+ .map(token => token.trim())
428
+ .filter(Boolean)
429
+ }
430
+
431
+ const normalizeRuntimeToken = (token) => {
432
+ if (!token)
433
+ return token
434
+ const parts = token.split(':')
435
+ const core = parts.pop()
436
+ const nakedCore = core.startsWith('!') ? core.slice(1) : core
437
+ const hasBreakpoint = parts.some((part) => {
438
+ const normalized = part.replace(/^!/, '')
439
+ return Object.prototype.hasOwnProperty.call(BREAKPOINT_MIN_WIDTHS, normalized)
440
+ })
441
+ const isTextSize = /^text-(xs|sm|base|lg|xl|\d+xl)$/.test(nakedCore)
442
+ const isFontUtility = /^font-([\w-]+|\[[^\]]+\])$/.test(nakedCore)
443
+ const nextCore = (hasBreakpoint || isTextSize || isFontUtility) && !core.startsWith('!')
444
+ ? `!${core}`
445
+ : core
446
+ return [...parts, nextCore].join(':')
447
+ }
448
+
449
+ const tokenVariants = (token) => {
450
+ if (!token)
451
+ return []
452
+ const variants = new Set([token])
453
+ const parts = token.split(':')
454
+ const core = parts.pop()
455
+ const nakedCore = core.startsWith('!') ? core.slice(1) : core
456
+ variants.add([...parts, nakedCore].join(':'))
457
+ variants.add([...parts, `!${nakedCore}`].join(':'))
458
+ return Array.from(variants).filter(Boolean)
459
+ }
460
+
461
+ const mapRuntimeTokens = (rawValue) => {
462
+ const mapped = toVarBackedUtilities(rawValue, props.theme)
463
+ return toClassTokens(mapped).map(normalizeRuntimeToken)
464
+ }
465
+
466
+ const replaceClassTokens = (el, currentTokens, nextTokens) => {
467
+ if (!el)
468
+ return nextTokens
469
+ currentTokens
470
+ .flatMap(tokenVariants)
471
+ .forEach(token => el.classList.remove(token))
472
+ const normalizedNextTokens = (nextTokens || []).map(normalizeRuntimeToken)
473
+ normalizedNextTokens.forEach(token => el.classList.add(token))
474
+ return normalizedNextTokens
475
+ }
476
+
477
+ const getAncestorElements = (el) => {
478
+ const ancestors = []
479
+ let parent = el?.parentElement || null
480
+ while (parent && parent !== document.body && parent !== document.documentElement) {
481
+ ancestors.push(parent)
482
+ parent = parent.parentElement
483
+ }
484
+ return ancestors
485
+ }
486
+
487
+ const isScrollableElement = (el) => {
488
+ if (!el || typeof window?.getComputedStyle !== 'function')
489
+ return false
490
+ const styles = window.getComputedStyle(el)
491
+ const overflowY = styles?.overflowY || ''
492
+ const overflow = styles?.overflow || ''
493
+ return /(auto|scroll|overlay)/.test(overflowY) || /(auto|scroll|overlay)/.test(overflow)
494
+ }
495
+
496
+ const collectScrollTargets = (el) => {
497
+ const targets = [window, document, document.documentElement, document.body]
498
+ const ancestors = getAncestorElements(el)
499
+ ancestors.forEach((ancestor) => {
500
+ if (isScrollableElement(ancestor))
501
+ targets.push(ancestor)
502
+ })
503
+ return Array.from(new Set(targets.filter(Boolean)))
504
+ }
505
+
506
+ const resolvePosition = () => {
507
+ const attrValue = String(root.getAttribute('data-cms-nav-position') || '').trim().toLowerCase()
508
+ if (attrValue === 'left' || attrValue === 'center' || attrValue === 'right')
509
+ return attrValue
510
+ if (root.classList.contains('cms-nav-pos-left'))
511
+ return 'left'
512
+ if (root.classList.contains('cms-nav-pos-center'))
513
+ return 'center'
514
+ return 'right'
515
+ }
516
+
517
+ const position = resolvePosition()
518
+ const openClass = root.getAttribute('data-cms-nav-open-class') || 'is-open'
519
+ const panel = root.querySelector('.cms-nav-panel, [data-cms-nav-panel]')
520
+ const overlay = root.querySelector('.cms-nav-overlay, [data-cms-nav-overlay]')
521
+ const toggles = Array.from(root.querySelectorAll('.cms-nav-toggle, [data-cms-nav-toggle]'))
522
+ const closeButtons = Array.from(root.querySelectorAll('.cms-nav-close, [data-cms-nav-close]'))
523
+ const links = Array.from(root.querySelectorAll('.cms-nav-link, [data-cms-nav-link]'))
524
+ const closeOnLink = root.getAttribute('data-cms-nav-close-on-link') !== 'false'
525
+ const navMain = root.querySelector('.cms-nav-main, [data-cms-nav-main], nav')
526
+ const navRow = root.querySelector('.cms-nav-layout, [data-cms-nav-layout], nav > div > div')
527
+ const desktopWrap = root.querySelector('.cms-nav-desktop, [data-cms-nav-desktop]')
528
+ || navRow?.children?.[1]
529
+ || null
530
+ const logoLink = root.querySelector('.cms-nav-logo, [data-cms-nav-logo]')
531
+ || navRow?.querySelector('a')
532
+ || null
533
+ const panelHiddenClass = position === 'left' ? '-translate-x-full' : 'translate-x-full'
534
+ const previewSurface = root.closest('[data-cms-preview-surface]')
535
+ const shouldContainFixedInPreview = Boolean(previewSurface)
536
+ const stickyEnabled = root.classList.contains('cms-nav-sticky') || parseBoolean(root.getAttribute('data-cms-nav-sticky'))
537
+ const getPreviewMode = () => String(previewSurface?.getAttribute('data-cms-preview-mode') || 'preview').toLowerCase()
538
+ const shouldPinInsidePreview = () => shouldContainFixedInPreview && getPreviewMode() === 'preview' && stickyEnabled
539
+ const hideOnDown = root.classList.contains('cms-nav-hide-on-down') || parseBoolean(root.getAttribute('data-cms-nav-hide-on-down'))
540
+ const scrollThreshold = Number(root.getAttribute('data-cms-nav-scroll-threshold') || 10)
541
+ const hideThreshold = Number(root.getAttribute('data-cms-nav-hide-threshold') || 80)
542
+ const hideDelta = Number(root.getAttribute('data-cms-nav-hide-delta') || 6)
543
+ const topNavClassTokens = mapRuntimeTokens(root.getAttribute('data-cms-nav-top-class') || '')
544
+ const scrolledNavClassTokens = mapRuntimeTokens(root.getAttribute('data-cms-nav-scrolled-class') || '')
545
+ const topRowClassTokens = mapRuntimeTokens(root.getAttribute('data-cms-nav-top-row-class') || '')
546
+ const scrolledRowClassTokens = mapRuntimeTokens(root.getAttribute('data-cms-nav-scrolled-row-class') || '')
547
+ const hiddenClassTokens = mapRuntimeTokens(root.getAttribute('data-cms-nav-hidden-class') || '-translate-y-full opacity-0')
548
+ const visibleClassTokens = mapRuntimeTokens(root.getAttribute('data-cms-nav-visible-class') || 'translate-y-0 opacity-100')
549
+ const transitionClassTokens = mapRuntimeTokens(root.getAttribute('data-cms-nav-transition-class') || 'transition-all duration-300')
550
+ const resolvePreviewPinAnchor = () => {
551
+ if (!previewSurface)
552
+ return null
553
+ const candidates = getAncestorElements(previewSurface).filter(isScrollableElement)
554
+ const scrollingCandidate = candidates.find(el => (el.scrollHeight - el.clientHeight) > 1)
555
+ return scrollingCandidate || candidates[0] || previewSurface
556
+ }
557
+ const resolvePreviewScrollTarget = () => {
558
+ if (!previewSurface)
559
+ return null
560
+ if (isScrollableElement(previewSurface) && (previewSurface.scrollHeight - previewSurface.clientHeight) > 1)
561
+ return previewSurface
562
+ const candidates = getAncestorElements(previewSurface).filter(isScrollableElement)
563
+ const scrollingCandidate = candidates.find(el => (el.scrollHeight - el.clientHeight) > 1)
564
+ return scrollingCandidate || candidates[0] || previewSurface
565
+ }
566
+ const previewPinAnchor = resolvePreviewPinAnchor()
567
+ const previewScrollTarget = resolvePreviewScrollTarget()
568
+ const scrollTargets = shouldContainFixedInPreview
569
+ ? Array.from(new Set([previewScrollTarget, previewPinAnchor, previewSurface].filter(Boolean)))
570
+ : collectScrollTargets(root)
571
+ const windowScrollY = () => window.scrollY || document.documentElement.scrollTop || 0
572
+ const readScrollY = () => {
573
+ if (shouldContainFixedInPreview) {
574
+ const scopedTarget = previewScrollTarget || previewSurface
575
+ if (scopedTarget && typeof scopedTarget.scrollTop === 'number')
576
+ return Number(scopedTarget.scrollTop || 0)
577
+ }
578
+ let maxScrollY = windowScrollY()
579
+ scrollTargets.forEach((target) => {
580
+ if (!target || target === window || target === document)
581
+ return
582
+ const current = Number(target.scrollTop || 0)
583
+ if (current > maxScrollY)
584
+ maxScrollY = current
585
+ })
586
+ return maxScrollY
587
+ }
588
+
589
+ const readPinAnchorTop = () => {
590
+ if (!previewPinAnchor || typeof previewPinAnchor.getBoundingClientRect !== 'function')
591
+ return 0
592
+ return Math.round(previewPinAnchor.getBoundingClientRect().top)
593
+ }
594
+
595
+ let appliedNavTokens = []
596
+ let appliedRowTokens = []
597
+ let appliedVisibilityTokens = []
598
+ let lastScrollY = readScrollY()
599
+ let pinnedViewportTop = null
600
+ let pinnedOffsetFromSurface = null
601
+ const resolvePinnedTop = () => {
602
+ const anchorTop = readPinAnchorTop()
603
+ const rootTop = Math.round(root.getBoundingClientRect().top)
604
+ const measuredOffset = Math.max(0, rootTop - anchorTop)
605
+ if (pinnedOffsetFromSurface == null) {
606
+ pinnedOffsetFromSurface = measuredOffset
607
+ }
608
+ pinnedViewportTop = Math.max(anchorTop + pinnedOffsetFromSurface, anchorTop, 0)
609
+ return pinnedViewportTop
610
+ }
611
+ if (shouldPinInsidePreview())
612
+ pinnedViewportTop = resolvePinnedTop()
613
+
614
+ if (navRow) {
615
+ navRow.classList.remove('flex-row', 'flex-row-reverse', 'justify-between', 'justify-center', 'flex-wrap', 'flex-wrap-reverse')
616
+ navRow.classList.add('flex', 'flex-nowrap', 'items-center', 'gap-6')
617
+ if (position === 'left') {
618
+ navRow.classList.add('flex-row-reverse', 'justify-between')
619
+ }
620
+ else if (position === 'center') {
621
+ navRow.classList.add('flex-row', 'justify-center')
622
+ }
623
+ else {
624
+ navRow.classList.add('flex-row', 'justify-between')
625
+ }
626
+ }
627
+
628
+ if (logoLink) {
629
+ logoLink.classList.remove('mr-6')
630
+ if (position === 'center')
631
+ logoLink.classList.add('mr-6')
632
+ }
633
+
634
+ if (desktopWrap) {
635
+ desktopWrap.classList.remove('ml-auto', 'md:flex-1', 'md:pl-6', 'items-center', 'gap-6')
636
+ if (position === 'left') {
637
+ desktopWrap.classList.add('md:flex-1', 'md:pl-6')
638
+ }
639
+ else if (position === 'center') {
640
+ desktopWrap.classList.add('items-center', 'gap-6')
641
+ }
642
+ else {
643
+ desktopWrap.classList.add('ml-auto')
644
+ }
645
+ }
646
+
647
+ const applyNavPinMode = () => {
648
+ if (!navMain)
649
+ return
650
+ if (shouldPinInsidePreview()) {
651
+ // Pin to viewport with explicit left/width so it stays within preview surface.
652
+ root.classList.remove('cms-nav-preview-relative')
653
+ navMain.classList.remove('sticky', 'absolute', 'inset-x-0')
654
+ navMain.classList.add('fixed', 'top-0')
655
+ }
656
+ else if (shouldContainFixedInPreview) {
657
+ // In preview but non-sticky: overlay within preview surface and scroll away naturally.
658
+ root.classList.add('cms-nav-preview-relative')
659
+ navMain.classList.remove('fixed', 'sticky')
660
+ navMain.classList.add('absolute', 'inset-x-0', 'top-0')
661
+ }
662
+ else {
663
+ root.classList.remove('cms-nav-preview-relative')
664
+ navMain.classList.remove('absolute')
665
+ if (stickyEnabled) {
666
+ navMain.classList.remove('sticky')
667
+ navMain.classList.add('fixed', 'inset-x-0', 'top-0')
668
+ }
669
+ }
670
+ }
671
+ applyNavPinMode()
672
+
673
+ const clearPinnedPreviewPosition = () => {
674
+ if (!navMain)
675
+ return
676
+ navMain.style.left = ''
677
+ navMain.style.width = ''
678
+ navMain.style.right = ''
679
+ navMain.style.top = ''
680
+ navMain.style.bottom = ''
681
+ }
682
+
683
+ const updatePinnedPreviewPosition = () => {
684
+ if (!shouldPinInsidePreview() || !navMain || !previewSurface)
685
+ return
686
+ const surfaceRect = previewSurface.getBoundingClientRect()
687
+ if (pinnedViewportTop == null)
688
+ pinnedViewportTop = resolvePinnedTop()
689
+ navMain.style.left = `${Math.round(surfaceRect.left)}px`
690
+ navMain.style.width = `${Math.round(surfaceRect.width)}px`
691
+ navMain.style.right = 'auto'
692
+ navMain.style.bottom = 'auto'
693
+ navMain.style.top = `${pinnedViewportTop}px`
694
+ }
695
+
696
+ if (navMain && transitionClassTokens.length) {
697
+ transitionClassTokens.forEach(token => navMain.classList.add(token))
698
+ }
699
+
700
+ const markInteractive = (el) => {
701
+ if (!el)
702
+ return
703
+ el.setAttribute('data-cms-interactive', 'true')
704
+ }
705
+
706
+ toggles.forEach(markInteractive)
707
+ closeButtons.forEach(markInteractive)
708
+ links.forEach(markInteractive)
709
+ markInteractive(panel)
710
+ markInteractive(overlay)
711
+
712
+ const folderEntries = []
713
+ const helperFolders = Array.from(root.querySelectorAll('.cms-nav-folder, [data-cms-nav-folder]'))
714
+ helperFolders.forEach((folder) => {
715
+ folderEntries.push({
716
+ folder,
717
+ toggle: folder.querySelector('.cms-nav-folder-toggle, [data-cms-nav-folder-toggle]'),
718
+ menu: folder.querySelector('.cms-nav-folder-menu, [data-cms-nav-folder-menu]'),
719
+ })
720
+ })
721
+
722
+ const fallbackFolders = Array.from(root.querySelectorAll('.cms-nav-desktop li.group, [data-cms-nav-desktop] li.group'))
723
+ fallbackFolders.forEach((folder) => {
724
+ if (folderEntries.some(entry => entry.folder === folder))
725
+ return
726
+ const directChildren = Array.from(folder.children || [])
727
+ const toggle = directChildren.find(child => child?.matches?.('a, button, [role="button"]'))
728
+ || folder.querySelector('a, button, [role="button"]')
729
+ const menu = directChildren.find((child) => {
730
+ if (!child?.matches?.('div.hidden, ul.hidden, [hidden], div.absolute, ul.absolute'))
731
+ return false
732
+ const hasItemLinks = child.querySelectorAll('a, button, [role="button"]').length > 0
733
+ return hasItemLinks
734
+ })
735
+ || folder.querySelector('div.hidden, ul.hidden, [hidden]')
736
+ if (!toggle || !menu)
737
+ return
738
+ folderEntries.push({ folder, toggle, menu })
739
+ })
740
+
741
+ const folderCleanupFns = []
742
+ const setFolderOpenState = (folder, menu, open) => {
743
+ folder.classList.toggle('cms-nav-folder-open', open)
744
+ folder.setAttribute('data-cms-nav-folder-open', open ? 'true' : 'false')
745
+ menu.classList.toggle('hidden', !open)
746
+ menu.classList.toggle('block', open)
747
+ menu.classList.toggle('pointer-events-none', !open)
748
+ menu.classList.toggle('pointer-events-auto', open)
749
+ }
750
+
751
+ folderEntries.forEach(({ folder, toggle, menu }) => {
752
+ if (!toggle || !menu)
753
+ return
754
+
755
+ Array.from(menu.classList).forEach((token) => {
756
+ if (token.startsWith('group-hover:') || token.startsWith('group-focus-within:'))
757
+ menu.classList.remove(token)
758
+ })
759
+ folder.classList.add('cms-nav-folder')
760
+ toggle.classList.add('cms-nav-folder-toggle')
761
+ menu.classList.add('cms-nav-folder-menu')
762
+ markInteractive(toggle)
763
+ markInteractive(menu)
764
+ Array.from(menu.querySelectorAll('a, button, [role="button"]')).forEach(markInteractive)
765
+
766
+ let closeTimer = 0
767
+ const clearCloseTimer = () => {
768
+ if (closeTimer) {
769
+ clearTimeout(closeTimer)
770
+ closeTimer = 0
771
+ }
772
+ }
773
+
774
+ const openFolder = () => {
775
+ clearCloseTimer()
776
+ setFolderOpenState(folder, menu, true)
777
+ }
778
+
779
+ const closeFolder = () => {
780
+ clearCloseTimer()
781
+ setFolderOpenState(folder, menu, false)
782
+ }
783
+
784
+ const scheduleCloseFolder = () => {
785
+ clearCloseTimer()
786
+ closeTimer = window.setTimeout(() => {
787
+ closeTimer = 0
788
+ setFolderOpenState(folder, menu, false)
789
+ }, 120)
790
+ }
791
+
792
+ const onPointerEnter = () => {
793
+ openFolder()
794
+ }
795
+
796
+ const onPointerLeave = (event) => {
797
+ const nextTarget = event?.relatedTarget
798
+ if (nextTarget && folder.contains(nextTarget))
799
+ return
800
+ scheduleCloseFolder()
801
+ }
802
+
803
+ const onFocusIn = () => {
804
+ openFolder()
805
+ }
806
+
807
+ const onFocusOut = (event) => {
808
+ const nextTarget = event?.relatedTarget
809
+ if (nextTarget && folder.contains(nextTarget))
810
+ return
811
+ closeFolder()
812
+ }
813
+
814
+ const onToggleClick = (event) => {
815
+ if (event.defaultPrevented)
816
+ return
817
+ if (window.matchMedia('(hover: hover) and (pointer: fine)').matches)
818
+ return
819
+ const isOpenNow = folder.getAttribute('data-cms-nav-folder-open') === 'true'
820
+ if (!isOpenNow) {
821
+ event.preventDefault()
822
+ event.stopPropagation()
823
+ openFolder()
824
+ return
825
+ }
826
+ closeFolder()
827
+ }
828
+
829
+ const onDocumentClickCapture = (event) => {
830
+ const clickTarget = event?.target
831
+ if (clickTarget && folder.contains(clickTarget))
832
+ return
833
+ closeFolder()
834
+ }
835
+
836
+ folder.addEventListener('pointerenter', onPointerEnter)
837
+ folder.addEventListener('pointerleave', onPointerLeave)
838
+ folder.addEventListener('focusin', onFocusIn)
839
+ folder.addEventListener('focusout', onFocusOut)
840
+ toggle.addEventListener('click', onToggleClick)
841
+ document.addEventListener('click', onDocumentClickCapture, true)
842
+
843
+ setFolderOpenState(folder, menu, false)
844
+
845
+ folderCleanupFns.push(() => {
846
+ clearCloseTimer()
847
+ setFolderOpenState(folder, menu, false)
848
+ folder.removeEventListener('pointerenter', onPointerEnter)
849
+ folder.removeEventListener('pointerleave', onPointerLeave)
850
+ folder.removeEventListener('focusin', onFocusIn)
851
+ folder.removeEventListener('focusout', onFocusOut)
852
+ toggle.removeEventListener('click', onToggleClick)
853
+ document.removeEventListener('click', onDocumentClickCapture, true)
854
+ })
855
+ })
856
+
857
+ const isOpen = () => root.classList.contains(openClass)
858
+
859
+ const setScrolledState = (isScrolled) => {
860
+ root.classList.toggle('cms-nav-scrolled', isScrolled)
861
+ root.setAttribute('data-cms-nav-scrolled', isScrolled ? 'true' : 'false')
862
+ appliedNavTokens = replaceClassTokens(navMain, appliedNavTokens, isScrolled ? scrolledNavClassTokens : topNavClassTokens)
863
+ appliedRowTokens = replaceClassTokens(navRow, appliedRowTokens, isScrolled ? scrolledRowClassTokens : topRowClassTokens)
864
+ }
865
+
866
+ const setVisibilityState = (isHidden) => {
867
+ root.classList.toggle('cms-nav-hidden', isHidden)
868
+ root.setAttribute('data-cms-nav-hidden', isHidden ? 'true' : 'false')
869
+ appliedVisibilityTokens = replaceClassTokens(navMain, appliedVisibilityTokens, isHidden ? hiddenClassTokens : visibleClassTokens)
870
+ }
871
+
872
+ const handleScroll = () => {
873
+ const currentScrollY = readScrollY()
874
+ const isScrolled = currentScrollY > scrollThreshold
875
+ setScrolledState(isScrolled)
876
+ updatePinnedPreviewPosition()
877
+
878
+ if (hideOnDown && navMain) {
879
+ const delta = currentScrollY - lastScrollY
880
+ const absDelta = Math.abs(delta)
881
+ if (currentScrollY <= hideThreshold) {
882
+ setVisibilityState(false)
883
+ }
884
+ else if (absDelta >= hideDelta) {
885
+ setVisibilityState(delta > 0)
886
+ }
887
+ }
888
+ else {
889
+ setVisibilityState(false)
890
+ }
891
+
892
+ lastScrollY = currentScrollY
893
+ }
894
+
895
+ const setOpen = (open) => {
896
+ root.classList.toggle(openClass, open)
897
+ root.setAttribute('data-cms-nav-open', open ? 'true' : 'false')
898
+
899
+ toggles.forEach((btn) => {
900
+ btn.setAttribute('aria-expanded', open ? 'true' : 'false')
901
+ })
902
+
903
+ if (panel) {
904
+ panel.classList.remove('left-0', 'right-0', 'right-auto', 'left-auto', 'translate-x-full', '-translate-x-full')
905
+ if (position === 'left') {
906
+ panel.classList.add('left-0', 'right-auto')
907
+ }
908
+ else {
909
+ panel.classList.add('right-0', 'left-auto')
910
+ }
911
+
912
+ panel.classList.toggle('translate-x-0', open)
913
+ panel.classList.toggle('opacity-100', open)
914
+ panel.classList.toggle('pointer-events-auto', open)
915
+ panel.classList.toggle(panelHiddenClass, !open)
916
+ panel.classList.toggle('opacity-0', !open)
917
+ panel.classList.toggle('pointer-events-none', !open)
918
+ panel.setAttribute('aria-hidden', open ? 'false' : 'true')
919
+ }
920
+
921
+ if (overlay) {
922
+ overlay.classList.toggle('opacity-100', open)
923
+ overlay.classList.toggle('pointer-events-auto', open)
924
+ overlay.classList.toggle('opacity-0', !open)
925
+ overlay.classList.toggle('pointer-events-none', !open)
926
+ overlay.setAttribute('aria-hidden', open ? 'false' : 'true')
927
+ }
928
+ }
929
+
930
+ setOpen(root.classList.contains(openClass) || root.getAttribute('data-cms-nav-open') === 'true')
931
+ handleScroll()
932
+ if (shouldPinInsidePreview())
933
+ updatePinnedPreviewPosition()
934
+ else
935
+ clearPinnedPreviewPosition()
936
+
937
+ toggles.forEach((btn) => {
938
+ btn.addEventListener('click', (event) => {
939
+ event.preventDefault()
940
+ event.stopPropagation()
941
+ setOpen(!isOpen())
942
+ })
943
+ })
944
+
945
+ closeButtons.forEach((btn) => {
946
+ btn.addEventListener('click', (event) => {
947
+ event.preventDefault()
948
+ event.stopPropagation()
949
+ setOpen(false)
950
+ })
951
+ })
952
+
953
+ if (overlay) {
954
+ overlay.addEventListener('click', (event) => {
955
+ event.preventDefault()
956
+ event.stopPropagation()
957
+ setOpen(false)
958
+ })
959
+ }
960
+
961
+ if (closeOnLink) {
962
+ links.forEach((link) => {
963
+ link.addEventListener('click', () => {
964
+ setOpen(false)
965
+ })
966
+ })
967
+ }
968
+
969
+ const scrollListeners = scrollTargets
970
+ scrollListeners.forEach((target) => {
971
+ target.addEventListener('scroll', handleScroll, { passive: true })
972
+ })
973
+ let previewModeObserver = null
974
+ let previewSurfaceObserver = null
975
+ let previewSurfaceAttrObserver = null
976
+ let pinSyncRaf = 0
977
+ const schedulePinnedPositionSync = () => {
978
+ if (pinSyncRaf)
979
+ cancelAnimationFrame(pinSyncRaf)
980
+ pinSyncRaf = requestAnimationFrame(() => {
981
+ pinSyncRaf = 0
982
+ updatePinnedPreviewPosition()
983
+ })
984
+ }
985
+ const handlePreviewModeMutation = () => {
986
+ pinnedViewportTop = null
987
+ pinnedOffsetFromSurface = null
988
+ applyNavPinMode()
989
+ if (shouldPinInsidePreview()) {
990
+ updatePinnedPreviewPosition()
991
+ }
992
+ else {
993
+ clearPinnedPreviewPosition()
994
+ }
995
+ handleScroll()
996
+ }
997
+ if (previewSurface) {
998
+ previewModeObserver = new MutationObserver((entries) => {
999
+ if (entries.some(entry => entry.type === 'attributes' && entry.attributeName === 'data-cms-preview-mode'))
1000
+ handlePreviewModeMutation()
1001
+ })
1002
+ previewModeObserver.observe(previewSurface, {
1003
+ attributes: true,
1004
+ attributeFilter: ['data-cms-preview-mode'],
1005
+ })
1006
+ // Viewport-mode switches change preview width/position without a window resize.
1007
+ // Keep fixed nav left/width aligned to the preview surface as it transitions.
1008
+ if (typeof ResizeObserver !== 'undefined') {
1009
+ previewSurfaceObserver = new ResizeObserver(() => {
1010
+ schedulePinnedPositionSync()
1011
+ })
1012
+ previewSurfaceObserver.observe(previewSurface)
1013
+ if (previewPinAnchor && previewPinAnchor !== previewSurface)
1014
+ previewSurfaceObserver.observe(previewPinAnchor)
1015
+ }
1016
+ previewSurfaceAttrObserver = new MutationObserver((entries) => {
1017
+ if (entries.some(entry => entry.type === 'attributes' && (entry.attributeName === 'class' || entry.attributeName === 'style')))
1018
+ schedulePinnedPositionSync()
1019
+ })
1020
+ previewSurfaceAttrObserver.observe(previewSurface, {
1021
+ attributes: true,
1022
+ attributeFilter: ['class', 'style'],
1023
+ })
1024
+ previewSurface.addEventListener('transitionrun', schedulePinnedPositionSync)
1025
+ previewSurface.addEventListener('transitionstart', schedulePinnedPositionSync)
1026
+ previewSurface.addEventListener('transitionend', schedulePinnedPositionSync)
1027
+ previewSurface.addEventListener('transitioncancel', schedulePinnedPositionSync)
1028
+ }
1029
+ const handleResize = () => {
1030
+ if (shouldPinInsidePreview() && pinnedViewportTop == null)
1031
+ pinnedViewportTop = resolvePinnedTop()
1032
+ updatePinnedPreviewPosition()
1033
+ }
1034
+ window.addEventListener('resize', handleResize, { passive: true })
1035
+
1036
+ scope.__cmsNavCleanupFns.push(() => {
1037
+ folderCleanupFns.forEach((cleanup) => {
1038
+ try {
1039
+ cleanup()
1040
+ }
1041
+ catch {}
1042
+ })
1043
+ scrollListeners.forEach((target) => {
1044
+ target.removeEventListener('scroll', handleScroll)
1045
+ })
1046
+ window.removeEventListener('resize', handleResize)
1047
+ if (previewModeObserver)
1048
+ previewModeObserver.disconnect()
1049
+ if (previewSurfaceObserver)
1050
+ previewSurfaceObserver.disconnect()
1051
+ if (previewSurfaceAttrObserver)
1052
+ previewSurfaceAttrObserver.disconnect()
1053
+ if (previewSurface) {
1054
+ previewSurface.removeEventListener('transitionrun', schedulePinnedPositionSync)
1055
+ previewSurface.removeEventListener('transitionstart', schedulePinnedPositionSync)
1056
+ previewSurface.removeEventListener('transitionend', schedulePinnedPositionSync)
1057
+ previewSurface.removeEventListener('transitioncancel', schedulePinnedPositionSync)
1058
+ }
1059
+ if (pinSyncRaf)
1060
+ cancelAnimationFrame(pinSyncRaf)
1061
+ clearPinnedPreviewPosition()
1062
+ })
1063
+ })
1064
+ }
1065
+
342
1066
  function renderSafeHtml(content) {
343
1067
  if (hostEl.value) {
344
1068
  // The HTML is already in the DOM via v-html; just (re)wire behaviors
345
1069
  initEmblaCarousels(hostEl.value)
1070
+ initCmsNavHelpers(hostEl.value)
346
1071
  }
347
1072
  }
348
1073
 
@@ -378,42 +1103,7 @@ function setScopedThemeVars(scopeEl, theme) {
378
1103
  document.head.appendChild(styleEl)
379
1104
  }
380
1105
 
381
- // Build CSS custom properties from theme tokens
382
- const { colors, fontFamily, fontSize, borderRadius, boxShadow } = theme
383
-
384
- const decls = []
385
- // colors
386
- Object.entries(colors).forEach(([k, v]) => {
387
- decls.push(`--color-${k}: ${Array.isArray(v) ? v[0] : v};`)
388
- })
389
- // fonts
390
- Object.entries(fontFamily).forEach(([k, v]) => {
391
- const val = Array.isArray(v) ? v.map(x => (x.includes(' ') ? `'${x}'` : x)).join(', ') : v
392
- decls.push(`--font-${k}: ${val};`)
393
- })
394
- // font sizes
395
- Object.entries(fontSize).forEach(([k, v]) => {
396
- if (Array.isArray(v)) {
397
- const [size, opts] = v
398
- decls.push(`--font-size-${k}: ${size};`)
399
- if (opts && opts.lineHeight)
400
- decls.push(`--line-height-${k}: ${opts.lineHeight};`)
401
- }
402
- else {
403
- decls.push(`--font-size-${k}: ${v};`)
404
- }
405
- })
406
- // radii
407
- Object.entries(borderRadius).forEach(([k, v]) => {
408
- decls.push(`--radius-${k}: ${v};`)
409
- })
410
- // shadows
411
- Object.entries(boxShadow).forEach(([k, v]) => {
412
- decls.push(`--shadow-${k}: ${v};`)
413
- })
414
-
415
- styleEl.textContent = `
416
- [data-theme-scope="${scopeId}"]{${decls.join('')}}`
1106
+ styleEl.textContent = buildScopedThemeCSS(theme, scopeId)
417
1107
  }
418
1108
 
419
1109
  // Convert utility tokens like text-brand/bg-surface/rounded-xl/shadow-card
@@ -446,7 +1136,7 @@ function toVarBackedUtilities(classList, theme) {
446
1136
  }
447
1137
 
448
1138
  if (colorKeys.has(key)) {
449
- const varRef = `var(--color-${key})`
1139
+ const varRef = cssVarRef('color', key)
450
1140
 
451
1141
  // no /opacity → plain var()
452
1142
  if (!opacity) {
@@ -477,7 +1167,7 @@ function toVarBackedUtilities(classList, theme) {
477
1167
  if (radiusMatch) {
478
1168
  const key = radiusMatch[1]
479
1169
  if (radiusKeys.has(key))
480
- return `rounded-[var(--radius-${key})]`
1170
+ return `rounded-[${cssVarRef('radius', key)}]`
481
1171
  return cls
482
1172
  }
483
1173
 
@@ -486,23 +1176,23 @@ function toVarBackedUtilities(classList, theme) {
486
1176
  if (shadowMatch) {
487
1177
  const key = shadowMatch[1]
488
1178
  if (shadowKeys.has(key))
489
- return `shadow-[var(--shadow-${key})]`
1179
+ return `shadow-[${cssVarRef('shadow', key)}]`
490
1180
  return cls
491
1181
  }
492
1182
 
493
1183
  // font families via root apply, including custom keys like "brand"
494
1184
  if (cls === 'font-sans')
495
- return 'font-[var(--font-sans)]'
1185
+ return `font-[${cssVarRef('font', 'sans')}]`
496
1186
  if (cls === 'font-serif')
497
- return 'font-[var(--font-serif)]'
1187
+ return `font-[${cssVarRef('font', 'serif')}]`
498
1188
  if (cls === 'font-mono')
499
- return 'font-[var(--font-mono)]'
1189
+ return `font-[${cssVarRef('font', 'mono')}]`
500
1190
 
501
1191
  const ffMatch = /^font-([\w-]+)$/.exec(cls)
502
1192
  if (ffMatch) {
503
1193
  const key = ffMatch[1]
504
1194
  if (Object.prototype.hasOwnProperty.call(tokens.fontFamily, key))
505
- return `font-[var(--font-${key})]`
1195
+ return `font-[${cssVarRef('font', key)}]`
506
1196
  }
507
1197
 
508
1198
  return cls
@@ -510,6 +1200,35 @@ function toVarBackedUtilities(classList, theme) {
510
1200
  .join(' ')
511
1201
  }
512
1202
 
1203
+ function readElementClass(el) {
1204
+ if (!el)
1205
+ return ''
1206
+ if (typeof el.className === 'string')
1207
+ return el.className
1208
+ if (el.className && typeof el.className.baseVal === 'string')
1209
+ return el.className.baseVal
1210
+ return el.getAttribute('class') || ''
1211
+ }
1212
+
1213
+ function writeElementClass(el, nextClass = '') {
1214
+ if (!el)
1215
+ return
1216
+ const normalized = typeof nextClass === 'string' ? nextClass : String(nextClass || '')
1217
+ if (typeof el.className === 'string') {
1218
+ el.className = normalized
1219
+ return
1220
+ }
1221
+ el.setAttribute('class', normalized)
1222
+ }
1223
+
1224
+ function appendElementClasses(el, classList) {
1225
+ const additions = typeof classList === 'string' ? classList.trim() : ''
1226
+ if (!additions)
1227
+ return
1228
+ const base = readElementClass(el)
1229
+ writeElementClass(el, `${base} ${additions}`.trim())
1230
+ }
1231
+
513
1232
  function applyThemeClasses(scopeEl, theme, variant = 'light', isolated = true) {
514
1233
  if (!scopeEl)
515
1234
  return
@@ -529,7 +1248,7 @@ function applyThemeClasses(scopeEl, theme, variant = 'light', isolated = true) {
529
1248
  if (apply.root) {
530
1249
  const mapped = toVarBackedUtilities(apply.root, t)
531
1250
  if (isolated) {
532
- scopeEl.className = `block-content ${mapped}`.trim()
1251
+ writeElementClass(scopeEl, `block-content ${mapped}`.trim())
533
1252
  }
534
1253
  else {
535
1254
  const applied = (scopeEl.dataset.themeRootClasses || '').split(/\s+/).filter(Boolean)
@@ -547,22 +1266,22 @@ function applyThemeClasses(scopeEl, theme, variant = 'light', isolated = true) {
547
1266
  // Optional convenience: map a few generic applies
548
1267
  if (apply.link) {
549
1268
  scopeEl.querySelectorAll('a').forEach((el) => {
550
- el.className = `${el.className} ${toVarBackedUtilities(apply.link, t)}`.trim()
1269
+ appendElementClasses(el, toVarBackedUtilities(apply.link, t))
551
1270
  })
552
1271
  }
553
1272
  if (apply.heading) {
554
1273
  scopeEl.querySelectorAll('h1,h2,h3,h4,h5,h6').forEach((el) => {
555
- el.className = `${el.className} ${toVarBackedUtilities(apply.heading, t)}`.trim()
1274
+ appendElementClasses(el, toVarBackedUtilities(apply.heading, t))
556
1275
  })
557
1276
  }
558
1277
  if (apply.button) {
559
1278
  scopeEl.querySelectorAll('button,[data-theme="button"]').forEach((el) => {
560
- el.className = `${el.className} ${toVarBackedUtilities(apply.button, t)}`.trim()
1279
+ appendElementClasses(el, toVarBackedUtilities(apply.button, t))
561
1280
  })
562
1281
  }
563
1282
  if (apply.badge) {
564
1283
  scopeEl.querySelectorAll('[data-theme="badge"]').forEach((el) => {
565
- el.className = `${el.className} ${toVarBackedUtilities(apply.badge, t)}`.trim()
1284
+ appendElementClasses(el, toVarBackedUtilities(apply.badge, t))
566
1285
  })
567
1286
  }
568
1287
 
@@ -573,7 +1292,7 @@ function applyThemeClasses(scopeEl, theme, variant = 'light', isolated = true) {
573
1292
  Object.entries(obj).forEach(([part, classes]) => {
574
1293
  const sel = `[data-slot="${slotBase}.${part}"]`
575
1294
  scopeEl.querySelectorAll(sel).forEach((el) => {
576
- el.className = `${el.className} ${toVarBackedUtilities(classes, t)}`.trim()
1295
+ appendElementClasses(el, toVarBackedUtilities(classes, t))
577
1296
  })
578
1297
  })
579
1298
  }
@@ -628,6 +1347,7 @@ function rewriteAllClasses(scopeEl, theme, isolated = true, viewportMode = 'auto
628
1347
  const forcedWidth = viewportModeToWidth(viewportMode)
629
1348
 
630
1349
  const TEXT_SIZE_RE = /^text-(xs|sm|base|lg|xl|\d+xl)$/
1350
+ const FONT_UTILITY_RE = /^font-([\w-]+|\[[^\]]+\])$/
631
1351
 
632
1352
  const mapToken = (token) => {
633
1353
  const parts = token.split(':')
@@ -654,7 +1374,8 @@ function rewriteAllClasses(scopeEl, theme, isolated = true, viewportMode = 'auto
654
1374
 
655
1375
  const mappedCore = toVarBackedUtilities(core, theme)
656
1376
  const isTextSize = TEXT_SIZE_RE.test(nakedCore)
657
- const shouldImportant = hadBreakpoint || isTextSize
1377
+ const isFontUtility = FONT_UTILITY_RE.test(nakedCore)
1378
+ const shouldImportant = hadBreakpoint || isTextSize || isFontUtility
658
1379
  const finalCore = shouldImportant ? importantify(mappedCore) : mappedCore
659
1380
 
660
1381
  return [...nextParts, finalCore].filter(Boolean).join(':')
@@ -691,7 +1412,10 @@ function rewriteAllClasses(scopeEl, theme, isolated = true, viewportMode = 'auto
691
1412
  return ''
692
1413
 
693
1414
  const mappedCore = toVarBackedUtilities(core, theme)
694
- const finalCore = hadBreakpoint ? importantify(mappedCore) : mappedCore
1415
+ const isTextSize = TEXT_SIZE_RE.test(nakedCore)
1416
+ const isFontUtility = FONT_UTILITY_RE.test(nakedCore)
1417
+ const shouldImportant = hadBreakpoint || isTextSize || isFontUtility
1418
+ const finalCore = shouldImportant ? importantify(mappedCore) : mappedCore
695
1419
 
696
1420
  return [...nextParts, finalCore].filter(Boolean).join(':')
697
1421
  }
@@ -699,15 +1423,7 @@ function rewriteAllClasses(scopeEl, theme, isolated = true, viewportMode = 'auto
699
1423
  scopeEl.querySelectorAll('[class]').forEach((el) => {
700
1424
  let base = el.dataset.viewportBaseClass
701
1425
  if (typeof base !== 'string') {
702
- if (typeof el.className === 'string') {
703
- base = el.className
704
- }
705
- else if (el.className && typeof el.className.baseVal === 'string') {
706
- base = el.className.baseVal
707
- }
708
- else {
709
- base = el.getAttribute('class') || ''
710
- }
1426
+ base = readElementClass(el)
711
1427
  el.dataset.viewportBaseClass = base
712
1428
  }
713
1429
  const orig = typeof base === 'string' ? base : String(base || '')
@@ -719,7 +1435,7 @@ function rewriteAllClasses(scopeEl, theme, isolated = true, viewportMode = 'auto
719
1435
  .filter(Boolean)
720
1436
  if (isolated) {
721
1437
  const mapped = mappedTokens.join(' ')
722
- el.className = mapped
1438
+ writeElementClass(el, mapped)
723
1439
  return
724
1440
  }
725
1441
 
@@ -740,16 +1456,16 @@ function rewriteAllClasses(scopeEl, theme, isolated = true, viewportMode = 'auto
740
1456
  onMounted(async () => {
741
1457
  await ensureUnoRuntime()
742
1458
 
743
- // Initialize carousels/behaviors for SSR-inserted HTML
744
- initEmblaCarousels(hostEl.value)
745
-
746
1459
  // Apply global theme once (keeps one style tag for vars; blocks can still override locally if needed)
747
1460
  // setGlobalThemeVars(props.theme)
748
- setScopedThemeVars(hostEl.value, normalizeTheme(props.theme))
1461
+ setScopedThemeVars(hostEl.value, props.theme)
749
1462
  // If you later need per-block overrides, keep the next line; otherwise, it can be omitted.
750
1463
  // setScopedThemeVars(hostEl.value, normalizeTheme(props.theme))
751
1464
  applyThemeClasses(hostEl.value, props.theme, (props.theme && props.theme.variant) || 'light')
752
1465
  rewriteAllClasses(hostEl.value, props.theme, props.isolated, props.viewportMode)
1466
+ // Initialize behaviors after class rewriting so helpers use final class state.
1467
+ initEmblaCarousels(hostEl.value)
1468
+ initCmsNavHelpers(hostEl.value)
753
1469
  await nextTick()
754
1470
  hasMounted = true
755
1471
  notifyLoaded()
@@ -760,12 +1476,13 @@ watch(
760
1476
  async (val) => {
761
1477
  // Wait for DOM to reflect new v-html, then (re)wire behaviors and class mappings
762
1478
  await nextTick()
763
- initEmblaCarousels(hostEl.value)
764
1479
  // setGlobalThemeVars(props.theme)
765
- setScopedThemeVars(hostEl.value, normalizeTheme(props.theme))
1480
+ setScopedThemeVars(hostEl.value, props.theme)
766
1481
 
767
1482
  applyThemeClasses(hostEl.value, props.theme, (props.theme && props.theme.variant) || 'light')
768
1483
  rewriteAllClasses(hostEl.value, props.theme, props.isolated, props.viewportMode)
1484
+ initEmblaCarousels(hostEl.value)
1485
+ initCmsNavHelpers(hostEl.value)
769
1486
  await nextTick()
770
1487
  notifyLoaded()
771
1488
  },
@@ -774,13 +1491,12 @@ watch(
774
1491
  watch(
775
1492
  () => props.theme,
776
1493
  async (val) => {
777
- const t = normalizeTheme(val)
778
- // 1) Write CSS variables globally
779
- // setGlobalThemeVars(t)
780
- setScopedThemeVars(hostEl.value, t)
1494
+ // 1) Write scoped CSS variables from the raw theme object
1495
+ setScopedThemeVars(hostEl.value, val)
781
1496
  // 2) Apply classes based on `apply`, `slots`, and optional variants
782
- applyThemeClasses(hostEl.value, t, (val && val.variant) || 'light')
783
- rewriteAllClasses(hostEl.value, t, props.isolated, props.viewportMode)
1497
+ applyThemeClasses(hostEl.value, val, (val && val.variant) || 'light')
1498
+ rewriteAllClasses(hostEl.value, val, props.isolated, props.viewportMode)
1499
+ initCmsNavHelpers(hostEl.value)
784
1500
  await nextTick()
785
1501
  notifyLoaded()
786
1502
  },
@@ -792,10 +1508,22 @@ watch(
792
1508
  async () => {
793
1509
  await nextTick()
794
1510
  rewriteAllClasses(hostEl.value, props.theme, props.isolated, props.viewportMode)
1511
+ // Viewport button changes can alter preview surface width without a window resize.
1512
+ // Reinitialize nav helpers so fixed nav left/width is recalculated immediately.
1513
+ initCmsNavHelpers(hostEl.value)
795
1514
  },
796
1515
  )
797
1516
 
798
1517
  onBeforeUnmount(() => {
1518
+ if (hostEl.value && Array.isArray(hostEl.value.__cmsNavCleanupFns)) {
1519
+ hostEl.value.__cmsNavCleanupFns.forEach((cleanup) => {
1520
+ try {
1521
+ cleanup()
1522
+ }
1523
+ catch {}
1524
+ })
1525
+ hostEl.value.__cmsNavCleanupFns = []
1526
+ }
799
1527
  // UnoCSS runtime attaches globally; no per-component teardown required.
800
1528
  })
801
1529
  </script>
@@ -812,4 +1540,8 @@ onBeforeUnmount(() => {
812
1540
  p {
813
1541
  margin-bottom: 1em;
814
1542
  }
1543
+
1544
+ .cms-nav-preview-relative {
1545
+ position: relative;
1546
+ }
815
1547
  </style>