@surdeddd/bottom-sheet 0.11.0 → 0.13.0

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 (58) hide show
  1. package/README.md +68 -1
  2. package/dist/{BottomSheetEngine-DzsIwD28.d.cts → BottomSheetCore-CsIjyq7C.d.cts} +61 -5
  3. package/dist/{BottomSheetEngine-DRjCU73e.d.ts → BottomSheetCore-D16lsa6g.d.ts} +61 -5
  4. package/dist/BottomSheetEngine-BYjAVrmd.d.cts +7 -0
  5. package/dist/BottomSheetEngine-YuCNugZn.d.ts +7 -0
  6. package/dist/core.cjs +3476 -0
  7. package/dist/core.d.cts +3 -0
  8. package/dist/core.d.ts +3 -0
  9. package/dist/core.js +3474 -0
  10. package/dist/debug.cjs +109 -0
  11. package/dist/debug.d.cts +22 -0
  12. package/dist/debug.d.ts +22 -0
  13. package/dist/debug.js +107 -0
  14. package/dist/element.cjs +994 -697
  15. package/dist/element.d.cts +2 -1
  16. package/dist/element.d.ts +2 -1
  17. package/dist/element.iife.global.js +2 -2
  18. package/dist/element.js +994 -697
  19. package/dist/features.cjs +533 -0
  20. package/dist/features.d.cts +13 -0
  21. package/dist/features.d.ts +13 -0
  22. package/dist/features.js +526 -0
  23. package/dist/index-DdYLOIIT.d.ts +15 -0
  24. package/dist/index-dfrzw-lI.d.cts +15 -0
  25. package/dist/index.cjs +823 -542
  26. package/dist/index.d.cts +6 -15
  27. package/dist/index.d.ts +6 -15
  28. package/dist/index.js +817 -543
  29. package/dist/integrations/formik.d.cts +1 -1
  30. package/dist/integrations/formik.d.ts +1 -1
  31. package/dist/integrations/react-hook-form.d.cts +1 -1
  32. package/dist/integrations/react-hook-form.d.ts +1 -1
  33. package/dist/overlay.cjs +13 -8
  34. package/dist/overlay.js +13 -8
  35. package/dist/preact.d.cts +2 -1
  36. package/dist/preact.d.ts +2 -1
  37. package/dist/qwik-lib/index.qwik.cjs +4267 -0
  38. package/dist/qwik-lib/index.qwik.mjs +4267 -0
  39. package/dist/qwik.cjs +816 -542
  40. package/dist/qwik.d.cts +2 -2
  41. package/dist/qwik.d.ts +2 -2
  42. package/dist/qwik.js +816 -542
  43. package/dist/react.cjs +816 -542
  44. package/dist/react.d.cts +3 -2
  45. package/dist/react.d.ts +3 -2
  46. package/dist/react.js +816 -542
  47. package/dist/solid.cjs +816 -542
  48. package/dist/solid.d.cts +64 -4
  49. package/dist/solid.d.ts +64 -4
  50. package/dist/solid.js +816 -542
  51. package/dist/solid.source.jsx +816 -542
  52. package/dist/svelte-core.cjs +816 -542
  53. package/dist/svelte-core.js +816 -542
  54. package/dist/svelte.cjs +816 -542
  55. package/dist/svelte.js +816 -542
  56. package/dist/vue.cjs +927 -645
  57. package/dist/vue.js +927 -645
  58. package/package.json +63 -11
package/dist/svelte.cjs CHANGED
@@ -372,74 +372,6 @@ var SnapResolver = class {
372
372
  }
373
373
  };
374
374
 
375
- // src/core/features/persist.ts
376
- function readPersistedId(key) {
377
- if (!key) return null;
378
- if (typeof window === "undefined") return null;
379
- if (typeof localStorage === "undefined") return null;
380
- try {
381
- return localStorage.getItem(key);
382
- } catch {
383
- return null;
384
- }
385
- }
386
- function installPersist(engine, key) {
387
- const unsubscribe = engine.on("snap", ({ id }) => {
388
- if (typeof window === "undefined") return;
389
- if (typeof localStorage === "undefined") return;
390
- try {
391
- localStorage.setItem(key, id);
392
- } catch {
393
- }
394
- });
395
- return unsubscribe;
396
- }
397
-
398
- // src/core/features/auto-collapse.ts
399
- function installAutoCollapse(deps) {
400
- const { ms } = deps;
401
- if (ms === void 0 || ms <= 0) {
402
- return () => {
403
- };
404
- }
405
- let timer = null;
406
- const clearTimer = () => {
407
- if (timer !== null) {
408
- clearTimeout(timer);
409
- timer = null;
410
- }
411
- };
412
- const fire = () => {
413
- if (deps.isDestroyed()) return;
414
- if (deps.isDragging()) return;
415
- const active = deps.resolveSnap(deps.getActiveId());
416
- if (!active || active.size === 0) return;
417
- let target = null;
418
- for (const id of deps.getAllowedIds()) {
419
- const snap = deps.resolveSnap(id);
420
- if (!snap || snap.size <= 0) continue;
421
- if (!target || snap.size < target.size) target = snap;
422
- }
423
- if (!target) return;
424
- if (target.id === active.id || target.size >= active.size) return;
425
- deps.snapTo(target.id);
426
- };
427
- const reset2 = () => {
428
- clearTimer();
429
- if (deps.isDestroyed()) return;
430
- timer = setTimeout(() => {
431
- timer = null;
432
- fire();
433
- }, ms);
434
- };
435
- const unsubscribe = deps.on("snap", () => reset2());
436
- if ((deps.resolveSnap(deps.getActiveId())?.size ?? 0) > 0) reset2();
437
- return () => {
438
- clearTimer();
439
- unsubscribe();
440
- };
441
- }
442
-
443
375
  // src/core/features/linked-sheets.ts
444
376
  function notifyLinkedSheets(sheets, self) {
445
377
  if (sheets.length === 0) return;
@@ -462,390 +394,65 @@ function notifyLinkedSheets(sheets, self) {
462
394
  }
463
395
  }
464
396
 
