@edgedev/create-edge-app 1.2.34 → 1.2.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -397,12 +397,124 @@ function initCmsNavHelpers(scope) {
397
397
  if (!scope || !import.meta.client)
398
398
  return
399
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
+
400
411
  const roots = scope.querySelectorAll('.cms-nav-root, [data-cms-nav-root]')
401
412
  roots.forEach((root) => {
402
- if (root.dataset.cmsNavInit === 'true')
403
- return
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
+ }
404
486
 
405
- root.dataset.cmsNavInit = 'true'
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()
406
518
  const openClass = root.getAttribute('data-cms-nav-open-class') || 'is-open'
407
519
  const panel = root.querySelector('.cms-nav-panel, [data-cms-nav-panel]')
408
520
  const overlay = root.querySelector('.cms-nav-overlay, [data-cms-nav-overlay]')
@@ -410,6 +522,180 @@ function initCmsNavHelpers(scope) {
410
522
  const closeButtons = Array.from(root.querySelectorAll('.cms-nav-close, [data-cms-nav-close]'))
411
523
  const links = Array.from(root.querySelectorAll('.cms-nav-link, [data-cms-nav-link]'))
412
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
+ }
413
699
 
414
700
  const markInteractive = (el) => {
415
701
  if (!el)
@@ -423,8 +709,189 @@ function initCmsNavHelpers(scope) {
423
709
  markInteractive(panel)
424
710
  markInteractive(overlay)
425
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
+
426
857
  const isOpen = () => root.classList.contains(openClass)
427
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
+
428
895
  const setOpen = (open) => {
429
896
  root.classList.toggle(openClass, open)
430
897
  root.setAttribute('data-cms-nav-open', open ? 'true' : 'false')
@@ -434,10 +901,18 @@ function initCmsNavHelpers(scope) {
434
901
  })
435
902
 
436
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
+
437
912
  panel.classList.toggle('translate-x-0', open)
438
913
  panel.classList.toggle('opacity-100', open)
439
914
  panel.classList.toggle('pointer-events-auto', open)
440
- panel.classList.toggle('translate-x-full', !open)
915
+ panel.classList.toggle(panelHiddenClass, !open)
441
916
  panel.classList.toggle('opacity-0', !open)
442
917
  panel.classList.toggle('pointer-events-none', !open)
443
918
  panel.setAttribute('aria-hidden', open ? 'false' : 'true')
@@ -453,6 +928,11 @@ function initCmsNavHelpers(scope) {
453
928
  }
454
929
 
455
930
  setOpen(root.classList.contains(openClass) || root.getAttribute('data-cms-nav-open') === 'true')
931
+ handleScroll()
932
+ if (shouldPinInsidePreview())
933
+ updatePinnedPreviewPosition()
934
+ else
935
+ clearPinnedPreviewPosition()
456
936
 
457
937
  toggles.forEach((btn) => {
458
938
  btn.addEventListener('click', (event) => {
@@ -485,6 +965,101 @@ function initCmsNavHelpers(scope) {
485
965
  })
486
966
  })
487
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
+ })
488
1063
  })
489
1064
  }
490
1065
 
@@ -881,10 +1456,6 @@ function rewriteAllClasses(scopeEl, theme, isolated = true, viewportMode = 'auto
881
1456
  onMounted(async () => {
882
1457
  await ensureUnoRuntime()
883
1458
 
884
- // Initialize carousels/behaviors for SSR-inserted HTML
885
- initEmblaCarousels(hostEl.value)
886
- initCmsNavHelpers(hostEl.value)
887
-
888
1459
  // Apply global theme once (keeps one style tag for vars; blocks can still override locally if needed)
889
1460
  // setGlobalThemeVars(props.theme)
890
1461
  setScopedThemeVars(hostEl.value, props.theme)
@@ -892,6 +1463,9 @@ onMounted(async () => {
892
1463
  // setScopedThemeVars(hostEl.value, normalizeTheme(props.theme))
893
1464
  applyThemeClasses(hostEl.value, props.theme, (props.theme && props.theme.variant) || 'light')
894
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)
895
1469
  await nextTick()
896
1470
  hasMounted = true
897
1471
  notifyLoaded()
@@ -902,13 +1476,13 @@ watch(
902
1476
  async (val) => {
903
1477
  // Wait for DOM to reflect new v-html, then (re)wire behaviors and class mappings
904
1478
  await nextTick()
905
- initEmblaCarousels(hostEl.value)
906
- initCmsNavHelpers(hostEl.value)
907
1479
  // setGlobalThemeVars(props.theme)
908
1480
  setScopedThemeVars(hostEl.value, props.theme)
909
1481
 
910
1482
  applyThemeClasses(hostEl.value, props.theme, (props.theme && props.theme.variant) || 'light')
911
1483
  rewriteAllClasses(hostEl.value, props.theme, props.isolated, props.viewportMode)
1484
+ initEmblaCarousels(hostEl.value)
1485
+ initCmsNavHelpers(hostEl.value)
912
1486
  await nextTick()
913
1487
  notifyLoaded()
914
1488
  },
@@ -922,6 +1496,7 @@ watch(
922
1496
  // 2) Apply classes based on `apply`, `slots`, and optional variants
923
1497
  applyThemeClasses(hostEl.value, val, (val && val.variant) || 'light')
924
1498
  rewriteAllClasses(hostEl.value, val, props.isolated, props.viewportMode)
1499
+ initCmsNavHelpers(hostEl.value)
925
1500
  await nextTick()
926
1501
  notifyLoaded()
927
1502
  },
@@ -933,10 +1508,22 @@ watch(
933
1508
  async () => {
934
1509
  await nextTick()
935
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)
936
1514
  },
937
1515
  )
938
1516
 
939
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
+ }
940
1527
  // UnoCSS runtime attaches globally; no per-component teardown required.
941
1528
  })
942
1529
  </script>
@@ -953,4 +1540,8 @@ onBeforeUnmount(() => {
953
1540
  p {
954
1541
  margin-bottom: 1em;
955
1542
  }
1543
+
1544
+ .cms-nav-preview-relative {
1545
+ position: relative;
1546
+ }
956
1547
  </style>