@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/react.js CHANGED
@@ -356,74 +356,6 @@ var SnapResolver = class {
356
356
  }
357
357
  };
358
358
 
359
- // src/core/features/persist.ts
360
- function readPersistedId(key) {
361
- if (!key) return null;
362
- if (typeof window === "undefined") return null;
363
- if (typeof localStorage === "undefined") return null;
364
- try {
365
- return localStorage.getItem(key);
366
- } catch {
367
- return null;
368
- }
369
- }
370
- function installPersist(engine, key) {
371
- const unsubscribe = engine.on("snap", ({ id }) => {
372
- if (typeof window === "undefined") return;
373
- if (typeof localStorage === "undefined") return;
374
- try {
375
- localStorage.setItem(key, id);
376
- } catch {
377
- }
378
- });
379
- return unsubscribe;
380
- }
381
-
382
- // src/core/features/auto-collapse.ts
383
- function installAutoCollapse(deps) {
384
- const { ms } = deps;
385
- if (ms === void 0 || ms <= 0) {
386
- return () => {
387
- };
388
- }
389
- let timer = null;
390
- const clearTimer = () => {
391
- if (timer !== null) {
392
- clearTimeout(timer);
393
- timer = null;
394
- }
395
- };
396
- const fire = () => {
397
- if (deps.isDestroyed()) return;
398
- if (deps.isDragging()) return;
399
- const active = deps.resolveSnap(deps.getActiveId());
400
- if (!active || active.size === 0) return;
401
- let target = null;
402
- for (const id of deps.getAllowedIds()) {
403
- const snap = deps.resolveSnap(id);
404
- if (!snap || snap.size <= 0) continue;
405
- if (!target || snap.size < target.size) target = snap;
406
- }
407
- if (!target) return;
408
- if (target.id === active.id || target.size >= active.size) return;
409
- deps.snapTo(target.id);
410
- };
411
- const reset = () => {
412
- clearTimer();
413
- if (deps.isDestroyed()) return;
414
- timer = setTimeout(() => {
415
- timer = null;
416
- fire();
417
- }, ms);
418
- };
419
- const unsubscribe = deps.on("snap", () => reset());
420
- if ((deps.resolveSnap(deps.getActiveId())?.size ?? 0) > 0) reset();
421
- return () => {
422
- clearTimer();
423
- unsubscribe();
424
- };
425
- }
426
-
427
359
  // src/core/features/linked-sheets.ts
428
360
  function notifyLinkedSheets(sheets, self) {
429
361
  if (sheets.length === 0) return;
@@ -446,390 +378,65 @@ function notifyLinkedSheets(sheets, self) {
446
378
  }
447
379
  }
448
380
 
