@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.
- package/README.md +1 -0
- package/agents.md +95 -2
- package/deploy.sh +136 -0
- package/edge/components/cms/block.vue +977 -305
- package/edge/components/cms/blockApi.vue +3 -3
- package/edge/components/cms/blockEditor.vue +688 -86
- package/edge/components/cms/blockPicker.vue +31 -5
- package/edge/components/cms/blockRender.vue +3 -3
- package/edge/components/cms/blocksManager.vue +790 -82
- package/edge/components/cms/codeEditor.vue +15 -6
- package/edge/components/cms/fontUpload.vue +318 -2
- package/edge/components/cms/htmlContent.vue +825 -93
- package/edge/components/cms/init_blocks/contact_us.html +55 -47
- package/edge/components/cms/init_blocks/newsletter.html +56 -96
- package/edge/components/cms/menu.vue +96 -34
- package/edge/components/cms/page.vue +902 -58
- package/edge/components/cms/posts.vue +13 -4
- package/edge/components/cms/site.vue +638 -87
- package/edge/components/cms/siteSettingsForm.vue +19 -9
- package/edge/components/cms/sitesManager.vue +5 -4
- package/edge/components/cms/themeDefaultMenu.vue +20 -2
- package/edge/components/cms/themeEditor.vue +196 -162
- package/edge/components/editor.vue +5 -1
- package/edge/composables/global.ts +37 -5
- package/edge/composables/siteSettingsTemplate.js +2 -0
- package/edge/composables/useCmsNewDocs.js +100 -0
- package/edge/composables/useEdgeCmsDialogPositionFix.js +19 -0
- package/edge/routes/cms/dashboard/blocks/[block].vue +5 -0
- package/edge/routes/cms/dashboard/blocks/index.vue +12 -1
- package/edge/routes/cms/dashboard/media/index.vue +5 -0
- package/edge/routes/cms/dashboard/sites/[site]/[[page]].vue +4 -0
- package/edge/routes/cms/dashboard/sites/[site].vue +4 -0
- package/edge/routes/cms/dashboard/sites/index.vue +4 -0
- package/edge/routes/cms/dashboard/templates/[page].vue +4 -0
- package/edge/routes/cms/dashboard/templates/index.vue +4 -0
- package/edge/routes/cms/dashboard/themes/[theme].vue +5 -0
- package/edge/routes/cms/dashboard/themes/index.vue +330 -1
- package/edge-pull.sh +16 -2
- package/edge-push.sh +9 -1
- package/edge-remote.sh +20 -0
- package/edge-status.sh +9 -5
- package/edge-update-all.sh +127 -0
- package/firebase.json +4 -0
- package/nuxt.config.ts +1 -1
- 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
|
|
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
|
|
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
|
|
138
|
+
pushVarDecl(decls, 'font-size', k, size)
|
|
101
139
|
if (opts && opts.lineHeight)
|
|
102
|
-
decls
|
|
140
|
+
pushVarDecl(decls, 'line-height', k, opts.lineHeight)
|
|
103
141
|
}
|
|
104
142
|
else {
|
|
105
|
-
decls
|
|
143
|
+
pushVarDecl(decls, 'font-size', k, v)
|
|
106
144
|
}
|
|
107
145
|
})
|
|
108
|
-
Object.entries(borderRadius).forEach(([k, v]) => decls
|
|
109
|
-
Object.entries(boxShadow).forEach(([k, v]) => decls
|
|
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
|
|
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
|
|
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
|
|
163
|
+
pushVarDecl(decls, 'font-size', k, size)
|
|
126
164
|
if (opts?.lineHeight)
|
|
127
|
-
decls
|
|
165
|
+
pushVarDecl(decls, 'line-height', k, opts.lineHeight)
|
|
128
166
|
}
|
|
129
167
|
else {
|
|
130
|
-
decls
|
|
168
|
+
pushVarDecl(decls, 'font-size', k, v)
|
|
131
169
|
}
|
|
132
170
|
})
|
|
133
|
-
Object.entries(borderRadius).forEach(([k, v]) => decls
|
|
134
|
-
Object.entries(boxShadow).forEach(([k, v]) => decls
|
|
135
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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-[
|
|
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-[
|
|
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
|
|
1185
|
+
return `font-[${cssVarRef('font', 'sans')}]`
|
|
496
1186
|
if (cls === 'font-serif')
|
|
497
|
-
return
|
|
1187
|
+
return `font-[${cssVarRef('font', 'serif')}]`
|
|
498
1188
|
if (cls === 'font-mono')
|
|
499
|
-
return
|
|
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-[
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
778
|
-
|
|
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,
|
|
783
|
-
rewriteAllClasses(hostEl.value,
|
|
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>
|