465
- // src/core/features/history-coordinator.ts
466
- var entries = [];
467
- var subscribers = /* @__PURE__ */ new Set();
468
- var suppress = 0;
469
- var internalDepth = 0;
470
- var pendingRebrand = null;
471
- var listenerInstalled = false;
472
- var patchInstalled = false;
473
- var origPushState = null;
474
- var origReplaceState = null;
475
- var patchedPushState = null;
476
- var patchedReplaceState = null;
477
- var hasWindow = () => typeof window !== "undefined";
478
- var hasHistory = () => typeof history !== "undefined";
479
- var runInternal = (fn) => {
480
- internalDepth += 1;
481
- try {
482
- fn();
483
- } finally {
484
- internalDepth -= 1;
485
- }
486
- };
487
- var notifyRouteChange = () => {
488
- const snapshot = Array.from(subscribers);
489
- for (const fn of snapshot) fn();
490
- };
491
- var ensureListener = () => {
492
- if (listenerInstalled || !hasWindow()) return;
493
- window.addEventListener("popstate", onPopState);
494
- listenerInstalled = true;
495
- };
496
- var maybeTeardownListener = () => {
497
- if (!listenerInstalled) return;
498
- if (entries.length > 0 || subscribers.size > 0 || suppress > 0) return;
499
- if (hasWindow()) window.removeEventListener("popstate", onPopState);
500
- listenerInstalled = false;
501
- };
502
- var installPatch = () => {
503
- if (patchInstalled || !hasHistory()) return;
504
- origPushState = history.pushState;
505
- origReplaceState = history.replaceState;
506
- patchedPushState = function(...args) {
507
- origPushState.apply(this, args);
508
- if (internalDepth === 0) notifyRouteChange();
509
- };
510
- patchedReplaceState = function(...args) {
511
- origReplaceState.apply(this, args);
512
- if (internalDepth === 0) notifyRouteChange();
513
- };
514
- history.pushState = patchedPushState;
515
- history.replaceState = patchedReplaceState;
516
- patchInstalled = true;
517
- };
518
- var removePatch = () => {
519
- if (!patchInstalled) return;
520
- if (history.pushState === patchedPushState && origPushState) {
521
- history.pushState = origPushState;
522
- }
523
- if (history.replaceState === patchedReplaceState && origReplaceState) {
524
- history.replaceState = origReplaceState;
525
- }
526
- origPushState = null;
527
- origReplaceState = null;
528
- patchedPushState = null;
529
- patchedReplaceState = null;
530
- patchInstalled = false;
531
- };
532
- function onPopState() {
533
- if (suppress > 0) {
534
- suppress -= 1;
535
- const rebrand = pendingRebrand;
536
- pendingRebrand = null;
537
- if (rebrand) rebrand();
538
- maybeTeardownListener();
539
- return;
540
- }
541
- if (entries.length === 0) {
542
- notifyRouteChange();
543
- maybeTeardownListener();
544
- return;
397
+ // src/core/features/resize-observer.ts
398
+ function installResizeObserver(deps) {
399
+ if (typeof window === "undefined") {
400
+ return () => {
401
+ };
545
402
  }
546
- let target = null;
547
- let targetIdx = -1;
548
- for (let i = entries.length - 1; i >= 0; i -= 1) {
549
- const entry = entries[i];
550
- if (!entry.surface.isOpen()) continue;
551
- if (!target) {
552
- target = entry;
553
- targetIdx = i;
554
- }
555
- if (entry.surface.isTop()) {
556
- target = entry;
557
- targetIdx = i;
558
- break;
403
+ const onResize = () => {
404
+ if (deps.isDestroyed()) return;
405
+ deps.recomputeSnaps();
406
+ const mode = deps.getMode();
407
+ const isVerticalAxis = mode === "bottom" || mode === "top";
408
+ const viewportSize2 = isVerticalAxis ? window.innerHeight : window.innerWidth;
409
+ const max = deps.getMaxAxisSize();
410
+ if (viewportSize2 > 0 && viewportSize2 < max) {
411
+ deps.setMaxAxisSize(viewportSize2);
559
412
  }
560
- }
561
- if (!target) {
562
- const removed = entries.pop();
563
- if (removed) removed.live = false;
564
- maybeTeardownListener();
565
- return;
566
- }
567
- target.live = false;
568
- entries.splice(targetIdx, 1);
569
- const settled = target;
570
- void Promise.resolve(settled.surface.close()).then(() => {
571
- if (settled.surface.isOpen()) restore(settled);
572
- });
573
- maybeTeardownListener();
574
- }
575
- function restore(entry) {
576
- if (!hasWindow() || !hasHistory()) return;
577
- entry.live = true;
578
- runInternal(() => {
579
- try {
580
- history.pushState(entry.surface.markerState(), "", entry.surface.url);
581
- } catch {
582
- entry.live = false;
413
+ const wasAnimating = deps.isAnimating?.() ?? false;
414
+ deps.cancelInFlight();
415
+ deps.newCycle();
416
+ const current = deps.resolveActiveSnap();
417
+ const newMax = deps.getMaxAxisSize();
418
+ const targetSize = current ? Math.min(current.size, newMax) : Math.min(deps.getSize(), newMax);
419
+ deps.setSize(targetSize);
420
+ if (current && !deps.isDragging()) {
421
+ deps.applySize(targetSize);
422
+ if (wasAnimating) deps.resyncAfterCancel?.();
583
423
  }
584
- });
585
- if (entry.live) {
586
- entries.push(entry);
587
- ensureListener();
424
+ };
425
+ let resizeObserver = null;
426
+ if (typeof ResizeObserver !== "undefined") {
427
+ resizeObserver = new ResizeObserver(onResize);
428
+ resizeObserver.observe(document.documentElement);
429
+ } else {
430
+ window.addEventListener("resize", onResize);
588
431
  }
432
+ window.addEventListener("orientationchange", onResize);
433
+ const onVisibility = () => {
434
+ if (deps.isDestroyed()) return;
435
+ if (document.hidden) deps.cancelInFlight();
436
+ };
437
+ document.addEventListener("visibilitychange", onVisibility);
438
+ return () => {
439
+ resizeObserver?.disconnect();
440
+ window.removeEventListener("resize", onResize);
441
+ window.removeEventListener("orientationchange", onResize);
442
+ document.removeEventListener("visibilitychange", onVisibility);
443
+ };
589
444
  }
590
- function pushBackMarker(surface) {
591
- const entry = { surface, priorUrl: void 0, live: false };
592
- if (!hasWindow() || !hasHistory()) {
593
- return entry;
594
- }
595
- entry.priorUrl = surface.url !== void 0 ? location.href : void 0;
596
- runInternal(() => {
597
- try {
598
- history.pushState(surface.markerState(), "", surface.url);
599
- entry.live = true;
600
- } catch {
601
- entry.live = false;
602
- }
603
- });
604
- if (entry.live) {
605
- entries.push(entry);
606
- ensureListener();
607
- }
608
- return entry;
609
- }
610
- function popBackMarker(handle) {
611
- if (!hasWindow() || !hasHistory()) return;
612
- const entry = handle;
613
- const idx = entries.indexOf(entry);
614
- if (idx === -1 || !entry.live) return;
615
- const last = entries.length - 1;
616
- entry.live = false;
617
- entries.splice(idx, 1);
618
- if (idx !== last) {
619
- const rebrandSurface = entries[entries.length - 1].surface;
620
- const rebrandUrl = entry.priorUrl;
621
- pendingRebrand = () => {
622
- runInternal(() => {
623
- history.replaceState(
624
- rebrandSurface.markerState(),
625
- "",
626
- rebrandUrl !== void 0 ? rebrandUrl : location.href
627
- );
628
- });
629
- };
630
- }
631
- suppress += 1;
632
- runInternal(() => {
633
- try {
634
- history.back();
635
- } catch {
636
- suppress = Math.max(0, suppress - 1);
637
- pendingRebrand = null;
638
- }
639
- });
640
- maybeTeardownListener();
641
- }
642
- function subscribeRouteChange(fn) {
643
- if (!hasWindow() || !hasHistory()) return () => {
644
- };
645
- subscribers.add(fn);
646
- if (subscribers.size === 1) installPatch();
647
- ensureListener();
648
- return () => {
649
- if (!subscribers.has(fn)) return;
650
- subscribers.delete(fn);
651
- if (subscribers.size === 0) removePatch();
652
- maybeTeardownListener();
653
- };
654
- }
655
-
656
- // src/core/features/route.ts
657
- function installRoute(deps) {
658
- if (typeof window === "undefined") {
659
- return () => {
660
- };
661
- }
662
- if (!deps.routedTo && !deps.closeOnBack) {
663
- return () => {
664
- };
665
- }
666
- let handle = null;
667
- const onOpen = () => {
668
- if (handle) return;
669
- handle = pushBackMarker({
670
- isOpen: () => !deps.isDestroyed() && deps.getSize() > 0,
671
- isTop: deps.isTopSheet,
672
- close: deps.close,
673
- markerState: () => deps.routedTo !== void 0 ? { __bsRouted: deps.sheetId } : { __bsSheet: deps.sheetId },
674
- url: deps.routedTo
675
- });
676
- };
677
- const onClose = () => {
678
- if (!handle) return;
679
- popBackMarker(handle);
680
- handle = null;
681
- };
682
- const unsubscribeOpen = deps.on("open", onOpen);
683
- const unsubscribeClose = deps.on("close", onClose);
684
- if (deps.getSize() > 0) onOpen();
685
- return () => {
686
- unsubscribeOpen();
687
- unsubscribeClose();
688
- if (handle) {
689
- popBackMarker(handle);
690
- handle = null;
691
- }
692
- };
693
- }
694
- function installRouteChange(deps) {
695
- return subscribeRouteChange(() => {
696
- if (deps.isDestroyed() || deps.getSize() <= 0) return;
697
- void deps.close();
698
- });
699
- }
700
-
701
- // src/core/features/content-swipe.ts
702
- var CONTENT_PULL_THRESHOLD = 60;
703
- function installContentSwipe(deps) {
704
- const { container } = deps;
705
- let startY = 0;
706
- let startScrollTop = 0;
707
- let moved = false;
708
- const onTouchStart = (e) => {
709
- const t = e.touches[0];
710
- if (!t) return;
711
- startY = t.clientY;
712
- startScrollTop = container.scrollTop;
713
- moved = false;
714
- };
715
- const onTouchMove = (e) => {
716
- const t = e.touches[0];
717
- if (!t) return;
718
- if (Math.abs(t.clientY - startY) > CONTENT_PULL_THRESHOLD) moved = true;
719
- };
720
- const onTouchEnd = (e) => {
721
- if (!moved) return;
722
- if (deps.isDragging() || deps.isAnimating()) return;
723
- if (container.scrollTop !== startScrollTop) return;
724
- const t = e.changedTouches[0];
725
- if (!t) return;
726
- const deltaY = t.clientY - startY;
727
- const atTop = container.scrollTop <= 0;
728
- const allowed = deps.getAllowedIds();
729
- const idx = allowed.indexOf(deps.getActiveId());
730
- if (idx === -1) return;
731
- if (atTop && deltaY > CONTENT_PULL_THRESHOLD && idx > 0) {
732
- deps.snapTo(allowed[idx - 1]);
733
- } else if (deltaY < -CONTENT_PULL_THRESHOLD && idx < allowed.length - 1) {
734
- deps.snapTo(allowed[idx + 1]);
735
- }
736
- };
737
- container.addEventListener("touchstart", onTouchStart, { passive: true });
738
- container.addEventListener("touchmove", onTouchMove, { passive: true });
739
- container.addEventListener("touchend", onTouchEnd, { passive: true });
740
- return () => {
741
- container.removeEventListener("touchstart", onTouchStart);
742
- container.removeEventListener("touchmove", onTouchMove);
743
- container.removeEventListener("touchend", onTouchEnd);
744
- };
745
- }
746
-
747
- // src/core/features/visual-viewport.ts
748
- var SAFETY_PAD = 8;
749
- function installVisualViewport(deps) {
750
- if (typeof window === "undefined" || !window.visualViewport) {
751
- return () => {
752
- };
753
- }
754
- const vv = window.visualViewport;
755
- let rafId = null;
756
- const onResize = () => {
757
- if (rafId !== null) return;
758
- rafId = requestAnimationFrame(() => {
759
- rafId = null;
760
- if (deps.isDestroyed()) return;
761
- deps.recomputeSnaps();
762
- const isVertical = deps.isVerticalAxis();
763
- const visible = isVertical ? vv.height : vv.width;
764
- const windowSize = isVertical ? window.innerHeight : window.innerWidth;
765
- const clamp = Math.max(0, Math.min(windowSize, visible) - SAFETY_PAD);
766
- if (clamp < deps.getMaxAxisSize()) {
767
- deps.setMaxAxisSize(clamp);
768
- }
769
- const wasAnimating = deps.isAnimating?.() ?? false;
770
- deps.cancelInFlight();
771
- deps.newCycle();
772
- const max = deps.getMaxAxisSize();
773
- deps.setSize(Math.min(deps.getSize(), max));
774
- const current = deps.resolveActiveSnap();
775
- if (current && !deps.isDragging()) {
776
- deps.applySize(Math.min(current.size, max));
777
- if (wasAnimating) deps.resyncAfterCancel?.();
778
- } else {
779
- deps.applySize(deps.getSize());
780
- }
781
- });
782
- };
783
- vv.addEventListener("resize", onResize);
784
- return () => {
785
- vv.removeEventListener("resize", onResize);
786
- if (rafId !== null) cancelAnimationFrame(rafId);
787
- };
788
- }
789
-
790
- // src/core/features/resize-observer.ts
791
- function installResizeObserver(deps) {
792
- if (typeof window === "undefined") {
793
- return () => {
794
- };
795
- }
796
- const onResize = () => {
797
- if (deps.isDestroyed()) return;
798
- deps.recomputeSnaps();
799
- const mode = deps.getMode();
800
- const isVerticalAxis = mode === "bottom" || mode === "top";
801
- const viewportSize2 = isVerticalAxis ? window.innerHeight : window.innerWidth;
802
- const max = deps.getMaxAxisSize();
803
- if (viewportSize2 > 0 && viewportSize2 < max) {
804
- deps.setMaxAxisSize(viewportSize2);
805
- }
806
- const wasAnimating = deps.isAnimating?.() ?? false;
807
- deps.cancelInFlight();
808
- deps.newCycle();
809
- const current = deps.resolveActiveSnap();
810
- const newMax = deps.getMaxAxisSize();
811
- const targetSize = current ? Math.min(current.size, newMax) : Math.min(deps.getSize(), newMax);
812
- deps.setSize(targetSize);
813
- if (current && !deps.isDragging()) {
814
- deps.applySize(targetSize);
815
- if (wasAnimating) deps.resyncAfterCancel?.();
816
- }
817
- };
818
- let resizeObserver = null;
819
- if (typeof ResizeObserver !== "undefined") {
820
- resizeObserver = new ResizeObserver(onResize);
821
- resizeObserver.observe(document.documentElement);
822
- } else {
823
- window.addEventListener("resize", onResize);
824
- }
825
- window.addEventListener("orientationchange", onResize);
826
- const onVisibility = () => {
827
- if (deps.isDestroyed()) return;
828
- if (document.hidden) deps.cancelInFlight();
829
- };
830
- document.addEventListener("visibilitychange", onVisibility);
831
- return () => {
832
- resizeObserver?.disconnect();
833
- window.removeEventListener("resize", onResize);
834
- window.removeEventListener("orientationchange", onResize);
835
- document.removeEventListener("visibilitychange", onVisibility);
836
- };
837
- }
838
-
839
- // src/core/features/fit-observer.ts
840
- var contentTargets = (scroller) => {
841
- if (!scroller) return [];
842
- const hasSlot = typeof HTMLSlotElement !== "undefined";
843
- const out = [];
844
- for (const child2 of Array.from(scroller.children)) {
845
- if (hasSlot && child2 instanceof HTMLSlotElement) {
846
- for (const el of child2.assignedElements()) out.push(el);
847
- } else {
848
- out.push(child2);
445
+
446
+ // src/core/features/fit-observer.ts
447
+ var contentTargets = (scroller) => {
448
+ if (!scroller) return [];
449
+ const hasSlot = typeof HTMLSlotElement !== "undefined";
450
+ const out = [];
451
+ for (const child2 of Array.from(scroller.children)) {
452
+ if (hasSlot && child2 instanceof HTMLSlotElement) {
453
+ for (const el of child2.assignedElements()) out.push(el);
454
+ } else {
455
+ out.push(child2);
849
456
  }
850
457
  }
851
458
  return out;
@@ -965,11 +572,15 @@ function createMaxHeightController(deps) {
965
572
 
966
573
  // src/core/features/fit-measurement.ts
967
574
  var measureSheetNatural = (element, content, vertical) => {
968
- if (!content) {
969
- return vertical ? element.offsetHeight : element.offsetWidth;
970
- }
971
575
  const axis = vertical ? "height" : "width";
972
576
  const sheetStyle = element.style;
577
+ if (!content) {
578
+ const prev = sheetStyle[axis];
579
+ sheetStyle[axis] = "auto";
580
+ const poked2 = vertical ? element.offsetHeight : element.offsetWidth;
581
+ sheetStyle[axis] = prev;
582
+ return poked2;
583
+ }
973
584
  const cs = content.style;
974
585
  const prevSheet = sheetStyle[axis];
975
586
  const prevFlex = cs.flex;
@@ -1683,6 +1294,52 @@ function resolveAnimationPreset(animation) {
1683
1294
  }
1684
1295
  }
1685
1296
 
1297
+ // src/core/animation/waapi-settle.ts
1298
+ var SIM_HZ = 240;
1299
+ var SAMPLE_STRIDE = 4;
1300
+ var MAX_SIM_MS = 3e3;
1301
+ function sampleSpringSettle(from, to, velocityPxPerS, config) {
1302
+ const cfg = { ...DEFAULT_SPRING, ...config };
1303
+ const subDt = 1 / SIM_HZ;
1304
+ const stepMs = SAMPLE_STRIDE / SIM_HZ * 1e3;
1305
+ const maxSteps = Math.ceil(MAX_SIM_MS / 1e3 * SIM_HZ);
1306
+ const values = [from];
1307
+ let x = from;
1308
+ let v = velocityPxPerS;
1309
+ for (let i = 1; i <= maxSteps; i += 1) {
1310
+ const force = -cfg.stiffness * (x - to) - cfg.damping * v;
1311
+ v += force / cfg.mass * subDt;
1312
+ x += v * subDt;
1313
+ if (!Number.isFinite(x) || !Number.isFinite(v)) {
1314
+ values.push(to);
1315
+ break;
1316
+ }
1317
+ if (i % SAMPLE_STRIDE === 0) values.push(x);
1318
+ const atRest = Math.abs(to - x) < (cfg.restDelta ?? 0.5) && Math.abs(v) < (cfg.restSpeed ?? 0.5);
1319
+ if (atRest) break;
1320
+ }
1321
+ if (values[values.length - 1] !== to) values.push(to);
1322
+ return {
1323
+ values,
1324
+ stepMs,
1325
+ durationMs: (values.length - 1) * stepMs
1326
+ };
1327
+ }
1328
+ function sampleTweenSettle(from, to, durationMs, easing) {
1329
+ const count = Math.max(2, Math.min(60, Math.round(durationMs / 16)));
1330
+ const values = [];
1331
+ for (let i = 0; i <= count; i += 1) {
1332
+ const t = i / count;
1333
+ values.push(from + (to - from) * easing(t));
1334
+ }
1335
+ values[values.length - 1] = to;
1336
+ return {
1337
+ values,
1338
+ stepMs: durationMs / count,
1339
+ durationMs
1340
+ };
1341
+ }
1342
+
1686
1343
  // src/core/controllers/animation-runner.ts
1687
1344
  var DEFAULT_DURATION2 = 220;
1688
1345
  var VELOCITY_PX_PER_S = 1e3;
@@ -1693,6 +1350,10 @@ var AnimationRunner = class {
1693
1350
  __publicField(this, "applySizeFn");
1694
1351
  __publicField(this, "getSize");
1695
1352
  __publicField(this, "isDragging");
1353
+ __publicField(this, "applyAux");
1354
+ __publicField(this, "getTransformFor");
1355
+ __publicField(this, "settleWaapi");
1356
+ __publicField(this, "currentWaapi", null);
1696
1357
  __publicField(this, "animationKind");
1697
1358
  __publicField(this, "springConfig");
1698
1359
  __publicField(this, "duration");
@@ -1708,6 +1369,9 @@ var AnimationRunner = class {
1708
1369
  this.applySizeFn = deps.applySize;
1709
1370
  this.getSize = deps.getSize;
1710
1371
  this.isDragging = deps.isDragging;
1372
+ this.applyAux = deps.applyAux;
1373
+ this.getTransformFor = deps.getTransformFor;
1374
+ this.settleWaapi = opts.settleAnimation === "waapi";
1711
1375
  const preset = resolveAnimationPreset(opts.animation);
1712
1376
  this.animationKind = preset.kind;
1713
1377
  this.duration = opts.duration ?? preset.duration ?? DEFAULT_DURATION2;
@@ -1732,11 +1396,20 @@ var AnimationRunner = class {
1732
1396
  }
1733
1397
  }
1734
1398
  get isAnimating() {
1735
- return this.currentTween !== null || this.currentSpring !== null;
1399
+ return this.currentTween !== null || this.currentSpring !== null || this.currentWaapi !== null;
1736
1400
  }
1737
1401
  cancel() {
1738
1402
  this.currentTween?.cancel();
1739
1403
  this.currentSpring?.cancel();
1404
+ if (this.currentWaapi) {
1405
+ const entry = this.currentWaapi;
1406
+ this.currentWaapi = null;
1407
+ entry.stop();
1408
+ try {
1409
+ entry.anim.cancel();
1410
+ } catch {
1411
+ }
1412
+ }
1740
1413
  }
1741
1414
  async animateTo(target, velocityPxPerMs) {
1742
1415
  if (target === this.getSize()) {
@@ -1755,7 +1428,28 @@ var AnimationRunner = class {
1755
1428
  if (!this.isDragging()) this.element.style.willChange = "";
1756
1429
  return;
1757
1430
  }
1758
- if (this.animationKind === "spring") {
1431
+ if (this.settleWaapi && this.applyAux && this.getTransformFor && typeof this.element.animate === "function") {
1432
+ const samples = this.animationKind === "spring" ? sampleSpringSettle(
1433
+ this.getSize(),
1434
+ target,
1435
+ velocityPxPerMs * VELOCITY_PX_PER_S,
1436
+ this.springConfig
1437
+ ) : sampleTweenSettle(
1438
+ this.getSize(),
1439
+ target,
1440
+ this.duration,
1441
+ this.easing
1442
+ );
1443
+ if (samples.values.length >= 2 && samples.durationMs > 0) {
1444
+ await this.runWaapiSettle(samples, target);
1445
+ if (!this.isAnimating) {
1446
+ this.getRootEl()?.removeAttribute("data-animating");
1447
+ if (!this.isDragging()) this.element.style.willChange = "";
1448
+ }
1449
+ return;
1450
+ }
1451
+ }
1452
+ if (this.animationKind === "spring") {
1759
1453
  const spring = runSpring({
1760
1454
  from: this.getSize(),
1761
1455
  to: target,
@@ -1783,11 +1477,66 @@ var AnimationRunner = class {
1783
1477
  if (!this.isDragging()) this.element.style.willChange = "";
1784
1478
  }
1785
1479
  }
1480
+ async runWaapiSettle(samples, target) {
1481
+ const applyAux = this.applyAux;
1482
+ const getTransformFor = this.getTransformFor;
1483
+ const frames = samples.values.map((v) => ({
1484
+ transform: getTransformFor(v)
1485
+ }));
1486
+ const anim = this.element.animate(frames, {
1487
+ duration: samples.durationMs,
1488
+ easing: "linear",
1489
+ fill: "forwards"
1490
+ });
1491
+ let stopped = false;
1492
+ let auxRaf = 0;
1493
+ const start = performance.now();
1494
+ const lastIdx = samples.values.length - 1;
1495
+ const auxTick = () => {
1496
+ if (stopped) return;
1497
+ const elapsed = performance.now() - start;
1498
+ const idx = Math.min(Math.floor(elapsed / samples.stepMs), lastIdx);
1499
+ applyAux(samples.values[idx]);
1500
+ auxRaf = requestAnimationFrame(auxTick);
1501
+ };
1502
+ auxRaf = requestAnimationFrame(auxTick);
1503
+ const entry = {
1504
+ anim,
1505
+ stop: () => {
1506
+ stopped = true;
1507
+ cancelAnimationFrame(auxRaf);
1508
+ }
1509
+ };
1510
+ this.currentWaapi = entry;
1511
+ try {
1512
+ await anim.finished;
1513
+ } catch {
1514
+ }
1515
+ entry.stop();
1516
+ const wasCurrent = this.currentWaapi === entry;
1517
+ if (wasCurrent) this.currentWaapi = null;
1518
+ if (wasCurrent && anim.playState === "finished") {
1519
+ this.applySizeFn(target);
1520
+ try {
1521
+ anim.cancel();
1522
+ } catch {
1523
+ }
1524
+ }
1525
+ }
1786
1526
  destroy() {
1787
1527
  this.detachReducedMotion?.();
1788
1528
  this.detachReducedMotion = null;
1789
1529
  this.currentTween = null;
1790
1530
  this.currentSpring = null;
1531
+ if (this.currentWaapi) {
1532
+ const entry = this.currentWaapi;
1533
+ this.currentWaapi = null;
1534
+ entry.stop();
1535
+ try {
1536
+ entry.anim.cancel();
1537
+ } catch {
1538
+ }
1539
+ }
1791
1540
  }
1792
1541
  };
1793
1542
 
@@ -2644,7 +2393,7 @@ function resolveEngineOptions(opts, extras) {
2644
2393
  const initialAllowed = opts.allowed ?? opts.snapPoints.map((p) => p.id);
2645
2394
  let initialId = opts.initial ?? initialAllowed[0] ?? opts.snapPoints[0]?.id ?? "default";
2646
2395
  if (opts.persistKey) {
2647
- const restored = readPersistedId(opts.persistKey);
2396
+ const restored = readPersistedIdSafe(opts.persistKey);
2648
2397
  if (restored && initialAllowed.includes(restored)) {
2649
2398
  initialId = restored;
2650
2399
  }
@@ -2675,6 +2424,7 @@ function resolveEngineOptions(opts, extras) {
2675
2424
  },
2676
2425
  animation: {
2677
2426
  animation: opts.animation,
2427
+ settleAnimation: opts.settleAnimation,
2678
2428
  duration: opts.duration,
2679
2429
  easing: opts.easing,
2680
2430
  spring: opts.spring,
@@ -2692,6 +2442,15 @@ function resolveEngineOptions(opts, extras) {
2692
2442
  }
2693
2443
  };
2694
2444
  }
2445
+ var readPersistedIdSafe = (key) => {
2446
+ if (typeof window === "undefined") return null;
2447
+ if (typeof localStorage === "undefined") return null;
2448
+ try {
2449
+ return localStorage.getItem(key);
2450
+ } catch {
2451
+ return null;
2452
+ }
2453
+ };
2695
2454
 
2696
2455
  // src/core/primitives/teardown-stack.ts
2697
2456
  var TeardownStack = class {
@@ -2714,9 +2473,9 @@ var TeardownStack = class {
2714
2473
  }
2715
2474
  };
2716
2475
 
2717
- // src/core/BottomSheetEngine.ts
2476
+ // src/core/BottomSheetCore.ts
2718
2477
  var HAPTIC_DURATION_MS = 8;
2719
- var BottomSheetEngine = class {
2478
+ var BottomSheetCore = class {
2720
2479
  constructor(opts) {
2721
2480
  __publicField(this, "id", nextInstanceId("bs"));
2722
2481
  __publicField(this, "element");
@@ -2777,6 +2536,8 @@ var BottomSheetEngine = class {
2777
2536
  __publicField(this, "stackEffectPrimed", false);
2778
2537
  __publicField(this, "fitDeps");
2779
2538
  __publicField(this, "transformTemplate");
2539
+ __publicField(this, "featureList", []);
2540
+ __publicField(this, "featureCtx");
2780
2541
  const resolved = resolveEngineOptions(opts, {
2781
2542
  shouldApplyInertSiblings: () => {
2782
2543
  if (typeof document === "undefined") return false;
@@ -2823,7 +2584,9 @@ var BottomSheetEngine = class {
2823
2584
  getRootEl: () => this.rootEl,
2824
2585
  applySize: (size) => this.applySize(size),
2825
2586
  getSize: () => this.size,
2826
- isDragging: () => this.gesture?.isDragging ?? false
2587
+ isDragging: () => this.gesture?.isDragging ?? false,
2588
+ applyAux: (size) => this.applySize(size, true),
2589
+ getTransformFor: (size) => this.transformTemplate(this.snaps.getMaxAxisSize() - size)
2827
2590
  },
2828
2591
  resolved.animation
2829
2592
  );
@@ -2846,6 +2609,11 @@ var BottomSheetEngine = class {
2846
2609
  if (opts.linkedSheets) {
2847
2610
  this.linkedSheets = opts.linkedSheets;
2848
2611
  }
2612
+ const byName = /* @__PURE__ */ new Map();
2613
+ for (const feature of opts.features ?? []) {
2614
+ byName.set(feature.name, feature);
2615
+ }
2616
+ this.featureList = Array.from(byName.values());
2849
2617
  this.activeId = resolved.initialId;
2850
2618
  auditVhUsage(this.snapPointsRaw);
2851
2619
  this.auditDuplicateSnapIds(this.snapPointsRaw);
@@ -2882,6 +2650,7 @@ var BottomSheetEngine = class {
2882
2650
  scrollContainer: this.scrollContainer,
2883
2651
  getMaxAxisSize: () => this.snaps.getMaxAxisSize()
2884
2652
  });
2653
+ this.featureCtx = this.buildFeatureContext(opts.autoCollapseAfter);
2885
2654
  this.attach();
2886
2655
  this.registerInStack();
2887
2656
  if (this.snaps.getMaxAxisSize() === 0 && typeof IntersectionObserver !== "undefined" && typeof document !== "undefined") {
@@ -2890,12 +2659,7 @@ var BottomSheetEngine = class {
2890
2659
  if (!entry.isIntersecting) continue;
2891
2660
  if (this.destroyed) return;
2892
2661
  this.snaps.recompute();
2893
- const current = this.snaps.findById(this.activeId);
2894
- if (current) {
2895
- this.size = current.size;
2896
- this.applySize(this.size);
2897
- this.updateAriaSlider();
2898
- }
2662
+ this.healActiveSnap();
2899
2663
  io.disconnect();
2900
2664
  return;
2901
2665
  }
@@ -2918,46 +2682,77 @@ var BottomSheetEngine = class {
2918
2682
  if (resolved.radius !== void 0) this.setRadius(resolved.radius);
2919
2683
  if (resolved.maxHeight !== void 0) this.setMaxHeight(resolved.maxHeight);
2920
2684
  this.applyAutoAriaLabelledBy();
2921
- if (this.closeOnRouteChange) {
2922
- this.teardowns.add(
2923
- installRouteChange({
2924
- isDestroyed: () => this.destroyed,
2925
- getSize: () => this.size,
2926
- close: () => this.close()
2927
- })
2928
- );
2929
- }
2930
- if (this.persistKey) {
2931
- this.teardowns.add(installPersist(this, this.persistKey));
2685
+ this.runFeatureStage("post");
2686
+ this.warnOrphanFeatureOptions();
2687
+ if (this.size > 0) {
2688
+ this.handleOpen();
2932
2689
  }
2933
- this.teardowns.add(
2934
- installAutoCollapse({
2935
- ms: opts.autoCollapseAfter,
2936
- isDestroyed: () => this.destroyed,
2937
- isDragging: () => this.gesture?.isDragging ?? false,
2938
- getAllowedIds: () => this.snaps.getAllowedIds().slice(),
2939
- getActiveId: () => this.activeId,
2940
- resolveSnap: (id) => this.snaps.findById(id),
2941
- snapTo: (id) => {
2942
- void this.snapTo(id);
2943
- },
2944
- on: (event, fn) => this.on(event, fn)
2945
- })
2946
- );
2947
- this.teardowns.add(
2948
- installRoute({
2690
+ }
2691
+ buildFeatureContext(autoCollapseAfter) {
2692
+ return {
2693
+ element: this.element,
2694
+ scrollContainer: this.scrollContainer,
2695
+ sheetId: this.id,
2696
+ options: {
2949
2697
  routedTo: this.routedTo,
2950
2698
  closeOnBack: this.closeOnBack,
2951
- isTopSheet: () => this.isTopSheet,
2952
- getSize: () => this.size,
2953
- isDestroyed: () => this.destroyed,
2954
- close: () => this.close("back"),
2955
- on: (event, fn) => this.on(event, fn),
2956
- sheetId: this.id
2957
- })
2958
- );
2959
- if (this.size > 0) {
2960
- this.handleOpen();
2699
+ closeOnRouteChange: this.closeOnRouteChange,
2700
+ persistKey: this.persistKey,
2701
+ autoCollapseAfter
2702
+ },
2703
+ isDestroyed: () => this.destroyed,
2704
+ isDragging: () => this.gesture?.isDragging ?? false,
2705
+ isAnimating: () => this.animation.isAnimating,
2706
+ isTopSheet: () => this.isTopSheet,
2707
+ isVerticalAxis: () => this.mode === "bottom" || this.mode === "top",
2708
+ getSize: () => this.size,
2709
+ getActiveId: () => this.activeId,
2710
+ getAllowedIds: () => this.snaps.getAllowedIds().slice(),
2711
+ allowedIdsBySize: () => this.allowedIdsBySize(),
2712
+ getMaxAxisSize: () => this.snaps.getMaxAxisSize(),
2713
+ resolveSnap: (id) => this.snaps.findById(id),
2714
+ resolveActiveSnap: () => this.snaps.findById(this.activeId),
2715
+ setSize: (size) => {
2716
+ this.size = size;
2717
+ },
2718
+ setMaxAxisSize: (size) => {
2719
+ this.snaps.setMaxAxisSize(size);
2720
+ },
2721
+ applySize: (size) => this.applySize(size),
2722
+ recomputeSnaps: () => this.recomputeSnaps(),
2723
+ newCycle: () => {
2724
+ this.newCycle();
2725
+ },
2726
+ cancelInFlight: () => {
2727
+ this.animation.cancel();
2728
+ },
2729
+ resyncAfterResize: () => this.resyncAfterResize(),
2730
+ snapTo: (id) => {
2731
+ void this.snapTo(id);
2732
+ },
2733
+ close: (reason) => this.close(reason),
2734
+ on: (event, fn) => this.on(event, fn),
2735
+ addTeardown: (fn) => this.teardowns.add(fn)
2736
+ };
2737
+ }
2738
+ runFeatureStage(stage) {
2739
+ for (const feature of this.featureList) {
2740
+ if ((feature.stage ?? "post") !== stage) continue;
2741
+ const teardown = feature.install(this.featureCtx);
2742
+ if (teardown) this.teardowns.add(teardown);
2743
+ }
2744
+ }
2745
+ warnOrphanFeatureOptions() {
2746
+ const names = new Set(this.featureList.map((f) => f.name));
2747
+ if ((this.closeOnBack || this.routedTo !== void 0 || this.closeOnRouteChange) && !names.has("route")) {
2748
+ devWarn(
2749
+ "[BottomSheet] closeOnBack/routedTo/closeOnRouteChange need routeFeature() \u2014 option ignored"
2750
+ );
2751
+ }
2752
+ if (this.persistKey && !names.has("persist")) {
2753
+ devWarn(
2754
+ "[BottomSheet] persistKey needs persistFeature() \u2014 snaps will not be saved"
2755
+ );
2961
2756
  }
2962
2757
  }
2963
2758
  get state() {
@@ -3059,6 +2854,8 @@ var BottomSheetEngine = class {
3059
2854
  const vt = document.startViewTransition(() => {
3060
2855
  this.applySize(target.size);
3061
2856
  });
2857
+ vt.ready?.catch(() => {
2858
+ });
3062
2859
  this.currentViewTransition = vt;
3063
2860
  try {
3064
2861
  await vt.finished;
@@ -3310,11 +3107,16 @@ var BottomSheetEngine = class {
3310
3107
  this.maxHeight.clampTo();
3311
3108
  this.scrim.invalidateOpacityCache();
3312
3109
  if (this.gesture?.isDragging) return;
3110
+ this.healActiveSnap();
3111
+ }
3112
+ healActiveSnap() {
3313
3113
  const current = this.snaps.findById(this.activeId);
3314
- if (current) {
3315
- this.size = current.size;
3316
- this.applySize(this.size);
3317
- this.updateAriaSlider();
3114
+ if (!current) return;
3115
+ this.size = current.size;
3116
+ this.applySize(this.size);
3117
+ this.updateAriaSlider();
3118
+ if (this.size > 0 && !this.lifecycle.isInstalled && !this.opening && !this.animation.isAnimating) {
3119
+ this.emitOpenSequence(this.activeId);
3318
3120
  }
3319
3121
  }
3320
3122
  destroy() {
@@ -3493,18 +3295,7 @@ var BottomSheetEngine = class {
3493
3295
  }
3494
3296
  }
3495
3297
  attachFeatures() {
3496
- if (this.scrollContainer) {
3497
- this.teardowns.add(installContentSwipe({
3498
- container: this.scrollContainer,
3499
- isDragging: () => this.gesture?.isDragging ?? false,
3500
- isAnimating: () => this.animation.isAnimating,
3501
- getAllowedIds: () => this.allowedIdsBySize(),
3502
- getActiveId: () => this.activeId,
3503
- snapTo: (id) => {
3504
- void this.snapTo(id);
3505
- }
3506
- }));
3507
- }
3298
+ this.runFeatureStage("attach");
3508
3299
  if (typeof window !== "undefined") {
3509
3300
  this.teardowns.add(installResizeObserver({
3510
3301
  element: this.element,
@@ -3544,31 +3335,6 @@ var BottomSheetEngine = class {
3544
3335
  () => document.removeEventListener("keydown", onKey)
3545
3336
  );
3546
3337
  }
3547
- this.teardowns.add(installVisualViewport({
3548
- element: this.element,
3549
- isVerticalAxis: () => this.mode === "bottom" || this.mode === "top",
3550
- isDestroyed: () => this.destroyed,
3551
- isDragging: () => this.gesture?.isDragging ?? false,
3552
- recomputeSnaps: () => this.recomputeSnaps(),
3553
- resolveActiveSnap: () => this.snaps.findById(this.activeId),
3554
- getMaxAxisSize: () => this.snaps.getMaxAxisSize(),
3555
- getSize: () => this.size,
3556
- setMaxAxisSize: (size) => {
3557
- this.snaps.setMaxAxisSize(size);
3558
- },
3559
- setSize: (size) => {
3560
- this.size = size;
3561
- },
3562
- applySize: (size) => this.applySize(size),
3563
- cancelInFlight: () => {
3564
- this.animation.cancel();
3565
- },
3566
- newCycle: () => {
3567
- this.newCycle();
3568
- },
3569
- isAnimating: () => this.animation.isAnimating,
3570
- resyncAfterCancel: () => this.resyncAfterResize()
3571
- }));
3572
3338
  }
3573
3339
  this.teardowns.add(installSliderKeyboard({
3574
3340
  handle: this.handle,
@@ -3645,13 +3411,13 @@ var BottomSheetEngine = class {
3645
3411
  }
3646
3412
  }
3647
3413
  }
3648
- applySize(size) {
3414
+ applySize(size, skipTransform = false) {
3649
3415
  const cap = this.snaps.getMaxAxisSize();
3650
3416
  const clamped = cap > 0 && size > cap && !this.gesture?.isDragging && !this.allowOvershoot ? cap : size;
3651
3417
  this.size = clamped;
3652
3418
  const offset = cap - clamped;
3653
3419
  const style = this.element.style;
3654
- style.transform = this.transformTemplate(offset);
3420
+ if (!skipTransform) style.transform = this.transformTemplate(offset);
3655
3421
  if (this.sizeWriteSentinel.shouldWrite(clamped, SIZE_WRITE_EPSILON)) {
3656
3422
  style.setProperty("--bs-size", `${clamped}px`);
3657
3423
  if (this.scrimParent) {
@@ -3729,6 +3495,514 @@ var BottomSheetEngine = class {
3729
3495
  }
3730
3496
  };
3731
3497
 
3498
+ // src/core/features/history-coordinator.ts
3499
+ var entries = [];
3500
+ var subscribers = /* @__PURE__ */ new Set();
3501
+ var suppress = 0;
3502
+ var internalDepth = 0;
3503
+ var pendingRebrands = [];
3504
+ var listenerInstalled = false;
3505
+ var patchInstalled = false;
3506
+ var origPushState = null;
3507
+ var origReplaceState = null;
3508
+ var patchedPushState = null;
3509
+ var patchedReplaceState = null;
3510
+ var hasWindow = () => typeof window !== "undefined";
3511
+ var hasHistory = () => typeof history !== "undefined";
3512
+ var brandMarker = (surface) => ({
3513
+ ...surface.markerState(),
3514
+ __bs: true
3515
+ });
3516
+ var runInternal = (fn) => {
3517
+ internalDepth += 1;
3518
+ try {
3519
+ fn();
3520
+ } finally {
3521
+ internalDepth -= 1;
3522
+ }
3523
+ };
3524
+ var notifyRouteChange = () => {
3525
+ const snapshot = Array.from(subscribers);
3526
+ for (const fn of snapshot) fn();
3527
+ };
3528
+ var ensureListener = () => {
3529
+ if (listenerInstalled || !hasWindow()) return;
3530
+ window.addEventListener("popstate", onPopState);
3531
+ listenerInstalled = true;
3532
+ };
3533
+ var maybeTeardownListener = () => {
3534
+ if (!listenerInstalled) return;
3535
+ if (entries.length > 0 || subscribers.size > 0 || suppress > 0) return;
3536
+ if (hasWindow()) window.removeEventListener("popstate", onPopState);
3537
+ listenerInstalled = false;
3538
+ };
3539
+ var installPatch = () => {
3540
+ if (patchInstalled || !hasHistory()) return;
3541
+ origPushState = history.pushState;
3542
+ origReplaceState = history.replaceState;
3543
+ patchedPushState = function(...args) {
3544
+ origPushState.apply(this, args);
3545
+ if (internalDepth === 0) notifyRouteChange();
3546
+ };
3547
+ patchedReplaceState = function(...args) {
3548
+ origReplaceState.apply(this, args);
3549
+ if (internalDepth === 0) notifyRouteChange();
3550
+ };
3551
+ history.pushState = patchedPushState;
3552
+ history.replaceState = patchedReplaceState;
3553
+ patchInstalled = true;
3554
+ };
3555
+ var removePatch = () => {
3556
+ if (!patchInstalled) return;
3557
+ if (history.pushState === patchedPushState && origPushState) {
3558
+ history.pushState = origPushState;
3559
+ }
3560
+ if (history.replaceState === patchedReplaceState && origReplaceState) {
3561
+ history.replaceState = origReplaceState;
3562
+ }
3563
+ origPushState = null;
3564
+ origReplaceState = null;
3565
+ patchedPushState = null;
3566
+ patchedReplaceState = null;
3567
+ patchInstalled = false;
3568
+ };
3569
+ function onPopState() {
3570
+ if (suppress > 0) {
3571
+ suppress -= 1;
3572
+ const rebrand = pendingRebrands.shift();
3573
+ if (rebrand) rebrand();
3574
+ maybeTeardownListener();
3575
+ return;
3576
+ }
3577
+ if (entries.length === 0) {
3578
+ notifyRouteChange();
3579
+ maybeTeardownListener();
3580
+ return;
3581
+ }
3582
+ let target = null;
3583
+ let targetIdx = -1;
3584
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
3585
+ const entry = entries[i];
3586
+ if (!entry.surface.isOpen()) continue;
3587
+ if (!target) {
3588
+ target = entry;
3589
+ targetIdx = i;
3590
+ }
3591
+ if (entry.surface.isTop()) {
3592
+ target = entry;
3593
+ targetIdx = i;
3594
+ break;
3595
+ }
3596
+ }
3597
+ if (!target) {
3598
+ const removed = entries.pop();
3599
+ if (removed) removed.live = false;
3600
+ maybeTeardownListener();
3601
+ return;
3602
+ }
3603
+ target.live = false;
3604
+ entries.splice(targetIdx, 1);
3605
+ const settled = target;
3606
+ void Promise.resolve(settled.surface.close()).then(() => {
3607
+ if (settled.surface.isOpen()) restore(settled);
3608
+ });
3609
+ maybeTeardownListener();
3610
+ }
3611
+ function restore(entry) {
3612
+ if (!hasWindow() || !hasHistory()) return;
3613
+ entry.live = true;
3614
+ runInternal(() => {
3615
+ try {
3616
+ history.pushState(brandMarker(entry.surface), "", entry.surface.url);
3617
+ } catch {
3618
+ entry.live = false;
3619
+ }
3620
+ });
3621
+ if (entry.live) {
3622
+ entries.push(entry);
3623
+ ensureListener();
3624
+ }
3625
+ }
3626
+ function pushBackMarker(surface) {
3627
+ const entry = { surface, priorUrl: void 0, live: false };
3628
+ if (!hasWindow() || !hasHistory()) {
3629
+ return entry;
3630
+ }
3631
+ entry.priorUrl = surface.url !== void 0 ? location.href : void 0;
3632
+ runInternal(() => {
3633
+ try {
3634
+ history.pushState(brandMarker(surface), "", surface.url);
3635
+ entry.live = true;
3636
+ } catch {
3637
+ entry.live = false;
3638
+ }
3639
+ });
3640
+ if (entry.live) {
3641
+ entries.push(entry);
3642
+ ensureListener();
3643
+ }
3644
+ return entry;
3645
+ }
3646
+ function popBackMarker(handle) {
3647
+ if (!hasWindow() || !hasHistory()) return;
3648
+ const entry = handle;
3649
+ const idx = entries.indexOf(entry);
3650
+ if (idx === -1 || !entry.live) return;
3651
+ const last = entries.length - 1;
3652
+ entry.live = false;
3653
+ entries.splice(idx, 1);
3654
+ let rebrand = null;
3655
+ if (idx !== last) {
3656
+ const rebrandSurface = entries[entries.length - 1].surface;
3657
+ const rebrandUrl = entry.priorUrl;
3658
+ rebrand = () => {
3659
+ runInternal(() => {
3660
+ history.replaceState(
3661
+ brandMarker(rebrandSurface),
3662
+ "",
3663
+ rebrandUrl !== void 0 ? rebrandUrl : location.href
3664
+ );
3665
+ });
3666
+ };
3667
+ }
3668
+ pendingRebrands.push(rebrand);
3669
+ suppress += 1;
3670
+ runInternal(() => {
3671
+ try {
3672
+ history.back();
3673
+ } catch {
3674
+ suppress = Math.max(0, suppress - 1);
3675
+ pendingRebrands.pop();
3676
+ }
3677
+ });
3678
+ maybeTeardownListener();
3679
+ }
3680
+ function subscribeRouteChange(fn) {
3681
+ if (!hasWindow() || !hasHistory()) return () => {
3682
+ };
3683
+ subscribers.add(fn);
3684
+ if (subscribers.size === 1) installPatch();
3685
+ ensureListener();
3686
+ return () => {
3687
+ if (!subscribers.has(fn)) return;
3688
+ subscribers.delete(fn);
3689
+ if (subscribers.size === 0) removePatch();
3690
+ maybeTeardownListener();
3691
+ };
3692
+ }
3693
+
3694
+ // src/core/features/route.ts
3695
+ function installRoute(deps) {
3696
+ if (typeof window === "undefined") {
3697
+ return () => {
3698
+ };
3699
+ }
3700
+ if (!deps.routedTo && !deps.closeOnBack) {
3701
+ return () => {
3702
+ };
3703
+ }
3704
+ let handle = null;
3705
+ const onOpen = () => {
3706
+ if (handle) return;
3707
+ handle = pushBackMarker({
3708
+ isOpen: () => !deps.isDestroyed() && deps.getSize() > 0,
3709
+ isTop: deps.isTopSheet,
3710
+ close: deps.close,
3711
+ markerState: () => deps.routedTo !== void 0 ? { __bsRouted: deps.sheetId } : { __bsSheet: deps.sheetId },
3712
+ url: deps.routedTo
3713
+ });
3714
+ };
3715
+ const onClose = () => {
3716
+ if (!handle) return;
3717
+ popBackMarker(handle);
3718
+ handle = null;
3719
+ };
3720
+ const unsubscribeOpen = deps.on("open", onOpen);
3721
+ const unsubscribeClose = deps.on("close", onClose);
3722
+ if (deps.getSize() > 0) onOpen();
3723
+ return () => {
3724
+ unsubscribeOpen();
3725
+ unsubscribeClose();
3726
+ if (handle) {
3727
+ popBackMarker(handle);
3728
+ handle = null;
3729
+ }
3730
+ };
3731
+ }
3732
+ function installRouteChange(deps) {
3733
+ return subscribeRouteChange(() => {
3734
+ if (deps.isDestroyed() || deps.getSize() <= 0) return;
3735
+ void deps.close();
3736
+ });
3737
+ }
3738
+
3739
+ // src/core/features/persist.ts
3740
+ function installPersist(engine, key) {
3741
+ const unsubscribe = engine.on("snap", ({ id }) => {
3742
+ if (typeof window === "undefined") return;
3743
+ if (typeof localStorage === "undefined") return;
3744
+ try {
3745
+ localStorage.setItem(key, id);
3746
+ } catch {
3747
+ }
3748
+ });
3749
+ return unsubscribe;
3750
+ }
3751
+
3752
+ // src/core/features/auto-collapse.ts
3753
+ function installAutoCollapse(deps) {
3754
+ const { ms } = deps;
3755
+ if (ms === void 0 || ms <= 0) {
3756
+ return () => {
3757
+ };
3758
+ }
3759
+ let timer = null;
3760
+ const clearTimer = () => {
3761
+ if (timer !== null) {
3762
+ clearTimeout(timer);
3763
+ timer = null;
3764
+ }
3765
+ };
3766
+ const fire = () => {
3767
+ if (deps.isDestroyed()) return;
3768
+ if (deps.isDragging()) return;
3769
+ const active = deps.resolveSnap(deps.getActiveId());
3770
+ if (!active || active.size === 0) return;
3771
+ let target = null;
3772
+ for (const id of deps.getAllowedIds()) {
3773
+ const snap = deps.resolveSnap(id);
3774
+ if (!snap || snap.size <= 0) continue;
3775
+ if (!target || snap.size < target.size) target = snap;
3776
+ }
3777
+ if (!target) return;
3778
+ if (target.id === active.id || target.size >= active.size) return;
3779
+ deps.snapTo(target.id);
3780
+ };
3781
+ const reset2 = () => {
3782
+ clearTimer();
3783
+ if (deps.isDestroyed()) return;
3784
+ timer = setTimeout(() => {
3785
+ timer = null;
3786
+ fire();
3787
+ }, ms);
3788
+ };
3789
+ const unsubscribe = deps.on("snap", () => reset2());
3790
+ if ((deps.resolveSnap(deps.getActiveId())?.size ?? 0) > 0) reset2();
3791
+ return () => {
3792
+ clearTimer();
3793
+ unsubscribe();
3794
+ };
3795
+ }
3796
+
3797
+ // src/core/features/content-swipe.ts
3798
+ var CONTENT_PULL_THRESHOLD = 60;
3799
+ function installContentSwipe(deps) {
3800
+ const { container } = deps;
3801
+ let startY = 0;
3802
+ let startScrollTop = 0;
3803
+ let moved = false;
3804
+ const onTouchStart = (e) => {
3805
+ const t = e.touches[0];
3806
+ if (!t) return;
3807
+ startY = t.clientY;
3808
+ startScrollTop = container.scrollTop;
3809
+ moved = false;
3810
+ };
3811
+ const onTouchMove = (e) => {
3812
+ const t = e.touches[0];
3813
+ if (!t) return;
3814
+ if (Math.abs(t.clientY - startY) > CONTENT_PULL_THRESHOLD) moved = true;
3815
+ };
3816
+ const onTouchEnd = (e) => {
3817
+ if (!moved) return;
3818
+ if (deps.isDragging() || deps.isAnimating()) return;
3819
+ if (container.scrollTop !== startScrollTop) return;
3820
+ const t = e.changedTouches[0];
3821
+ if (!t) return;
3822
+ const deltaY = t.clientY - startY;
3823
+ const atTop = container.scrollTop <= 0;
3824
+ const allowed = deps.getAllowedIds();
3825
+ const idx = allowed.indexOf(deps.getActiveId());
3826
+ if (idx === -1) return;
3827
+ if (atTop && deltaY > CONTENT_PULL_THRESHOLD && idx > 0) {
3828
+ deps.snapTo(allowed[idx - 1]);
3829
+ } else if (deltaY < -CONTENT_PULL_THRESHOLD && idx < allowed.length - 1) {
3830
+ deps.snapTo(allowed[idx + 1]);
3831
+ }
3832
+ };
3833
+ container.addEventListener("touchstart", onTouchStart, { passive: true });
3834
+ container.addEventListener("touchmove", onTouchMove, { passive: true });
3835
+ container.addEventListener("touchend", onTouchEnd, { passive: true });
3836
+ return () => {
3837
+ container.removeEventListener("touchstart", onTouchStart);
3838
+ container.removeEventListener("touchmove", onTouchMove);
3839
+ container.removeEventListener("touchend", onTouchEnd);
3840
+ };
3841
+ }
3842
+
3843
+ // src/core/features/visual-viewport.ts
3844
+ var SAFETY_PAD = 8;
3845
+ function installVisualViewport(deps) {
3846
+ if (typeof window === "undefined" || !window.visualViewport) {
3847
+ return () => {
3848
+ };
3849
+ }
3850
+ const vv = window.visualViewport;
3851
+ let rafId = null;
3852
+ const onResize = () => {
3853
+ if (rafId !== null) return;
3854
+ rafId = requestAnimationFrame(() => {
3855
+ rafId = null;
3856
+ if (deps.isDestroyed()) return;
3857
+ deps.recomputeSnaps();
3858
+ const isVertical = deps.isVerticalAxis();
3859
+ const visible = isVertical ? vv.height : vv.width;
3860
+ const windowSize = isVertical ? window.innerHeight : window.innerWidth;
3861
+ const clamp = Math.max(0, Math.min(windowSize, visible) - SAFETY_PAD);
3862
+ if (clamp < deps.getMaxAxisSize()) {
3863
+ deps.setMaxAxisSize(clamp);
3864
+ }
3865
+ const wasAnimating = deps.isAnimating?.() ?? false;
3866
+ deps.cancelInFlight();
3867
+ deps.newCycle();
3868
+ const max = deps.getMaxAxisSize();
3869
+ deps.setSize(Math.min(deps.getSize(), max));
3870
+ const current = deps.resolveActiveSnap();
3871
+ if (current && !deps.isDragging()) {
3872
+ deps.applySize(Math.min(current.size, max));
3873
+ if (wasAnimating) deps.resyncAfterCancel?.();
3874
+ } else {
3875
+ deps.applySize(deps.getSize());
3876
+ }
3877
+ });
3878
+ };
3879
+ vv.addEventListener("resize", onResize);
3880
+ return () => {
3881
+ vv.removeEventListener("resize", onResize);
3882
+ if (rafId !== null) cancelAnimationFrame(rafId);
3883
+ };
3884
+ }
3885
+
3886
+ // src/core/features/engine-features.ts
3887
+ function routeFeature() {
3888
+ return {
3889
+ name: "route",
3890
+ install: (ctx) => {
3891
+ const teardowns = new TeardownStack();
3892
+ if (ctx.options.closeOnRouteChange) {
3893
+ teardowns.add(
3894
+ installRouteChange({
3895
+ isDestroyed: ctx.isDestroyed,
3896
+ getSize: ctx.getSize,
3897
+ close: () => ctx.close()
3898
+ })
3899
+ );
3900
+ }
3901
+ teardowns.add(
3902
+ installRoute({
3903
+ routedTo: ctx.options.routedTo,
3904
+ closeOnBack: ctx.options.closeOnBack,
3905
+ isTopSheet: ctx.isTopSheet,
3906
+ getSize: ctx.getSize,
3907
+ isDestroyed: ctx.isDestroyed,
3908
+ close: () => ctx.close("back"),
3909
+ on: ctx.on,
3910
+ sheetId: ctx.sheetId
3911
+ })
3912
+ );
3913
+ return () => teardowns.drain();
3914
+ }
3915
+ };
3916
+ }
3917
+ function persistFeature() {
3918
+ return {
3919
+ name: "persist",
3920
+ install: (ctx) => {
3921
+ const key = ctx.options.persistKey;
3922
+ if (!key) return;
3923
+ return installPersist({ on: ctx.on }, key);
3924
+ }
3925
+ };
3926
+ }
3927
+ function autoCollapseFeature() {
3928
+ return {
3929
+ name: "auto-collapse",
3930
+ install: (ctx) => installAutoCollapse({
3931
+ ms: ctx.options.autoCollapseAfter,
3932
+ isDestroyed: ctx.isDestroyed,
3933
+ isDragging: ctx.isDragging,
3934
+ getAllowedIds: ctx.getAllowedIds,
3935
+ getActiveId: ctx.getActiveId,
3936
+ resolveSnap: ctx.resolveSnap,
3937
+ snapTo: ctx.snapTo,
3938
+ on: ctx.on
3939
+ })
3940
+ };
3941
+ }
3942
+ function contentSwipeFeature() {
3943
+ return {
3944
+ name: "content-swipe",
3945
+ stage: "attach",
3946
+ install: (ctx) => {
3947
+ if (!ctx.scrollContainer) return;
3948
+ return installContentSwipe({
3949
+ container: ctx.scrollContainer,
3950
+ isDragging: ctx.isDragging,
3951
+ isAnimating: ctx.isAnimating,
3952
+ getAllowedIds: ctx.allowedIdsBySize,
3953
+ getActiveId: ctx.getActiveId,
3954
+ snapTo: ctx.snapTo
3955
+ });
3956
+ }
3957
+ };
3958
+ }
3959
+ function visualViewportFeature() {
3960
+ return {
3961
+ name: "visual-viewport",
3962
+ install: (ctx) => {
3963
+ if (typeof window === "undefined") return;
3964
+ return installVisualViewport({
3965
+ element: ctx.element,
3966
+ isVerticalAxis: ctx.isVerticalAxis,
3967
+ isDestroyed: ctx.isDestroyed,
3968
+ isDragging: ctx.isDragging,
3969
+ recomputeSnaps: ctx.recomputeSnaps,
3970
+ resolveActiveSnap: ctx.resolveActiveSnap,
3971
+ getMaxAxisSize: ctx.getMaxAxisSize,
3972
+ getSize: ctx.getSize,
3973
+ setMaxAxisSize: ctx.setMaxAxisSize,
3974
+ setSize: ctx.setSize,
3975
+ applySize: ctx.applySize,
3976
+ cancelInFlight: ctx.cancelInFlight,
3977
+ newCycle: ctx.newCycle,
3978
+ isAnimating: ctx.isAnimating,
3979
+ resyncAfterCancel: ctx.resyncAfterResize
3980
+ });
3981
+ }
3982
+ };
3983
+ }
3984
+
3985
+ // src/core/default-features.ts
3986
+ function defaultEngineFeatures() {
3987
+ return [
3988
+ contentSwipeFeature(),
3989
+ visualViewportFeature(),
3990
+ persistFeature(),
3991
+ autoCollapseFeature(),
3992
+ routeFeature()
3993
+ ];
3994
+ }
3995
+
3996
+ // src/core/BottomSheetEngine.ts
3997
+ var BottomSheetEngine = class extends BottomSheetCore {
3998
+ constructor(opts) {
3999
+ super({
4000
+ ...opts,
4001
+ features: [...defaultEngineFeatures(), ...opts.features ?? []]
4002
+ });
4003
+ }
4004
+ };
4005
+
3732
4006
  // src/core/features/teleport.ts
3733
4007
  var resolveTeleportTarget = (target) => {
3734
4008
  if (!target) return null;