449
- // src/core/features/history-coordinator.ts
450
- var entries = [];
451
- var subscribers = /* @__PURE__ */ new Set();
452
- var suppress = 0;
453
- var internalDepth = 0;
454
- var pendingRebrand = null;
455
- var listenerInstalled = false;
456
- var patchInstalled = false;
457
- var origPushState = null;
458
- var origReplaceState = null;
459
- var patchedPushState = null;
460
- var patchedReplaceState = null;
461
- var hasWindow = () => typeof window !== "undefined";
462
- var hasHistory = () => typeof history !== "undefined";
463
- var runInternal = (fn) => {
464
- internalDepth += 1;
465
- try {
466
- fn();
467
- } finally {
468
- internalDepth -= 1;
469
- }
470
- };
471
- var notifyRouteChange = () => {
472
- const snapshot = Array.from(subscribers);
473
- for (const fn of snapshot) fn();
474
- };
475
- var ensureListener = () => {
476
- if (listenerInstalled || !hasWindow()) return;
477
- window.addEventListener("popstate", onPopState);
478
- listenerInstalled = true;
479
- };
480
- var maybeTeardownListener = () => {
481
- if (!listenerInstalled) return;
482
- if (entries.length > 0 || subscribers.size > 0 || suppress > 0) return;
483
- if (hasWindow()) window.removeEventListener("popstate", onPopState);
484
- listenerInstalled = false;
485
- };
486
- var installPatch = () => {
487
- if (patchInstalled || !hasHistory()) return;
488
- origPushState = history.pushState;
489
- origReplaceState = history.replaceState;
490
- patchedPushState = function(...args) {
491
- origPushState.apply(this, args);
492
- if (internalDepth === 0) notifyRouteChange();
493
- };
494
- patchedReplaceState = function(...args) {
495
- origReplaceState.apply(this, args);
496
- if (internalDepth === 0) notifyRouteChange();
497
- };
498
- history.pushState = patchedPushState;
499
- history.replaceState = patchedReplaceState;
500
- patchInstalled = true;
501
- };
502
- var removePatch = () => {
503
- if (!patchInstalled) return;
504
- if (history.pushState === patchedPushState && origPushState) {
505
- history.pushState = origPushState;
506
- }
507
- if (history.replaceState === patchedReplaceState && origReplaceState) {
508
- history.replaceState = origReplaceState;
509
- }
510
- origPushState = null;
511
- origReplaceState = null;
512
- patchedPushState = null;
513
- patchedReplaceState = null;
514
- patchInstalled = false;
515
- };
516
- function onPopState() {
517
- if (suppress > 0) {
518
- suppress -= 1;
519
- const rebrand = pendingRebrand;
520
- pendingRebrand = null;
521
- if (rebrand) rebrand();
522
- maybeTeardownListener();
523
- return;
524
- }
525
- if (entries.length === 0) {
526
- notifyRouteChange();
527
- maybeTeardownListener();
528
- return;
381
+ // src/core/features/resize-observer.ts
382
+ function installResizeObserver(deps) {
383
+ if (typeof window === "undefined") {
384
+ return () => {
385
+ };
529
386
  }
530
- let target = null;
531
- let targetIdx = -1;
532
- for (let i = entries.length - 1; i >= 0; i -= 1) {
533
- const entry = entries[i];
534
- if (!entry.surface.isOpen()) continue;
535
- if (!target) {
536
- target = entry;
537
- targetIdx = i;
538
- }
539
- if (entry.surface.isTop()) {
540
- target = entry;
541
- targetIdx = i;
542
- break;
387
+ const onResize = () => {
388
+ if (deps.isDestroyed()) return;
389
+ deps.recomputeSnaps();
390
+ const mode = deps.getMode();
391
+ const isVerticalAxis = mode === "bottom" || mode === "top";
392
+ const viewportSize2 = isVerticalAxis ? window.innerHeight : window.innerWidth;
393
+ const max = deps.getMaxAxisSize();
394
+ if (viewportSize2 > 0 && viewportSize2 < max) {
395
+ deps.setMaxAxisSize(viewportSize2);
543
396
  }
544
- }
545
- if (!target) {
546
- const removed = entries.pop();
547
- if (removed) removed.live = false;
548
- maybeTeardownListener();
549
- return;
550
- }
551
- target.live = false;
552
- entries.splice(targetIdx, 1);
553
- const settled = target;
554
- void Promise.resolve(settled.surface.close()).then(() => {
555
- if (settled.surface.isOpen()) restore(settled);
556
- });
557
- maybeTeardownListener();
558
- }
559
- function restore(entry) {
560
- if (!hasWindow() || !hasHistory()) return;
561
- entry.live = true;
562
- runInternal(() => {
563
- try {
564
- history.pushState(entry.surface.markerState(), "", entry.surface.url);
565
- } catch {
566
- entry.live = false;
397
+ const wasAnimating = deps.isAnimating?.() ?? false;
398
+ deps.cancelInFlight();
399
+ deps.newCycle();
400
+ const current = deps.resolveActiveSnap();
401
+ const newMax = deps.getMaxAxisSize();
402
+ const targetSize = current ? Math.min(current.size, newMax) : Math.min(deps.getSize(), newMax);
403
+ deps.setSize(targetSize);
404
+ if (current && !deps.isDragging()) {
405
+ deps.applySize(targetSize);
406
+ if (wasAnimating) deps.resyncAfterCancel?.();
567
407
  }
568
- });
569
- if (entry.live) {
570
- entries.push(entry);
571
- ensureListener();
408
+ };
409
+ let resizeObserver = null;
410
+ if (typeof ResizeObserver !== "undefined") {
411
+ resizeObserver = new ResizeObserver(onResize);
412
+ resizeObserver.observe(document.documentElement);
413
+ } else {
414
+ window.addEventListener("resize", onResize);
572
415
  }
416
+ window.addEventListener("orientationchange", onResize);
417
+ const onVisibility = () => {
418
+ if (deps.isDestroyed()) return;
419
+ if (document.hidden) deps.cancelInFlight();
420
+ };
421
+ document.addEventListener("visibilitychange", onVisibility);
422
+ return () => {
423
+ resizeObserver?.disconnect();
424
+ window.removeEventListener("resize", onResize);
425
+ window.removeEventListener("orientationchange", onResize);
426
+ document.removeEventListener("visibilitychange", onVisibility);
427
+ };
573
428
  }
574
- function pushBackMarker(surface) {
575
- const entry = { surface, priorUrl: void 0, live: false };
576
- if (!hasWindow() || !hasHistory()) {
577
- return entry;
578
- }
579
- entry.priorUrl = surface.url !== void 0 ? location.href : void 0;
580
- runInternal(() => {
581
- try {
582
- history.pushState(surface.markerState(), "", surface.url);
583
- entry.live = true;
584
- } catch {
585
- entry.live = false;
586
- }
587
- });
588
- if (entry.live) {
589
- entries.push(entry);
590
- ensureListener();
591
- }
592
- return entry;
593
- }
594
- function popBackMarker(handle) {
595
- if (!hasWindow() || !hasHistory()) return;
596
- const entry = handle;
597
- const idx = entries.indexOf(entry);
598
- if (idx === -1 || !entry.live) return;
599
- const last = entries.length - 1;
600
- entry.live = false;
601
- entries.splice(idx, 1);
602
- if (idx !== last) {
603
- const rebrandSurface = entries[entries.length - 1].surface;
604
- const rebrandUrl = entry.priorUrl;
605
- pendingRebrand = () => {
606
- runInternal(() => {
607
- history.replaceState(
608
- rebrandSurface.markerState(),
609
- "",
610
- rebrandUrl !== void 0 ? rebrandUrl : location.href
611
- );
612
- });
613
- };
614
- }
615
- suppress += 1;
616
- runInternal(() => {
617
- try {
618
- history.back();
619
- } catch {
620
- suppress = Math.max(0, suppress - 1);
621
- pendingRebrand = null;
622
- }
623
- });
624
- maybeTeardownListener();
625
- }
626
- function subscribeRouteChange(fn) {
627
- if (!hasWindow() || !hasHistory()) return () => {
628
- };
629
- subscribers.add(fn);
630
- if (subscribers.size === 1) installPatch();
631
- ensureListener();
632
- return () => {
633
- if (!subscribers.has(fn)) return;
634
- subscribers.delete(fn);
635
- if (subscribers.size === 0) removePatch();
636
- maybeTeardownListener();
637
- };
638
- }
639
-
640
- // src/core/features/route.ts
641
- function installRoute(deps) {
642
- if (typeof window === "undefined") {
643
- return () => {
644
- };
645
- }
646
- if (!deps.routedTo && !deps.closeOnBack) {
647
- return () => {
648
- };
649
- }
650
- let handle = null;
651
- const onOpen = () => {
652
- if (handle) return;
653
- handle = pushBackMarker({
654
- isOpen: () => !deps.isDestroyed() && deps.getSize() > 0,
655
- isTop: deps.isTopSheet,
656
- close: deps.close,
657
- markerState: () => deps.routedTo !== void 0 ? { __bsRouted: deps.sheetId } : { __bsSheet: deps.sheetId },
658
- url: deps.routedTo
659
- });
660
- };
661
- const onClose = () => {
662
- if (!handle) return;
663
- popBackMarker(handle);
664
- handle = null;
665
- };
666
- const unsubscribeOpen = deps.on("open", onOpen);
667
- const unsubscribeClose = deps.on("close", onClose);
668
- if (deps.getSize() > 0) onOpen();
669
- return () => {
670
- unsubscribeOpen();
671
- unsubscribeClose();
672
- if (handle) {
673
- popBackMarker(handle);
674
- handle = null;
675
- }
676
- };
677
- }
678
- function installRouteChange(deps) {
679
- return subscribeRouteChange(() => {
680
- if (deps.isDestroyed() || deps.getSize() <= 0) return;
681
- void deps.close();
682
- });
683
- }
684
-
685
- // src/core/features/content-swipe.ts
686
- var CONTENT_PULL_THRESHOLD = 60;
687
- function installContentSwipe(deps) {
688
- const { container } = deps;
689
- let startY = 0;
690
- let startScrollTop = 0;
691
- let moved = false;
692
- const onTouchStart = (e) => {
693
- const t = e.touches[0];
694
- if (!t) return;
695
- startY = t.clientY;
696
- startScrollTop = container.scrollTop;
697
- moved = false;
698
- };
699
- const onTouchMove = (e) => {
700
- const t = e.touches[0];
701
- if (!t) return;
702
- if (Math.abs(t.clientY - startY) > CONTENT_PULL_THRESHOLD) moved = true;
703
- };
704
- const onTouchEnd = (e) => {
705
- if (!moved) return;
706
- if (deps.isDragging() || deps.isAnimating()) return;
707
- if (container.scrollTop !== startScrollTop) return;
708
- const t = e.changedTouches[0];
709
- if (!t) return;
710
- const deltaY = t.clientY - startY;
711
- const atTop = container.scrollTop <= 0;
712
- const allowed = deps.getAllowedIds();
713
- const idx = allowed.indexOf(deps.getActiveId());
714
- if (idx === -1) return;
715
- if (atTop && deltaY > CONTENT_PULL_THRESHOLD && idx > 0) {
716
- deps.snapTo(allowed[idx - 1]);
717
- } else if (deltaY < -CONTENT_PULL_THRESHOLD && idx < allowed.length - 1) {
718
- deps.snapTo(allowed[idx + 1]);
719
- }
720
- };
721
- container.addEventListener("touchstart", onTouchStart, { passive: true });
722
- container.addEventListener("touchmove", onTouchMove, { passive: true });
723
- container.addEventListener("touchend", onTouchEnd, { passive: true });
724
- return () => {
725
- container.removeEventListener("touchstart", onTouchStart);
726
- container.removeEventListener("touchmove", onTouchMove);
727
- container.removeEventListener("touchend", onTouchEnd);
728
- };
729
- }
730
-
731
- // src/core/features/visual-viewport.ts
732
- var SAFETY_PAD = 8;
733
- function installVisualViewport(deps) {
734
- if (typeof window === "undefined" || !window.visualViewport) {
735
- return () => {
736
- };
737
- }
738
- const vv = window.visualViewport;
739
- let rafId = null;
740
- const onResize = () => {
741
- if (rafId !== null) return;
742
- rafId = requestAnimationFrame(() => {
743
- rafId = null;
744
- if (deps.isDestroyed()) return;
745
- deps.recomputeSnaps();
746
- const isVertical = deps.isVerticalAxis();
747
- const visible = isVertical ? vv.height : vv.width;
748
- const windowSize = isVertical ? window.innerHeight : window.innerWidth;
749
- const clamp = Math.max(0, Math.min(windowSize, visible) - SAFETY_PAD);
750
- if (clamp < deps.getMaxAxisSize()) {
751
- deps.setMaxAxisSize(clamp);
752
- }
753
- const wasAnimating = deps.isAnimating?.() ?? false;
754
- deps.cancelInFlight();
755
- deps.newCycle();
756
- const max = deps.getMaxAxisSize();
757
- deps.setSize(Math.min(deps.getSize(), max));
758
- const current = deps.resolveActiveSnap();
759
- if (current && !deps.isDragging()) {
760
- deps.applySize(Math.min(current.size, max));
761
- if (wasAnimating) deps.resyncAfterCancel?.();
762
- } else {
763
- deps.applySize(deps.getSize());
764
- }
765
- });
766
- };
767
- vv.addEventListener("resize", onResize);
768
- return () => {
769
- vv.removeEventListener("resize", onResize);
770
- if (rafId !== null) cancelAnimationFrame(rafId);
771
- };
772
- }
773
-
774
- // src/core/features/resize-observer.ts
775
- function installResizeObserver(deps) {
776
- if (typeof window === "undefined") {
777
- return () => {
778
- };
779
- }
780
- const onResize = () => {
781
- if (deps.isDestroyed()) return;
782
- deps.recomputeSnaps();
783
- const mode = deps.getMode();
784
- const isVerticalAxis = mode === "bottom" || mode === "top";
785
- const viewportSize2 = isVerticalAxis ? window.innerHeight : window.innerWidth;
786
- const max = deps.getMaxAxisSize();
787
- if (viewportSize2 > 0 && viewportSize2 < max) {
788
- deps.setMaxAxisSize(viewportSize2);
789
- }
790
- const wasAnimating = deps.isAnimating?.() ?? false;
791
- deps.cancelInFlight();
792
- deps.newCycle();
793
- const current = deps.resolveActiveSnap();
794
- const newMax = deps.getMaxAxisSize();
795
- const targetSize = current ? Math.min(current.size, newMax) : Math.min(deps.getSize(), newMax);
796
- deps.setSize(targetSize);
797
- if (current && !deps.isDragging()) {
798
- deps.applySize(targetSize);
799
- if (wasAnimating) deps.resyncAfterCancel?.();
800
- }
801
- };
802
- let resizeObserver = null;
803
- if (typeof ResizeObserver !== "undefined") {
804
- resizeObserver = new ResizeObserver(onResize);
805
- resizeObserver.observe(document.documentElement);
806
- } else {
807
- window.addEventListener("resize", onResize);
808
- }
809
- window.addEventListener("orientationchange", onResize);
810
- const onVisibility = () => {
811
- if (deps.isDestroyed()) return;
812
- if (document.hidden) deps.cancelInFlight();
813
- };
814
- document.addEventListener("visibilitychange", onVisibility);
815
- return () => {
816
- resizeObserver?.disconnect();
817
- window.removeEventListener("resize", onResize);
818
- window.removeEventListener("orientationchange", onResize);
819
- document.removeEventListener("visibilitychange", onVisibility);
820
- };
821
- }
822
-
823
- // src/core/features/fit-observer.ts
824
- var contentTargets = (scroller) => {
825
- if (!scroller) return [];
826
- const hasSlot = typeof HTMLSlotElement !== "undefined";
827
- const out = [];
828
- for (const child of Array.from(scroller.children)) {
829
- if (hasSlot && child instanceof HTMLSlotElement) {
830
- for (const el of child.assignedElements()) out.push(el);
831
- } else {
832
- out.push(child);
429
+
430
+ // src/core/features/fit-observer.ts
431
+ var contentTargets = (scroller) => {
432
+ if (!scroller) return [];
433
+ const hasSlot = typeof HTMLSlotElement !== "undefined";
434
+ const out = [];
435
+ for (const child of Array.from(scroller.children)) {
436
+ if (hasSlot && child instanceof HTMLSlotElement) {
437
+ for (const el of child.assignedElements()) out.push(el);
438
+ } else {
439
+ out.push(child);
833
440
  }
834
441
  }
835
442
  return out;
@@ -949,11 +556,15 @@ function createMaxHeightController(deps) {
949
556
 
950
557
  // src/core/features/fit-measurement.ts
951
558
  var measureSheetNatural = (element, content, vertical) => {
952
- if (!content) {
953
- return vertical ? element.offsetHeight : element.offsetWidth;
954
- }
955
559
  const axis = vertical ? "height" : "width";
956
560
  const sheetStyle = element.style;
561
+ if (!content) {
562
+ const prev = sheetStyle[axis];
563
+ sheetStyle[axis] = "auto";
564
+ const poked2 = vertical ? element.offsetHeight : element.offsetWidth;
565
+ sheetStyle[axis] = prev;
566
+ return poked2;
567
+ }
957
568
  const cs = content.style;
958
569
  const prevSheet = sheetStyle[axis];
959
570
  const prevFlex = cs.flex;
@@ -1667,6 +1278,52 @@ function resolveAnimationPreset(animation) {
1667
1278
  }
1668
1279
  }
1669
1280
 
1281
+ // src/core/animation/waapi-settle.ts
1282
+ var SIM_HZ = 240;
1283
+ var SAMPLE_STRIDE = 4;
1284
+ var MAX_SIM_MS = 3e3;
1285
+ function sampleSpringSettle(from, to, velocityPxPerS, config) {
1286
+ const cfg = { ...DEFAULT_SPRING, ...config };
1287
+ const subDt = 1 / SIM_HZ;
1288
+ const stepMs = SAMPLE_STRIDE / SIM_HZ * 1e3;
1289
+ const maxSteps = Math.ceil(MAX_SIM_MS / 1e3 * SIM_HZ);
1290
+ const values = [from];
1291
+ let x = from;
1292
+ let v = velocityPxPerS;
1293
+ for (let i = 1; i <= maxSteps; i += 1) {
1294
+ const force = -cfg.stiffness * (x - to) - cfg.damping * v;
1295
+ v += force / cfg.mass * subDt;
1296
+ x += v * subDt;
1297
+ if (!Number.isFinite(x) || !Number.isFinite(v)) {
1298
+ values.push(to);
1299
+ break;
1300
+ }
1301
+ if (i % SAMPLE_STRIDE === 0) values.push(x);
1302
+ const atRest = Math.abs(to - x) < (cfg.restDelta ?? 0.5) && Math.abs(v) < (cfg.restSpeed ?? 0.5);
1303
+ if (atRest) break;
1304
+ }
1305
+ if (values[values.length - 1] !== to) values.push(to);
1306
+ return {
1307
+ values,
1308
+ stepMs,
1309
+ durationMs: (values.length - 1) * stepMs
1310
+ };
1311
+ }
1312
+ function sampleTweenSettle(from, to, durationMs, easing) {
1313
+ const count = Math.max(2, Math.min(60, Math.round(durationMs / 16)));
1314
+ const values = [];
1315
+ for (let i = 0; i <= count; i += 1) {
1316
+ const t = i / count;
1317
+ values.push(from + (to - from) * easing(t));
1318
+ }
1319
+ values[values.length - 1] = to;
1320
+ return {
1321
+ values,
1322
+ stepMs: durationMs / count,
1323
+ durationMs
1324
+ };
1325
+ }
1326
+
1670
1327
  // src/core/controllers/animation-runner.ts
1671
1328
  var DEFAULT_DURATION2 = 220;
1672
1329
  var VELOCITY_PX_PER_S = 1e3;
@@ -1677,6 +1334,10 @@ var AnimationRunner = class {
1677
1334
  __publicField(this, "applySizeFn");
1678
1335
  __publicField(this, "getSize");
1679
1336
  __publicField(this, "isDragging");
1337
+ __publicField(this, "applyAux");
1338
+ __publicField(this, "getTransformFor");
1339
+ __publicField(this, "settleWaapi");
1340
+ __publicField(this, "currentWaapi", null);
1680
1341
  __publicField(this, "animationKind");
1681
1342
  __publicField(this, "springConfig");
1682
1343
  __publicField(this, "duration");
@@ -1692,6 +1353,9 @@ var AnimationRunner = class {
1692
1353
  this.applySizeFn = deps.applySize;
1693
1354
  this.getSize = deps.getSize;
1694
1355
  this.isDragging = deps.isDragging;
1356
+ this.applyAux = deps.applyAux;
1357
+ this.getTransformFor = deps.getTransformFor;
1358
+ this.settleWaapi = opts.settleAnimation === "waapi";
1695
1359
  const preset = resolveAnimationPreset(opts.animation);
1696
1360
  this.animationKind = preset.kind;
1697
1361
  this.duration = opts.duration ?? preset.duration ?? DEFAULT_DURATION2;
@@ -1716,11 +1380,20 @@ var AnimationRunner = class {
1716
1380
  }
1717
1381
  }
1718
1382
  get isAnimating() {
1719
- return this.currentTween !== null || this.currentSpring !== null;
1383
+ return this.currentTween !== null || this.currentSpring !== null || this.currentWaapi !== null;
1720
1384
  }
1721
1385
  cancel() {
1722
1386
  this.currentTween?.cancel();
1723
1387
  this.currentSpring?.cancel();
1388
+ if (this.currentWaapi) {
1389
+ const entry = this.currentWaapi;
1390
+ this.currentWaapi = null;
1391
+ entry.stop();
1392
+ try {
1393
+ entry.anim.cancel();
1394
+ } catch {
1395
+ }
1396
+ }
1724
1397
  }
1725
1398
  async animateTo(target, velocityPxPerMs) {
1726
1399
  if (target === this.getSize()) {
@@ -1739,7 +1412,28 @@ var AnimationRunner = class {
1739
1412
  if (!this.isDragging()) this.element.style.willChange = "";
1740
1413
  return;
1741
1414
  }
1742
- if (this.animationKind === "spring") {
1415
+ if (this.settleWaapi && this.applyAux && this.getTransformFor && typeof this.element.animate === "function") {
1416
+ const samples = this.animationKind === "spring" ? sampleSpringSettle(
1417
+ this.getSize(),
1418
+ target,
1419
+ velocityPxPerMs * VELOCITY_PX_PER_S,
1420
+ this.springConfig
1421
+ ) : sampleTweenSettle(
1422
+ this.getSize(),
1423
+ target,
1424
+ this.duration,
1425
+ this.easing
1426
+ );
1427
+ if (samples.values.length >= 2 && samples.durationMs > 0) {
1428
+ await this.runWaapiSettle(samples, target);
1429
+ if (!this.isAnimating) {
1430
+ this.getRootEl()?.removeAttribute("data-animating");
1431
+ if (!this.isDragging()) this.element.style.willChange = "";
1432
+ }
1433
+ return;
1434
+ }
1435
+ }
1436
+ if (this.animationKind === "spring") {
1743
1437
  const spring = runSpring({
1744
1438
  from: this.getSize(),
1745
1439
  to: target,
@@ -1767,11 +1461,66 @@ var AnimationRunner = class {
1767
1461
  if (!this.isDragging()) this.element.style.willChange = "";
1768
1462
  }
1769
1463
  }
1464
+ async runWaapiSettle(samples, target) {
1465
+ const applyAux = this.applyAux;
1466
+ const getTransformFor = this.getTransformFor;
1467
+ const frames = samples.values.map((v) => ({
1468
+ transform: getTransformFor(v)
1469
+ }));
1470
+ const anim = this.element.animate(frames, {
1471
+ duration: samples.durationMs,
1472
+ easing: "linear",
1473
+ fill: "forwards"
1474
+ });
1475
+ let stopped = false;
1476
+ let auxRaf = 0;
1477
+ const start = performance.now();
1478
+ const lastIdx = samples.values.length - 1;
1479
+ const auxTick = () => {
1480
+ if (stopped) return;
1481
+ const elapsed = performance.now() - start;
1482
+ const idx = Math.min(Math.floor(elapsed / samples.stepMs), lastIdx);
1483
+ applyAux(samples.values[idx]);
1484
+ auxRaf = requestAnimationFrame(auxTick);
1485
+ };
1486
+ auxRaf = requestAnimationFrame(auxTick);
1487
+ const entry = {
1488
+ anim,
1489
+ stop: () => {
1490
+ stopped = true;
1491
+ cancelAnimationFrame(auxRaf);
1492
+ }
1493
+ };
1494
+ this.currentWaapi = entry;
1495
+ try {
1496
+ await anim.finished;
1497
+ } catch {
1498
+ }
1499
+ entry.stop();
1500
+ const wasCurrent = this.currentWaapi === entry;
1501
+ if (wasCurrent) this.currentWaapi = null;
1502
+ if (wasCurrent && anim.playState === "finished") {
1503
+ this.applySizeFn(target);
1504
+ try {
1505
+ anim.cancel();
1506
+ } catch {
1507
+ }
1508
+ }
1509
+ }
1770
1510
  destroy() {
1771
1511
  this.detachReducedMotion?.();
1772
1512
  this.detachReducedMotion = null;
1773
1513
  this.currentTween = null;
1774
1514
  this.currentSpring = null;
1515
+ if (this.currentWaapi) {
1516
+ const entry = this.currentWaapi;
1517
+ this.currentWaapi = null;
1518
+ entry.stop();
1519
+ try {
1520
+ entry.anim.cancel();
1521
+ } catch {
1522
+ }
1523
+ }
1775
1524
  }
1776
1525
  };
1777
1526
 
@@ -2628,7 +2377,7 @@ function resolveEngineOptions(opts, extras) {
2628
2377
  const initialAllowed = opts.allowed ?? opts.snapPoints.map((p) => p.id);
2629
2378
  let initialId = opts.initial ?? initialAllowed[0] ?? opts.snapPoints[0]?.id ?? "default";
2630
2379
  if (opts.persistKey) {
2631
- const restored = readPersistedId(opts.persistKey);
2380
+ const restored = readPersistedIdSafe(opts.persistKey);
2632
2381
  if (restored && initialAllowed.includes(restored)) {
2633
2382
  initialId = restored;
2634
2383
  }
@@ -2659,6 +2408,7 @@ function resolveEngineOptions(opts, extras) {
2659
2408
  },
2660
2409
  animation: {
2661
2410
  animation: opts.animation,
2411
+ settleAnimation: opts.settleAnimation,
2662
2412
  duration: opts.duration,
2663
2413
  easing: opts.easing,
2664
2414
  spring: opts.spring,
@@ -2676,6 +2426,15 @@ function resolveEngineOptions(opts, extras) {
2676
2426
  }
2677
2427
  };
2678
2428
  }
2429
+ var readPersistedIdSafe = (key) => {
2430
+ if (typeof window === "undefined") return null;
2431
+ if (typeof localStorage === "undefined") return null;
2432
+ try {
2433
+ return localStorage.getItem(key);
2434
+ } catch {
2435
+ return null;
2436
+ }
2437
+ };
2679
2438
 
2680
2439
  // src/core/primitives/teardown-stack.ts
2681
2440
  var TeardownStack = class {
@@ -2698,9 +2457,9 @@ var TeardownStack = class {
2698
2457
  }
2699
2458
  };
2700
2459
 
2701
- // src/core/BottomSheetEngine.ts
2460
+ // src/core/BottomSheetCore.ts
2702
2461
  var HAPTIC_DURATION_MS = 8;
2703
- var BottomSheetEngine = class {
2462
+ var BottomSheetCore = class {
2704
2463
  constructor(opts) {
2705
2464
  __publicField(this, "id", nextInstanceId("bs"));
2706
2465
  __publicField(this, "element");
@@ -2761,6 +2520,8 @@ var BottomSheetEngine = class {
2761
2520
  __publicField(this, "stackEffectPrimed", false);
2762
2521
  __publicField(this, "fitDeps");
2763
2522
  __publicField(this, "transformTemplate");
2523
+ __publicField(this, "featureList", []);
2524
+ __publicField(this, "featureCtx");
2764
2525
  const resolved = resolveEngineOptions(opts, {
2765
2526
  shouldApplyInertSiblings: () => {
2766
2527
  if (typeof document === "undefined") return false;
@@ -2807,7 +2568,9 @@ var BottomSheetEngine = class {
2807
2568
  getRootEl: () => this.rootEl,
2808
2569
  applySize: (size) => this.applySize(size),
2809
2570
  getSize: () => this.size,
2810
- isDragging: () => this.gesture?.isDragging ?? false
2571
+ isDragging: () => this.gesture?.isDragging ?? false,
2572
+ applyAux: (size) => this.applySize(size, true),
2573
+ getTransformFor: (size) => this.transformTemplate(this.snaps.getMaxAxisSize() - size)
2811
2574
  },
2812
2575
  resolved.animation
2813
2576
  );
@@ -2830,6 +2593,11 @@ var BottomSheetEngine = class {
2830
2593
  if (opts.linkedSheets) {
2831
2594
  this.linkedSheets = opts.linkedSheets;
2832
2595
  }
2596
+ const byName = /* @__PURE__ */ new Map();
2597
+ for (const feature of opts.features ?? []) {
2598
+ byName.set(feature.name, feature);
2599
+ }
2600
+ this.featureList = Array.from(byName.values());
2833
2601
  this.activeId = resolved.initialId;
2834
2602
  auditVhUsage(this.snapPointsRaw);
2835
2603
  this.auditDuplicateSnapIds(this.snapPointsRaw);
@@ -2866,6 +2634,7 @@ var BottomSheetEngine = class {
2866
2634
  scrollContainer: this.scrollContainer,
2867
2635
  getMaxAxisSize: () => this.snaps.getMaxAxisSize()
2868
2636
  });
2637
+ this.featureCtx = this.buildFeatureContext(opts.autoCollapseAfter);
2869
2638
  this.attach();
2870
2639
  this.registerInStack();
2871
2640
  if (this.snaps.getMaxAxisSize() === 0 && typeof IntersectionObserver !== "undefined" && typeof document !== "undefined") {
@@ -2874,12 +2643,7 @@ var BottomSheetEngine = class {
2874
2643
  if (!entry.isIntersecting) continue;
2875
2644
  if (this.destroyed) return;
2876
2645
  this.snaps.recompute();
2877
- const current = this.snaps.findById(this.activeId);
2878
- if (current) {
2879
- this.size = current.size;
2880
- this.applySize(this.size);
2881
- this.updateAriaSlider();
2882
- }
2646
+ this.healActiveSnap();
2883
2647
  io.disconnect();
2884
2648
  return;
2885
2649
  }
@@ -2902,46 +2666,77 @@ var BottomSheetEngine = class {
2902
2666
  if (resolved.radius !== void 0) this.setRadius(resolved.radius);
2903
2667
  if (resolved.maxHeight !== void 0) this.setMaxHeight(resolved.maxHeight);
2904
2668
  this.applyAutoAriaLabelledBy();
2905
- if (this.closeOnRouteChange) {
2906
- this.teardowns.add(
2907
- installRouteChange({
2908
- isDestroyed: () => this.destroyed,
2909
- getSize: () => this.size,
2910
- close: () => this.close()
2911
- })
2912
- );
2913
- }
2914
- if (this.persistKey) {
2915
- this.teardowns.add(installPersist(this, this.persistKey));
2669
+ this.runFeatureStage("post");
2670
+ this.warnOrphanFeatureOptions();
2671
+ if (this.size > 0) {
2672
+ this.handleOpen();
2916
2673
  }
2917
- this.teardowns.add(
2918
- installAutoCollapse({
2919
- ms: opts.autoCollapseAfter,
2920
- isDestroyed: () => this.destroyed,
2921
- isDragging: () => this.gesture?.isDragging ?? false,
2922
- getAllowedIds: () => this.snaps.getAllowedIds().slice(),
2923
- getActiveId: () => this.activeId,
2924
- resolveSnap: (id) => this.snaps.findById(id),
2925
- snapTo: (id) => {
2926
- void this.snapTo(id);
2927
- },
2928
- on: (event, fn) => this.on(event, fn)
2929
- })
2930
- );
2931
- this.teardowns.add(
2932
- installRoute({
2674
+ }
2675
+ buildFeatureContext(autoCollapseAfter) {
2676
+ return {
2677
+ element: this.element,
2678
+ scrollContainer: this.scrollContainer,
2679
+ sheetId: this.id,
2680
+ options: {
2933
2681
  routedTo: this.routedTo,
2934
2682
  closeOnBack: this.closeOnBack,
2935
- isTopSheet: () => this.isTopSheet,
2936
- getSize: () => this.size,
2937
- isDestroyed: () => this.destroyed,
2938
- close: () => this.close("back"),
2939
- on: (event, fn) => this.on(event, fn),
2940
- sheetId: this.id
2941
- })
2942
- );
2943
- if (this.size > 0) {
2944
- this.handleOpen();
2683
+ closeOnRouteChange: this.closeOnRouteChange,
2684
+ persistKey: this.persistKey,
2685
+ autoCollapseAfter
2686
+ },
2687
+ isDestroyed: () => this.destroyed,
2688
+ isDragging: () => this.gesture?.isDragging ?? false,
2689
+ isAnimating: () => this.animation.isAnimating,
2690
+ isTopSheet: () => this.isTopSheet,
2691
+ isVerticalAxis: () => this.mode === "bottom" || this.mode === "top",
2692
+ getSize: () => this.size,
2693
+ getActiveId: () => this.activeId,
2694
+ getAllowedIds: () => this.snaps.getAllowedIds().slice(),
2695
+ allowedIdsBySize: () => this.allowedIdsBySize(),
2696
+ getMaxAxisSize: () => this.snaps.getMaxAxisSize(),
2697
+ resolveSnap: (id) => this.snaps.findById(id),
2698
+ resolveActiveSnap: () => this.snaps.findById(this.activeId),
2699
+ setSize: (size) => {
2700
+ this.size = size;
2701
+ },
2702
+ setMaxAxisSize: (size) => {
2703
+ this.snaps.setMaxAxisSize(size);
2704
+ },
2705
+ applySize: (size) => this.applySize(size),
2706
+ recomputeSnaps: () => this.recomputeSnaps(),
2707
+ newCycle: () => {
2708
+ this.newCycle();
2709
+ },
2710
+ cancelInFlight: () => {
2711
+ this.animation.cancel();
2712
+ },
2713
+ resyncAfterResize: () => this.resyncAfterResize(),
2714
+ snapTo: (id) => {
2715
+ void this.snapTo(id);
2716
+ },
2717
+ close: (reason) => this.close(reason),
2718
+ on: (event, fn) => this.on(event, fn),
2719
+ addTeardown: (fn) => this.teardowns.add(fn)
2720
+ };
2721
+ }
2722
+ runFeatureStage(stage) {
2723
+ for (const feature of this.featureList) {
2724
+ if ((feature.stage ?? "post") !== stage) continue;
2725
+ const teardown = feature.install(this.featureCtx);
2726
+ if (teardown) this.teardowns.add(teardown);
2727
+ }
2728
+ }
2729
+ warnOrphanFeatureOptions() {
2730
+ const names = new Set(this.featureList.map((f) => f.name));
2731
+ if ((this.closeOnBack || this.routedTo !== void 0 || this.closeOnRouteChange) && !names.has("route")) {
2732
+ devWarn(
2733
+ "[BottomSheet] closeOnBack/routedTo/closeOnRouteChange need routeFeature() \u2014 option ignored"
2734
+ );
2735
+ }
2736
+ if (this.persistKey && !names.has("persist")) {
2737
+ devWarn(
2738
+ "[BottomSheet] persistKey needs persistFeature() \u2014 snaps will not be saved"
2739
+ );
2945
2740
  }
2946
2741
  }
2947
2742
  get state() {
@@ -3043,6 +2838,8 @@ var BottomSheetEngine = class {
3043
2838
  const vt = document.startViewTransition(() => {
3044
2839
  this.applySize(target.size);
3045
2840
  });
2841
+ vt.ready?.catch(() => {
2842
+ });
3046
2843
  this.currentViewTransition = vt;
3047
2844
  try {
3048
2845
  await vt.finished;
@@ -3294,11 +3091,16 @@ var BottomSheetEngine = class {
3294
3091
  this.maxHeight.clampTo();
3295
3092
  this.scrim.invalidateOpacityCache();
3296
3093
  if (this.gesture?.isDragging) return;
3094
+ this.healActiveSnap();
3095
+ }
3096
+ healActiveSnap() {
3297
3097
  const current = this.snaps.findById(this.activeId);
3298
- if (current) {
3299
- this.size = current.size;
3300
- this.applySize(this.size);
3301
- this.updateAriaSlider();
3098
+ if (!current) return;
3099
+ this.size = current.size;
3100
+ this.applySize(this.size);
3101
+ this.updateAriaSlider();
3102
+ if (this.size > 0 && !this.lifecycle.isInstalled && !this.opening && !this.animation.isAnimating) {
3103
+ this.emitOpenSequence(this.activeId);
3302
3104
  }
3303
3105
  }
3304
3106
  destroy() {
@@ -3477,18 +3279,7 @@ var BottomSheetEngine = class {
3477
3279
  }
3478
3280
  }
3479
3281
  attachFeatures() {
3480
- if (this.scrollContainer) {
3481
- this.teardowns.add(installContentSwipe({
3482
- container: this.scrollContainer,
3483
- isDragging: () => this.gesture?.isDragging ?? false,
3484
- isAnimating: () => this.animation.isAnimating,
3485
- getAllowedIds: () => this.allowedIdsBySize(),
3486
- getActiveId: () => this.activeId,
3487
- snapTo: (id) => {
3488
- void this.snapTo(id);
3489
- }
3490
- }));
3491
- }
3282
+ this.runFeatureStage("attach");
3492
3283
  if (typeof window !== "undefined") {
3493
3284
  this.teardowns.add(installResizeObserver({
3494
3285
  element: this.element,
@@ -3528,31 +3319,6 @@ var BottomSheetEngine = class {
3528
3319
  () => document.removeEventListener("keydown", onKey)
3529
3320
  );
3530
3321
  }
3531
- this.teardowns.add(installVisualViewport({
3532
- element: this.element,
3533
- isVerticalAxis: () => this.mode === "bottom" || this.mode === "top",
3534
- isDestroyed: () => this.destroyed,
3535
- isDragging: () => this.gesture?.isDragging ?? false,
3536
- recomputeSnaps: () => this.recomputeSnaps(),
3537
- resolveActiveSnap: () => this.snaps.findById(this.activeId),
3538
- getMaxAxisSize: () => this.snaps.getMaxAxisSize(),
3539
- getSize: () => this.size,
3540
- setMaxAxisSize: (size) => {
3541
- this.snaps.setMaxAxisSize(size);
3542
- },
3543
- setSize: (size) => {
3544
- this.size = size;
3545
- },
3546
- applySize: (size) => this.applySize(size),
3547
- cancelInFlight: () => {
3548
- this.animation.cancel();
3549
- },
3550
- newCycle: () => {
3551
- this.newCycle();
3552
- },
3553
- isAnimating: () => this.animation.isAnimating,
3554
- resyncAfterCancel: () => this.resyncAfterResize()
3555
- }));
3556
3322
  }
3557
3323
  this.teardowns.add(installSliderKeyboard({
3558
3324
  handle: this.handle,
@@ -3629,13 +3395,13 @@ var BottomSheetEngine = class {
3629
3395
  }
3630
3396
  }
3631
3397
  }
3632
- applySize(size) {
3398
+ applySize(size, skipTransform = false) {
3633
3399
  const cap = this.snaps.getMaxAxisSize();
3634
3400
  const clamped = cap > 0 && size > cap && !this.gesture?.isDragging && !this.allowOvershoot ? cap : size;
3635
3401
  this.size = clamped;
3636
3402
  const offset = cap - clamped;
3637
3403
  const style = this.element.style;
3638
- style.transform = this.transformTemplate(offset);
3404
+ if (!skipTransform) style.transform = this.transformTemplate(offset);
3639
3405
  if (this.sizeWriteSentinel.shouldWrite(clamped, SIZE_WRITE_EPSILON)) {
3640
3406
  style.setProperty("--bs-size", `${clamped}px`);
3641
3407
  if (this.scrimParent) {
@@ -3712,6 +3478,514 @@ var BottomSheetEngine = class {
3712
3478
  sheetStack.update();
3713
3479
  }
3714
3480
  };
3481
+
3482
+ // src/core/features/history-coordinator.ts
3483
+ var entries = [];
3484
+ var subscribers = /* @__PURE__ */ new Set();
3485
+ var suppress = 0;
3486
+ var internalDepth = 0;
3487
+ var pendingRebrands = [];
3488
+ var listenerInstalled = false;
3489
+ var patchInstalled = false;
3490
+ var origPushState = null;
3491
+ var origReplaceState = null;
3492
+ var patchedPushState = null;
3493
+ var patchedReplaceState = null;
3494
+ var hasWindow = () => typeof window !== "undefined";
3495
+ var hasHistory = () => typeof history !== "undefined";
3496
+ var brandMarker = (surface) => ({
3497
+ ...surface.markerState(),
3498
+ __bs: true
3499
+ });
3500
+ var runInternal = (fn) => {
3501
+ internalDepth += 1;
3502
+ try {
3503
+ fn();
3504
+ } finally {
3505
+ internalDepth -= 1;
3506
+ }
3507
+ };
3508
+ var notifyRouteChange = () => {
3509
+ const snapshot = Array.from(subscribers);
3510
+ for (const fn of snapshot) fn();
3511
+ };
3512
+ var ensureListener = () => {
3513
+ if (listenerInstalled || !hasWindow()) return;
3514
+ window.addEventListener("popstate", onPopState);
3515
+ listenerInstalled = true;
3516
+ };
3517
+ var maybeTeardownListener = () => {
3518
+ if (!listenerInstalled) return;
3519
+ if (entries.length > 0 || subscribers.size > 0 || suppress > 0) return;
3520
+ if (hasWindow()) window.removeEventListener("popstate", onPopState);
3521
+ listenerInstalled = false;
3522
+ };
3523
+ var installPatch = () => {
3524
+ if (patchInstalled || !hasHistory()) return;
3525
+ origPushState = history.pushState;
3526
+ origReplaceState = history.replaceState;
3527
+ patchedPushState = function(...args) {
3528
+ origPushState.apply(this, args);
3529
+ if (internalDepth === 0) notifyRouteChange();
3530
+ };
3531
+ patchedReplaceState = function(...args) {
3532
+ origReplaceState.apply(this, args);
3533
+ if (internalDepth === 0) notifyRouteChange();
3534
+ };
3535
+ history.pushState = patchedPushState;
3536
+ history.replaceState = patchedReplaceState;
3537
+ patchInstalled = true;
3538
+ };
3539
+ var removePatch = () => {
3540
+ if (!patchInstalled) return;
3541
+ if (history.pushState === patchedPushState && origPushState) {
3542
+ history.pushState = origPushState;
3543
+ }
3544
+ if (history.replaceState === patchedReplaceState && origReplaceState) {
3545
+ history.replaceState = origReplaceState;
3546
+ }
3547
+ origPushState = null;
3548
+ origReplaceState = null;
3549
+ patchedPushState = null;
3550
+ patchedReplaceState = null;
3551
+ patchInstalled = false;
3552
+ };
3553
+ function onPopState() {
3554
+ if (suppress > 0) {
3555
+ suppress -= 1;
3556
+ const rebrand = pendingRebrands.shift();
3557
+ if (rebrand) rebrand();
3558
+ maybeTeardownListener();
3559
+ return;
3560
+ }
3561
+ if (entries.length === 0) {
3562
+ notifyRouteChange();
3563
+ maybeTeardownListener();
3564
+ return;
3565
+ }
3566
+ let target = null;
3567
+ let targetIdx = -1;
3568
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
3569
+ const entry = entries[i];
3570
+ if (!entry.surface.isOpen()) continue;
3571
+ if (!target) {
3572
+ target = entry;
3573
+ targetIdx = i;
3574
+ }
3575
+ if (entry.surface.isTop()) {
3576
+ target = entry;
3577
+ targetIdx = i;
3578
+ break;
3579
+ }
3580
+ }
3581
+ if (!target) {
3582
+ const removed = entries.pop();
3583
+ if (removed) removed.live = false;
3584
+ maybeTeardownListener();
3585
+ return;
3586
+ }
3587
+ target.live = false;
3588
+ entries.splice(targetIdx, 1);
3589
+ const settled = target;
3590
+ void Promise.resolve(settled.surface.close()).then(() => {
3591
+ if (settled.surface.isOpen()) restore(settled);
3592
+ });
3593
+ maybeTeardownListener();
3594
+ }
3595
+ function restore(entry) {
3596
+ if (!hasWindow() || !hasHistory()) return;
3597
+ entry.live = true;
3598
+ runInternal(() => {
3599
+ try {
3600
+ history.pushState(brandMarker(entry.surface), "", entry.surface.url);
3601
+ } catch {
3602
+ entry.live = false;
3603
+ }
3604
+ });
3605
+ if (entry.live) {
3606
+ entries.push(entry);
3607
+ ensureListener();
3608
+ }
3609
+ }
3610
+ function pushBackMarker(surface) {
3611
+ const entry = { surface, priorUrl: void 0, live: false };
3612
+ if (!hasWindow() || !hasHistory()) {
3613
+ return entry;
3614
+ }
3615
+ entry.priorUrl = surface.url !== void 0 ? location.href : void 0;
3616
+ runInternal(() => {
3617
+ try {
3618
+ history.pushState(brandMarker(surface), "", surface.url);
3619
+ entry.live = true;
3620
+ } catch {
3621
+ entry.live = false;
3622
+ }
3623
+ });
3624
+ if (entry.live) {
3625
+ entries.push(entry);
3626
+ ensureListener();
3627
+ }
3628
+ return entry;
3629
+ }
3630
+ function popBackMarker(handle) {
3631
+ if (!hasWindow() || !hasHistory()) return;
3632
+ const entry = handle;
3633
+ const idx = entries.indexOf(entry);
3634
+ if (idx === -1 || !entry.live) return;
3635
+ const last = entries.length - 1;
3636
+ entry.live = false;
3637
+ entries.splice(idx, 1);
3638
+ let rebrand = null;
3639
+ if (idx !== last) {
3640
+ const rebrandSurface = entries[entries.length - 1].surface;
3641
+ const rebrandUrl = entry.priorUrl;
3642
+ rebrand = () => {
3643
+ runInternal(() => {
3644
+ history.replaceState(
3645
+ brandMarker(rebrandSurface),
3646
+ "",
3647
+ rebrandUrl !== void 0 ? rebrandUrl : location.href
3648
+ );
3649
+ });
3650
+ };
3651
+ }
3652
+ pendingRebrands.push(rebrand);
3653
+ suppress += 1;
3654
+ runInternal(() => {
3655
+ try {
3656
+ history.back();
3657
+ } catch {
3658
+ suppress = Math.max(0, suppress - 1);
3659
+ pendingRebrands.pop();
3660
+ }
3661
+ });
3662
+ maybeTeardownListener();
3663
+ }
3664
+ function subscribeRouteChange(fn) {
3665
+ if (!hasWindow() || !hasHistory()) return () => {
3666
+ };
3667
+ subscribers.add(fn);
3668
+ if (subscribers.size === 1) installPatch();
3669
+ ensureListener();
3670
+ return () => {
3671
+ if (!subscribers.has(fn)) return;
3672
+ subscribers.delete(fn);
3673
+ if (subscribers.size === 0) removePatch();
3674
+ maybeTeardownListener();
3675
+ };
3676
+ }
3677
+
3678
+ // src/core/features/route.ts
3679
+ function installRoute(deps) {
3680
+ if (typeof window === "undefined") {
3681
+ return () => {
3682
+ };
3683
+ }
3684
+ if (!deps.routedTo && !deps.closeOnBack) {
3685
+ return () => {
3686
+ };
3687
+ }
3688
+ let handle = null;
3689
+ const onOpen = () => {
3690
+ if (handle) return;
3691
+ handle = pushBackMarker({
3692
+ isOpen: () => !deps.isDestroyed() && deps.getSize() > 0,
3693
+ isTop: deps.isTopSheet,
3694
+ close: deps.close,
3695
+ markerState: () => deps.routedTo !== void 0 ? { __bsRouted: deps.sheetId } : { __bsSheet: deps.sheetId },
3696
+ url: deps.routedTo
3697
+ });
3698
+ };
3699
+ const onClose = () => {
3700
+ if (!handle) return;
3701
+ popBackMarker(handle);
3702
+ handle = null;
3703
+ };
3704
+ const unsubscribeOpen = deps.on("open", onOpen);
3705
+ const unsubscribeClose = deps.on("close", onClose);
3706
+ if (deps.getSize() > 0) onOpen();
3707
+ return () => {
3708
+ unsubscribeOpen();
3709
+ unsubscribeClose();
3710
+ if (handle) {
3711
+ popBackMarker(handle);
3712
+ handle = null;
3713
+ }
3714
+ };
3715
+ }
3716
+ function installRouteChange(deps) {
3717
+ return subscribeRouteChange(() => {
3718
+ if (deps.isDestroyed() || deps.getSize() <= 0) return;
3719
+ void deps.close();
3720
+ });
3721
+ }
3722
+
3723
+ // src/core/features/persist.ts
3724
+ function installPersist(engine, key) {
3725
+ const unsubscribe = engine.on("snap", ({ id }) => {
3726
+ if (typeof window === "undefined") return;
3727
+ if (typeof localStorage === "undefined") return;
3728
+ try {
3729
+ localStorage.setItem(key, id);
3730
+ } catch {
3731
+ }
3732
+ });
3733
+ return unsubscribe;
3734
+ }
3735
+
3736
+ // src/core/features/auto-collapse.ts
3737
+ function installAutoCollapse(deps) {
3738
+ const { ms } = deps;
3739
+ if (ms === void 0 || ms <= 0) {
3740
+ return () => {
3741
+ };
3742
+ }
3743
+ let timer = null;
3744
+ const clearTimer = () => {
3745
+ if (timer !== null) {
3746
+ clearTimeout(timer);
3747
+ timer = null;
3748
+ }
3749
+ };
3750
+ const fire = () => {
3751
+ if (deps.isDestroyed()) return;
3752
+ if (deps.isDragging()) return;
3753
+ const active = deps.resolveSnap(deps.getActiveId());
3754
+ if (!active || active.size === 0) return;
3755
+ let target = null;
3756
+ for (const id of deps.getAllowedIds()) {
3757
+ const snap = deps.resolveSnap(id);
3758
+ if (!snap || snap.size <= 0) continue;
3759
+ if (!target || snap.size < target.size) target = snap;
3760
+ }
3761
+ if (!target) return;
3762
+ if (target.id === active.id || target.size >= active.size) return;
3763
+ deps.snapTo(target.id);
3764
+ };
3765
+ const reset = () => {
3766
+ clearTimer();
3767
+ if (deps.isDestroyed()) return;
3768
+ timer = setTimeout(() => {
3769
+ timer = null;
3770
+ fire();
3771
+ }, ms);
3772
+ };
3773
+ const unsubscribe = deps.on("snap", () => reset());
3774
+ if ((deps.resolveSnap(deps.getActiveId())?.size ?? 0) > 0) reset();
3775
+ return () => {
3776
+ clearTimer();
3777
+ unsubscribe();
3778
+ };
3779
+ }
3780
+
3781
+ // src/core/features/content-swipe.ts
3782
+ var CONTENT_PULL_THRESHOLD = 60;
3783
+ function installContentSwipe(deps) {
3784
+ const { container } = deps;
3785
+ let startY = 0;
3786
+ let startScrollTop = 0;
3787
+ let moved = false;
3788
+ const onTouchStart = (e) => {
3789
+ const t = e.touches[0];
3790
+ if (!t) return;
3791
+ startY = t.clientY;
3792
+ startScrollTop = container.scrollTop;
3793
+ moved = false;
3794
+ };
3795
+ const onTouchMove = (e) => {
3796
+ const t = e.touches[0];
3797
+ if (!t) return;
3798
+ if (Math.abs(t.clientY - startY) > CONTENT_PULL_THRESHOLD) moved = true;
3799
+ };
3800
+ const onTouchEnd = (e) => {
3801
+ if (!moved) return;
3802
+ if (deps.isDragging() || deps.isAnimating()) return;
3803
+ if (container.scrollTop !== startScrollTop) return;
3804
+ const t = e.changedTouches[0];
3805
+ if (!t) return;
3806
+ const deltaY = t.clientY - startY;
3807
+ const atTop = container.scrollTop <= 0;
3808
+ const allowed = deps.getAllowedIds();
3809
+ const idx = allowed.indexOf(deps.getActiveId());
3810
+ if (idx === -1) return;
3811
+ if (atTop && deltaY > CONTENT_PULL_THRESHOLD && idx > 0) {
3812
+ deps.snapTo(allowed[idx - 1]);
3813
+ } else if (deltaY < -CONTENT_PULL_THRESHOLD && idx < allowed.length - 1) {
3814
+ deps.snapTo(allowed[idx + 1]);
3815
+ }
3816
+ };
3817
+ container.addEventListener("touchstart", onTouchStart, { passive: true });
3818
+ container.addEventListener("touchmove", onTouchMove, { passive: true });
3819
+ container.addEventListener("touchend", onTouchEnd, { passive: true });
3820
+ return () => {
3821
+ container.removeEventListener("touchstart", onTouchStart);
3822
+ container.removeEventListener("touchmove", onTouchMove);
3823
+ container.removeEventListener("touchend", onTouchEnd);
3824
+ };
3825
+ }
3826
+
3827
+ // src/core/features/visual-viewport.ts
3828
+ var SAFETY_PAD = 8;
3829
+ function installVisualViewport(deps) {
3830
+ if (typeof window === "undefined" || !window.visualViewport) {
3831
+ return () => {
3832
+ };
3833
+ }
3834
+ const vv = window.visualViewport;
3835
+ let rafId = null;
3836
+ const onResize = () => {
3837
+ if (rafId !== null) return;
3838
+ rafId = requestAnimationFrame(() => {
3839
+ rafId = null;
3840
+ if (deps.isDestroyed()) return;
3841
+ deps.recomputeSnaps();
3842
+ const isVertical = deps.isVerticalAxis();
3843
+ const visible = isVertical ? vv.height : vv.width;
3844
+ const windowSize = isVertical ? window.innerHeight : window.innerWidth;
3845
+ const clamp = Math.max(0, Math.min(windowSize, visible) - SAFETY_PAD);
3846
+ if (clamp < deps.getMaxAxisSize()) {
3847
+ deps.setMaxAxisSize(clamp);
3848
+ }
3849
+ const wasAnimating = deps.isAnimating?.() ?? false;
3850
+ deps.cancelInFlight();
3851
+ deps.newCycle();
3852
+ const max = deps.getMaxAxisSize();
3853
+ deps.setSize(Math.min(deps.getSize(), max));
3854
+ const current = deps.resolveActiveSnap();
3855
+ if (current && !deps.isDragging()) {
3856
+ deps.applySize(Math.min(current.size, max));
3857
+ if (wasAnimating) deps.resyncAfterCancel?.();
3858
+ } else {
3859
+ deps.applySize(deps.getSize());
3860
+ }
3861
+ });
3862
+ };
3863
+ vv.addEventListener("resize", onResize);
3864
+ return () => {
3865
+ vv.removeEventListener("resize", onResize);
3866
+ if (rafId !== null) cancelAnimationFrame(rafId);
3867
+ };
3868
+ }
3869
+
3870
+ // src/core/features/engine-features.ts
3871
+ function routeFeature() {
3872
+ return {
3873
+ name: "route",
3874
+ install: (ctx) => {
3875
+ const teardowns = new TeardownStack();
3876
+ if (ctx.options.closeOnRouteChange) {
3877
+ teardowns.add(
3878
+ installRouteChange({
3879
+ isDestroyed: ctx.isDestroyed,
3880
+ getSize: ctx.getSize,
3881
+ close: () => ctx.close()
3882
+ })
3883
+ );
3884
+ }
3885
+ teardowns.add(
3886
+ installRoute({
3887
+ routedTo: ctx.options.routedTo,
3888
+ closeOnBack: ctx.options.closeOnBack,
3889
+ isTopSheet: ctx.isTopSheet,
3890
+ getSize: ctx.getSize,
3891
+ isDestroyed: ctx.isDestroyed,
3892
+ close: () => ctx.close("back"),
3893
+ on: ctx.on,
3894
+ sheetId: ctx.sheetId
3895
+ })
3896
+ );
3897
+ return () => teardowns.drain();
3898
+ }
3899
+ };
3900
+ }
3901
+ function persistFeature() {
3902
+ return {
3903
+ name: "persist",
3904
+ install: (ctx) => {
3905
+ const key = ctx.options.persistKey;
3906
+ if (!key) return;
3907
+ return installPersist({ on: ctx.on }, key);
3908
+ }
3909
+ };
3910
+ }
3911
+ function autoCollapseFeature() {
3912
+ return {
3913
+ name: "auto-collapse",
3914
+ install: (ctx) => installAutoCollapse({
3915
+ ms: ctx.options.autoCollapseAfter,
3916
+ isDestroyed: ctx.isDestroyed,
3917
+ isDragging: ctx.isDragging,
3918
+ getAllowedIds: ctx.getAllowedIds,
3919
+ getActiveId: ctx.getActiveId,
3920
+ resolveSnap: ctx.resolveSnap,
3921
+ snapTo: ctx.snapTo,
3922
+ on: ctx.on
3923
+ })
3924
+ };
3925
+ }
3926
+ function contentSwipeFeature() {
3927
+ return {
3928
+ name: "content-swipe",
3929
+ stage: "attach",
3930
+ install: (ctx) => {
3931
+ if (!ctx.scrollContainer) return;
3932
+ return installContentSwipe({
3933
+ container: ctx.scrollContainer,
3934
+ isDragging: ctx.isDragging,
3935
+ isAnimating: ctx.isAnimating,
3936
+ getAllowedIds: ctx.allowedIdsBySize,
3937
+ getActiveId: ctx.getActiveId,
3938
+ snapTo: ctx.snapTo
3939
+ });
3940
+ }
3941
+ };
3942
+ }
3943
+ function visualViewportFeature() {
3944
+ return {
3945
+ name: "visual-viewport",
3946
+ install: (ctx) => {
3947
+ if (typeof window === "undefined") return;
3948
+ return installVisualViewport({
3949
+ element: ctx.element,
3950
+ isVerticalAxis: ctx.isVerticalAxis,
3951
+ isDestroyed: ctx.isDestroyed,
3952
+ isDragging: ctx.isDragging,
3953
+ recomputeSnaps: ctx.recomputeSnaps,
3954
+ resolveActiveSnap: ctx.resolveActiveSnap,
3955
+ getMaxAxisSize: ctx.getMaxAxisSize,
3956
+ getSize: ctx.getSize,
3957
+ setMaxAxisSize: ctx.setMaxAxisSize,
3958
+ setSize: ctx.setSize,
3959
+ applySize: ctx.applySize,
3960
+ cancelInFlight: ctx.cancelInFlight,
3961
+ newCycle: ctx.newCycle,
3962
+ isAnimating: ctx.isAnimating,
3963
+ resyncAfterCancel: ctx.resyncAfterResize
3964
+ });
3965
+ }
3966
+ };
3967
+ }
3968
+
3969
+ // src/core/default-features.ts
3970
+ function defaultEngineFeatures() {
3971
+ return [
3972
+ contentSwipeFeature(),
3973
+ visualViewportFeature(),
3974
+ persistFeature(),
3975
+ autoCollapseFeature(),
3976
+ routeFeature()
3977
+ ];
3978
+ }
3979
+
3980
+ // src/core/BottomSheetEngine.ts
3981
+ var BottomSheetEngine = class extends BottomSheetCore {
3982
+ constructor(opts) {
3983
+ super({
3984
+ ...opts,
3985
+ features: [...defaultEngineFeatures(), ...opts.features ?? []]
3986
+ });
3987
+ }
3988
+ };
3715
3989
  function useEnginePropSync(engineRef, opts) {
3716
3990
  const optsRef = useRef(opts);
3717
3991
  optsRef.current = opts;