@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/qwik.js CHANGED
@@ -349,74 +349,6 @@ var SnapResolver = class {
349
349
  }
350
350
  };
351
351
 
352
- // src/core/features/persist.ts
353
- function readPersistedId(key) {
354
- if (!key) return null;
355
- if (typeof window === "undefined") return null;
356
- if (typeof localStorage === "undefined") return null;
357
- try {
358
- return localStorage.getItem(key);
359
- } catch {
360
- return null;
361
- }
362
- }
363
- function installPersist(engine, key) {
364
- const unsubscribe = engine.on("snap", ({ id }) => {
365
- if (typeof window === "undefined") return;
366
- if (typeof localStorage === "undefined") return;
367
- try {
368
- localStorage.setItem(key, id);
369
- } catch {
370
- }
371
- });
372
- return unsubscribe;
373
- }
374
-
375
- // src/core/features/auto-collapse.ts
376
- function installAutoCollapse(deps) {
377
- const { ms } = deps;
378
- if (ms === void 0 || ms <= 0) {
379
- return () => {
380
- };
381
- }
382
- let timer = null;
383
- const clearTimer = () => {
384
- if (timer !== null) {
385
- clearTimeout(timer);
386
- timer = null;
387
- }
388
- };
389
- const fire = () => {
390
- if (deps.isDestroyed()) return;
391
- if (deps.isDragging()) return;
392
- const active = deps.resolveSnap(deps.getActiveId());
393
- if (!active || active.size === 0) return;
394
- let target = null;
395
- for (const id of deps.getAllowedIds()) {
396
- const snap = deps.resolveSnap(id);
397
- if (!snap || snap.size <= 0) continue;
398
- if (!target || snap.size < target.size) target = snap;
399
- }
400
- if (!target) return;
401
- if (target.id === active.id || target.size >= active.size) return;
402
- deps.snapTo(target.id);
403
- };
404
- const reset = () => {
405
- clearTimer();
406
- if (deps.isDestroyed()) return;
407
- timer = setTimeout(() => {
408
- timer = null;
409
- fire();
410
- }, ms);
411
- };
412
- const unsubscribe = deps.on("snap", () => reset());
413
- if ((deps.resolveSnap(deps.getActiveId())?.size ?? 0) > 0) reset();
414
- return () => {
415
- clearTimer();
416
- unsubscribe();
417
- };
418
- }
419
-
420
352
  // src/core/features/linked-sheets.ts
421
353
  function notifyLinkedSheets(sheets, self) {
422
354
  if (sheets.length === 0) return;
@@ -439,390 +371,65 @@ function notifyLinkedSheets(sheets, self) {
439
371
  }
440
372
  }
441
373
 
442
- // src/core/features/history-coordinator.ts
443
- var entries = [];
444
- var subscribers = /* @__PURE__ */ new Set();
445
- var suppress = 0;
446
- var internalDepth = 0;
447
- var pendingRebrand = null;
448
- var listenerInstalled = false;
449
- var patchInstalled = false;
450
- var origPushState = null;
451
- var origReplaceState = null;
452
- var patchedPushState = null;
453
- var patchedReplaceState = null;
454
- var hasWindow = () => typeof window !== "undefined";
455
- var hasHistory = () => typeof history !== "undefined";
456
- var runInternal = (fn) => {
457
- internalDepth += 1;
458
- try {
459
- fn();
460
- } finally {
461
- internalDepth -= 1;
462
- }
463
- };
464
- var notifyRouteChange = () => {
465
- const snapshot = Array.from(subscribers);
466
- for (const fn of snapshot) fn();
467
- };
468
- var ensureListener = () => {
469
- if (listenerInstalled || !hasWindow()) return;
470
- window.addEventListener("popstate", onPopState);
471
- listenerInstalled = true;
472
- };
473
- var maybeTeardownListener = () => {
474
- if (!listenerInstalled) return;
475
- if (entries.length > 0 || subscribers.size > 0 || suppress > 0) return;
476
- if (hasWindow()) window.removeEventListener("popstate", onPopState);
477
- listenerInstalled = false;
478
- };
479
- var installPatch = () => {
480
- if (patchInstalled || !hasHistory()) return;
481
- origPushState = history.pushState;
482
- origReplaceState = history.replaceState;
483
- patchedPushState = function(...args) {
484
- origPushState.apply(this, args);
485
- if (internalDepth === 0) notifyRouteChange();
486
- };
487
- patchedReplaceState = function(...args) {
488
- origReplaceState.apply(this, args);
489
- if (internalDepth === 0) notifyRouteChange();
490
- };
491
- history.pushState = patchedPushState;
492
- history.replaceState = patchedReplaceState;
493
- patchInstalled = true;
494
- };
495
- var removePatch = () => {
496
- if (!patchInstalled) return;
497
- if (history.pushState === patchedPushState && origPushState) {
498
- history.pushState = origPushState;
499
- }
500
- if (history.replaceState === patchedReplaceState && origReplaceState) {
501
- history.replaceState = origReplaceState;
502
- }
503
- origPushState = null;
504
- origReplaceState = null;
505
- patchedPushState = null;
506
- patchedReplaceState = null;
507
- patchInstalled = false;
508
- };
509
- function onPopState() {
510
- if (suppress > 0) {
511
- suppress -= 1;
512
- const rebrand = pendingRebrand;
513
- pendingRebrand = null;
514
- if (rebrand) rebrand();
515
- maybeTeardownListener();
516
- return;
517
- }
518
- if (entries.length === 0) {
519
- notifyRouteChange();
520
- maybeTeardownListener();
521
- return;
374
+ // src/core/features/resize-observer.ts
375
+ function installResizeObserver(deps) {
376
+ if (typeof window === "undefined") {
377
+ return () => {
378
+ };
522
379
  }
523
- let target = null;
524
- let targetIdx = -1;
525
- for (let i = entries.length - 1; i >= 0; i -= 1) {
526
- const entry = entries[i];
527
- if (!entry.surface.isOpen()) continue;
528
- if (!target) {
529
- target = entry;
530
- targetIdx = i;
531
- }
532
- if (entry.surface.isTop()) {
533
- target = entry;
534
- targetIdx = i;
535
- break;
380
+ const onResize = () => {
381
+ if (deps.isDestroyed()) return;
382
+ deps.recomputeSnaps();
383
+ const mode = deps.getMode();
384
+ const isVerticalAxis = mode === "bottom" || mode === "top";
385
+ const viewportSize2 = isVerticalAxis ? window.innerHeight : window.innerWidth;
386
+ const max = deps.getMaxAxisSize();
387
+ if (viewportSize2 > 0 && viewportSize2 < max) {
388
+ deps.setMaxAxisSize(viewportSize2);
536
389
  }
537
- }
538
- if (!target) {
539
- const removed = entries.pop();
540
- if (removed) removed.live = false;
541
- maybeTeardownListener();
542
- return;
543
- }
544
- target.live = false;
545
- entries.splice(targetIdx, 1);
546
- const settled = target;
547
- void Promise.resolve(settled.surface.close()).then(() => {
548
- if (settled.surface.isOpen()) restore(settled);
549
- });
550
- maybeTeardownListener();
551
- }
552
- function restore(entry) {
553
- if (!hasWindow() || !hasHistory()) return;
554
- entry.live = true;
555
- runInternal(() => {
556
- try {
557
- history.pushState(entry.surface.markerState(), "", entry.surface.url);
558
- } catch {
559
- entry.live = false;
390
+ const wasAnimating = deps.isAnimating?.() ?? false;
391
+ deps.cancelInFlight();
392
+ deps.newCycle();
393
+ const current = deps.resolveActiveSnap();
394
+ const newMax = deps.getMaxAxisSize();
395
+ const targetSize = current ? Math.min(current.size, newMax) : Math.min(deps.getSize(), newMax);
396
+ deps.setSize(targetSize);
397
+ if (current && !deps.isDragging()) {
398
+ deps.applySize(targetSize);
399
+ if (wasAnimating) deps.resyncAfterCancel?.();
560
400
  }
561
- });
562
- if (entry.live) {
563
- entries.push(entry);
564
- ensureListener();
401
+ };
402
+ let resizeObserver = null;
403
+ if (typeof ResizeObserver !== "undefined") {
404
+ resizeObserver = new ResizeObserver(onResize);
405
+ resizeObserver.observe(document.documentElement);
406
+ } else {
407
+ window.addEventListener("resize", onResize);
565
408
  }
409
+ window.addEventListener("orientationchange", onResize);
410
+ const onVisibility = () => {
411
+ if (deps.isDestroyed()) return;
412
+ if (document.hidden) deps.cancelInFlight();
413
+ };
414
+ document.addEventListener("visibilitychange", onVisibility);
415
+ return () => {
416
+ resizeObserver?.disconnect();
417
+ window.removeEventListener("resize", onResize);
418
+ window.removeEventListener("orientationchange", onResize);
419
+ document.removeEventListener("visibilitychange", onVisibility);
420
+ };
566
421
  }
567
- function pushBackMarker(surface) {
568
- const entry = { surface, priorUrl: void 0, live: false };
569
- if (!hasWindow() || !hasHistory()) {
570
- return entry;
571
- }
572
- entry.priorUrl = surface.url !== void 0 ? location.href : void 0;
573
- runInternal(() => {
574
- try {
575
- history.pushState(surface.markerState(), "", surface.url);
576
- entry.live = true;
577
- } catch {
578
- entry.live = false;
579
- }
580
- });
581
- if (entry.live) {
582
- entries.push(entry);
583
- ensureListener();
584
- }
585
- return entry;
586
- }
587
- function popBackMarker(handle) {
588
- if (!hasWindow() || !hasHistory()) return;
589
- const entry = handle;
590
- const idx = entries.indexOf(entry);
591
- if (idx === -1 || !entry.live) return;
592
- const last = entries.length - 1;
593
- entry.live = false;
594
- entries.splice(idx, 1);
595
- if (idx !== last) {
596
- const rebrandSurface = entries[entries.length - 1].surface;
597
- const rebrandUrl = entry.priorUrl;
598
- pendingRebrand = () => {
599
- runInternal(() => {
600
- history.replaceState(
601
- rebrandSurface.markerState(),
602
- "",
603
- rebrandUrl !== void 0 ? rebrandUrl : location.href
604
- );
605
- });
606
- };
607
- }
608
- suppress += 1;
609
- runInternal(() => {
610
- try {
611
- history.back();
612
- } catch {
613
- suppress = Math.max(0, suppress - 1);
614
- pendingRebrand = null;
615
- }
616
- });
617
- maybeTeardownListener();
618
- }
619
- function subscribeRouteChange(fn) {
620
- if (!hasWindow() || !hasHistory()) return () => {
621
- };
622
- subscribers.add(fn);
623
- if (subscribers.size === 1) installPatch();
624
- ensureListener();
625
- return () => {
626
- if (!subscribers.has(fn)) return;
627
- subscribers.delete(fn);
628
- if (subscribers.size === 0) removePatch();
629
- maybeTeardownListener();
630
- };
631
- }
632
-
633
- // src/core/features/route.ts
634
- function installRoute(deps) {
635
- if (typeof window === "undefined") {
636
- return () => {
637
- };
638
- }
639
- if (!deps.routedTo && !deps.closeOnBack) {
640
- return () => {
641
- };
642
- }
643
- let handle = null;
644
- const onOpen = () => {
645
- if (handle) return;
646
- handle = pushBackMarker({
647
- isOpen: () => !deps.isDestroyed() && deps.getSize() > 0,
648
- isTop: deps.isTopSheet,
649
- close: deps.close,
650
- markerState: () => deps.routedTo !== void 0 ? { __bsRouted: deps.sheetId } : { __bsSheet: deps.sheetId },
651
- url: deps.routedTo
652
- });
653
- };
654
- const onClose = () => {
655
- if (!handle) return;
656
- popBackMarker(handle);
657
- handle = null;
658
- };
659
- const unsubscribeOpen = deps.on("open", onOpen);
660
- const unsubscribeClose = deps.on("close", onClose);
661
- if (deps.getSize() > 0) onOpen();
662
- return () => {
663
- unsubscribeOpen();
664
- unsubscribeClose();
665
- if (handle) {
666
- popBackMarker(handle);
667
- handle = null;
668
- }
669
- };
670
- }
671
- function installRouteChange(deps) {
672
- return subscribeRouteChange(() => {
673
- if (deps.isDestroyed() || deps.getSize() <= 0) return;
674
- void deps.close();
675
- });
676
- }
677
-
678
- // src/core/features/content-swipe.ts
679
- var CONTENT_PULL_THRESHOLD = 60;
680
- function installContentSwipe(deps) {
681
- const { container } = deps;
682
- let startY = 0;
683
- let startScrollTop = 0;
684
- let moved = false;
685
- const onTouchStart = (e) => {
686
- const t = e.touches[0];
687
- if (!t) return;
688
- startY = t.clientY;
689
- startScrollTop = container.scrollTop;
690
- moved = false;
691
- };
692
- const onTouchMove = (e) => {
693
- const t = e.touches[0];
694
- if (!t) return;
695
- if (Math.abs(t.clientY - startY) > CONTENT_PULL_THRESHOLD) moved = true;
696
- };
697
- const onTouchEnd = (e) => {
698
- if (!moved) return;
699
- if (deps.isDragging() || deps.isAnimating()) return;
700
- if (container.scrollTop !== startScrollTop) return;
701
- const t = e.changedTouches[0];
702
- if (!t) return;
703
- const deltaY = t.clientY - startY;
704
- const atTop = container.scrollTop <= 0;
705
- const allowed = deps.getAllowedIds();
706
- const idx = allowed.indexOf(deps.getActiveId());
707
- if (idx === -1) return;
708
- if (atTop && deltaY > CONTENT_PULL_THRESHOLD && idx > 0) {
709
- deps.snapTo(allowed[idx - 1]);
710
- } else if (deltaY < -CONTENT_PULL_THRESHOLD && idx < allowed.length - 1) {
711
- deps.snapTo(allowed[idx + 1]);
712
- }
713
- };
714
- container.addEventListener("touchstart", onTouchStart, { passive: true });
715
- container.addEventListener("touchmove", onTouchMove, { passive: true });
716
- container.addEventListener("touchend", onTouchEnd, { passive: true });
717
- return () => {
718
- container.removeEventListener("touchstart", onTouchStart);
719
- container.removeEventListener("touchmove", onTouchMove);
720
- container.removeEventListener("touchend", onTouchEnd);
721
- };
722
- }
723
-
724
- // src/core/features/visual-viewport.ts
725
- var SAFETY_PAD = 8;
726
- function installVisualViewport(deps) {
727
- if (typeof window === "undefined" || !window.visualViewport) {
728
- return () => {
729
- };
730
- }
731
- const vv = window.visualViewport;
732
- let rafId = null;
733
- const onResize = () => {
734
- if (rafId !== null) return;
735
- rafId = requestAnimationFrame(() => {
736
- rafId = null;
737
- if (deps.isDestroyed()) return;
738
- deps.recomputeSnaps();
739
- const isVertical = deps.isVerticalAxis();
740
- const visible = isVertical ? vv.height : vv.width;
741
- const windowSize = isVertical ? window.innerHeight : window.innerWidth;
742
- const clamp = Math.max(0, Math.min(windowSize, visible) - SAFETY_PAD);
743
- if (clamp < deps.getMaxAxisSize()) {
744
- deps.setMaxAxisSize(clamp);
745
- }
746
- const wasAnimating = deps.isAnimating?.() ?? false;
747
- deps.cancelInFlight();
748
- deps.newCycle();
749
- const max = deps.getMaxAxisSize();
750
- deps.setSize(Math.min(deps.getSize(), max));
751
- const current = deps.resolveActiveSnap();
752
- if (current && !deps.isDragging()) {
753
- deps.applySize(Math.min(current.size, max));
754
- if (wasAnimating) deps.resyncAfterCancel?.();
755
- } else {
756
- deps.applySize(deps.getSize());
757
- }
758
- });
759
- };
760
- vv.addEventListener("resize", onResize);
761
- return () => {
762
- vv.removeEventListener("resize", onResize);
763
- if (rafId !== null) cancelAnimationFrame(rafId);
764
- };
765
- }
766
-
767
- // src/core/features/resize-observer.ts
768
- function installResizeObserver(deps) {
769
- if (typeof window === "undefined") {
770
- return () => {
771
- };
772
- }
773
- const onResize = () => {
774
- if (deps.isDestroyed()) return;
775
- deps.recomputeSnaps();
776
- const mode = deps.getMode();
777
- const isVerticalAxis = mode === "bottom" || mode === "top";
778
- const viewportSize2 = isVerticalAxis ? window.innerHeight : window.innerWidth;
779
- const max = deps.getMaxAxisSize();
780
- if (viewportSize2 > 0 && viewportSize2 < max) {
781
- deps.setMaxAxisSize(viewportSize2);
782
- }
783
- const wasAnimating = deps.isAnimating?.() ?? false;
784
- deps.cancelInFlight();
785
- deps.newCycle();
786
- const current = deps.resolveActiveSnap();
787
- const newMax = deps.getMaxAxisSize();
788
- const targetSize = current ? Math.min(current.size, newMax) : Math.min(deps.getSize(), newMax);
789
- deps.setSize(targetSize);
790
- if (current && !deps.isDragging()) {
791
- deps.applySize(targetSize);
792
- if (wasAnimating) deps.resyncAfterCancel?.();
793
- }
794
- };
795
- let resizeObserver = null;
796
- if (typeof ResizeObserver !== "undefined") {
797
- resizeObserver = new ResizeObserver(onResize);
798
- resizeObserver.observe(document.documentElement);
799
- } else {
800
- window.addEventListener("resize", onResize);
801
- }
802
- window.addEventListener("orientationchange", onResize);
803
- const onVisibility = () => {
804
- if (deps.isDestroyed()) return;
805
- if (document.hidden) deps.cancelInFlight();
806
- };
807
- document.addEventListener("visibilitychange", onVisibility);
808
- return () => {
809
- resizeObserver?.disconnect();
810
- window.removeEventListener("resize", onResize);
811
- window.removeEventListener("orientationchange", onResize);
812
- document.removeEventListener("visibilitychange", onVisibility);
813
- };
814
- }
815
-
816
- // src/core/features/fit-observer.ts
817
- var contentTargets = (scroller) => {
818
- if (!scroller) return [];
819
- const hasSlot = typeof HTMLSlotElement !== "undefined";
820
- const out = [];
821
- for (const child of Array.from(scroller.children)) {
822
- if (hasSlot && child instanceof HTMLSlotElement) {
823
- for (const el of child.assignedElements()) out.push(el);
824
- } else {
825
- out.push(child);
422
+
423
+ // src/core/features/fit-observer.ts
424
+ var contentTargets = (scroller) => {
425
+ if (!scroller) return [];
426
+ const hasSlot = typeof HTMLSlotElement !== "undefined";
427
+ const out = [];
428
+ for (const child of Array.from(scroller.children)) {
429
+ if (hasSlot && child instanceof HTMLSlotElement) {
430
+ for (const el of child.assignedElements()) out.push(el);
431
+ } else {
432
+ out.push(child);
826
433
  }
827
434
  }
828
435
  return out;
@@ -942,11 +549,15 @@ function createMaxHeightController(deps) {
942
549
 
943
550
  // src/core/features/fit-measurement.ts
944
551
  var measureSheetNatural = (element, content, vertical) => {
945
- if (!content) {
946
- return vertical ? element.offsetHeight : element.offsetWidth;
947
- }
948
552
  const axis = vertical ? "height" : "width";
949
553
  const sheetStyle = element.style;
554
+ if (!content) {
555
+ const prev = sheetStyle[axis];
556
+ sheetStyle[axis] = "auto";
557
+ const poked2 = vertical ? element.offsetHeight : element.offsetWidth;
558
+ sheetStyle[axis] = prev;
559
+ return poked2;
560
+ }
950
561
  const cs = content.style;
951
562
  const prevSheet = sheetStyle[axis];
952
563
  const prevFlex = cs.flex;
@@ -1660,6 +1271,52 @@ function resolveAnimationPreset(animation) {
1660
1271
  }
1661
1272
  }
1662
1273
 
1274
+ // src/core/animation/waapi-settle.ts
1275
+ var SIM_HZ = 240;
1276
+ var SAMPLE_STRIDE = 4;
1277
+ var MAX_SIM_MS = 3e3;
1278
+ function sampleSpringSettle(from, to, velocityPxPerS, config) {
1279
+ const cfg = { ...DEFAULT_SPRING, ...config };
1280
+ const subDt = 1 / SIM_HZ;
1281
+ const stepMs = SAMPLE_STRIDE / SIM_HZ * 1e3;
1282
+ const maxSteps = Math.ceil(MAX_SIM_MS / 1e3 * SIM_HZ);
1283
+ const values = [from];
1284
+ let x = from;
1285
+ let v = velocityPxPerS;
1286
+ for (let i = 1; i <= maxSteps; i += 1) {
1287
+ const force = -cfg.stiffness * (x - to) - cfg.damping * v;
1288
+ v += force / cfg.mass * subDt;
1289
+ x += v * subDt;
1290
+ if (!Number.isFinite(x) || !Number.isFinite(v)) {
1291
+ values.push(to);
1292
+ break;
1293
+ }
1294
+ if (i % SAMPLE_STRIDE === 0) values.push(x);
1295
+ const atRest = Math.abs(to - x) < (cfg.restDelta ?? 0.5) && Math.abs(v) < (cfg.restSpeed ?? 0.5);
1296
+ if (atRest) break;
1297
+ }
1298
+ if (values[values.length - 1] !== to) values.push(to);
1299
+ return {
1300
+ values,
1301
+ stepMs,
1302
+ durationMs: (values.length - 1) * stepMs
1303
+ };
1304
+ }
1305
+ function sampleTweenSettle(from, to, durationMs, easing) {
1306
+ const count = Math.max(2, Math.min(60, Math.round(durationMs / 16)));
1307
+ const values = [];
1308
+ for (let i = 0; i <= count; i += 1) {
1309
+ const t = i / count;
1310
+ values.push(from + (to - from) * easing(t));
1311
+ }
1312
+ values[values.length - 1] = to;
1313
+ return {
1314
+ values,
1315
+ stepMs: durationMs / count,
1316
+ durationMs
1317
+ };
1318
+ }
1319
+
1663
1320
  // src/core/controllers/animation-runner.ts
1664
1321
  var DEFAULT_DURATION2 = 220;
1665
1322
  var VELOCITY_PX_PER_S = 1e3;
@@ -1670,6 +1327,10 @@ var AnimationRunner = class {
1670
1327
  __publicField(this, "applySizeFn");
1671
1328
  __publicField(this, "getSize");
1672
1329
  __publicField(this, "isDragging");
1330
+ __publicField(this, "applyAux");
1331
+ __publicField(this, "getTransformFor");
1332
+ __publicField(this, "settleWaapi");
1333
+ __publicField(this, "currentWaapi", null);
1673
1334
  __publicField(this, "animationKind");
1674
1335
  __publicField(this, "springConfig");
1675
1336
  __publicField(this, "duration");
@@ -1685,6 +1346,9 @@ var AnimationRunner = class {
1685
1346
  this.applySizeFn = deps.applySize;
1686
1347
  this.getSize = deps.getSize;
1687
1348
  this.isDragging = deps.isDragging;
1349
+ this.applyAux = deps.applyAux;
1350
+ this.getTransformFor = deps.getTransformFor;
1351
+ this.settleWaapi = opts.settleAnimation === "waapi";
1688
1352
  const preset = resolveAnimationPreset(opts.animation);
1689
1353
  this.animationKind = preset.kind;
1690
1354
  this.duration = opts.duration ?? preset.duration ?? DEFAULT_DURATION2;
@@ -1709,11 +1373,20 @@ var AnimationRunner = class {
1709
1373
  }
1710
1374
  }
1711
1375
  get isAnimating() {
1712
- return this.currentTween !== null || this.currentSpring !== null;
1376
+ return this.currentTween !== null || this.currentSpring !== null || this.currentWaapi !== null;
1713
1377
  }
1714
1378
  cancel() {
1715
1379
  this.currentTween?.cancel();
1716
1380
  this.currentSpring?.cancel();
1381
+ if (this.currentWaapi) {
1382
+ const entry = this.currentWaapi;
1383
+ this.currentWaapi = null;
1384
+ entry.stop();
1385
+ try {
1386
+ entry.anim.cancel();
1387
+ } catch {
1388
+ }
1389
+ }
1717
1390
  }
1718
1391
  async animateTo(target, velocityPxPerMs) {
1719
1392
  if (target === this.getSize()) {
@@ -1732,7 +1405,28 @@ var AnimationRunner = class {
1732
1405
  if (!this.isDragging()) this.element.style.willChange = "";
1733
1406
  return;
1734
1407
  }
1735
- if (this.animationKind === "spring") {
1408
+ if (this.settleWaapi && this.applyAux && this.getTransformFor && typeof this.element.animate === "function") {
1409
+ const samples = this.animationKind === "spring" ? sampleSpringSettle(
1410
+ this.getSize(),
1411
+ target,
1412
+ velocityPxPerMs * VELOCITY_PX_PER_S,
1413
+ this.springConfig
1414
+ ) : sampleTweenSettle(
1415
+ this.getSize(),
1416
+ target,
1417
+ this.duration,
1418
+ this.easing
1419
+ );
1420
+ if (samples.values.length >= 2 && samples.durationMs > 0) {
1421
+ await this.runWaapiSettle(samples, target);
1422
+ if (!this.isAnimating) {
1423
+ this.getRootEl()?.removeAttribute("data-animating");
1424
+ if (!this.isDragging()) this.element.style.willChange = "";
1425
+ }
1426
+ return;
1427
+ }
1428
+ }
1429
+ if (this.animationKind === "spring") {
1736
1430
  const spring = runSpring({
1737
1431
  from: this.getSize(),
1738
1432
  to: target,
@@ -1760,11 +1454,66 @@ var AnimationRunner = class {
1760
1454
  if (!this.isDragging()) this.element.style.willChange = "";
1761
1455
  }
1762
1456
  }
1457
+ async runWaapiSettle(samples, target) {
1458
+ const applyAux = this.applyAux;
1459
+ const getTransformFor = this.getTransformFor;
1460
+ const frames = samples.values.map((v) => ({
1461
+ transform: getTransformFor(v)
1462
+ }));
1463
+ const anim = this.element.animate(frames, {
1464
+ duration: samples.durationMs,
1465
+ easing: "linear",
1466
+ fill: "forwards"
1467
+ });
1468
+ let stopped = false;
1469
+ let auxRaf = 0;
1470
+ const start = performance.now();
1471
+ const lastIdx = samples.values.length - 1;
1472
+ const auxTick = () => {
1473
+ if (stopped) return;
1474
+ const elapsed = performance.now() - start;
1475
+ const idx = Math.min(Math.floor(elapsed / samples.stepMs), lastIdx);
1476
+ applyAux(samples.values[idx]);
1477
+ auxRaf = requestAnimationFrame(auxTick);
1478
+ };
1479
+ auxRaf = requestAnimationFrame(auxTick);
1480
+ const entry = {
1481
+ anim,
1482
+ stop: () => {
1483
+ stopped = true;
1484
+ cancelAnimationFrame(auxRaf);
1485
+ }
1486
+ };
1487
+ this.currentWaapi = entry;
1488
+ try {
1489
+ await anim.finished;
1490
+ } catch {
1491
+ }
1492
+ entry.stop();
1493
+ const wasCurrent = this.currentWaapi === entry;
1494
+ if (wasCurrent) this.currentWaapi = null;
1495
+ if (wasCurrent && anim.playState === "finished") {
1496
+ this.applySizeFn(target);
1497
+ try {
1498
+ anim.cancel();
1499
+ } catch {
1500
+ }
1501
+ }
1502
+ }
1763
1503
  destroy() {
1764
1504
  this.detachReducedMotion?.();
1765
1505
  this.detachReducedMotion = null;
1766
1506
  this.currentTween = null;
1767
1507
  this.currentSpring = null;
1508
+ if (this.currentWaapi) {
1509
+ const entry = this.currentWaapi;
1510
+ this.currentWaapi = null;
1511
+ entry.stop();
1512
+ try {
1513
+ entry.anim.cancel();
1514
+ } catch {
1515
+ }
1516
+ }
1768
1517
  }
1769
1518
  };
1770
1519
 
@@ -2621,7 +2370,7 @@ function resolveEngineOptions(opts, extras) {
2621
2370
  const initialAllowed = opts.allowed ?? opts.snapPoints.map((p) => p.id);
2622
2371
  let initialId = opts.initial ?? initialAllowed[0] ?? opts.snapPoints[0]?.id ?? "default";
2623
2372
  if (opts.persistKey) {
2624
- const restored = readPersistedId(opts.persistKey);
2373
+ const restored = readPersistedIdSafe(opts.persistKey);
2625
2374
  if (restored && initialAllowed.includes(restored)) {
2626
2375
  initialId = restored;
2627
2376
  }
@@ -2652,6 +2401,7 @@ function resolveEngineOptions(opts, extras) {
2652
2401
  },
2653
2402
  animation: {
2654
2403
  animation: opts.animation,
2404
+ settleAnimation: opts.settleAnimation,
2655
2405
  duration: opts.duration,
2656
2406
  easing: opts.easing,
2657
2407
  spring: opts.spring,
@@ -2669,6 +2419,15 @@ function resolveEngineOptions(opts, extras) {
2669
2419
  }
2670
2420
  };
2671
2421
  }
2422
+ var readPersistedIdSafe = (key) => {
2423
+ if (typeof window === "undefined") return null;
2424
+ if (typeof localStorage === "undefined") return null;
2425
+ try {
2426
+ return localStorage.getItem(key);
2427
+ } catch {
2428
+ return null;
2429
+ }
2430
+ };
2672
2431
 
2673
2432
  // src/core/primitives/teardown-stack.ts
2674
2433
  var TeardownStack = class {
@@ -2691,9 +2450,9 @@ var TeardownStack = class {
2691
2450
  }
2692
2451
  };
2693
2452
 
2694
- // src/core/BottomSheetEngine.ts
2453
+ // src/core/BottomSheetCore.ts
2695
2454
  var HAPTIC_DURATION_MS = 8;
2696
- var BottomSheetEngine = class {
2455
+ var BottomSheetCore = class {
2697
2456
  constructor(opts) {
2698
2457
  __publicField(this, "id", nextInstanceId("bs"));
2699
2458
  __publicField(this, "element");
@@ -2754,6 +2513,8 @@ var BottomSheetEngine = class {
2754
2513
  __publicField(this, "stackEffectPrimed", false);
2755
2514
  __publicField(this, "fitDeps");
2756
2515
  __publicField(this, "transformTemplate");
2516
+ __publicField(this, "featureList", []);
2517
+ __publicField(this, "featureCtx");
2757
2518
  const resolved = resolveEngineOptions(opts, {
2758
2519
  shouldApplyInertSiblings: () => {
2759
2520
  if (typeof document === "undefined") return false;
@@ -2800,7 +2561,9 @@ var BottomSheetEngine = class {
2800
2561
  getRootEl: () => this.rootEl,
2801
2562
  applySize: (size) => this.applySize(size),
2802
2563
  getSize: () => this.size,
2803
- isDragging: () => this.gesture?.isDragging ?? false
2564
+ isDragging: () => this.gesture?.isDragging ?? false,
2565
+ applyAux: (size) => this.applySize(size, true),
2566
+ getTransformFor: (size) => this.transformTemplate(this.snaps.getMaxAxisSize() - size)
2804
2567
  },
2805
2568
  resolved.animation
2806
2569
  );
@@ -2823,6 +2586,11 @@ var BottomSheetEngine = class {
2823
2586
  if (opts.linkedSheets) {
2824
2587
  this.linkedSheets = opts.linkedSheets;
2825
2588
  }
2589
+ const byName = /* @__PURE__ */ new Map();
2590
+ for (const feature of opts.features ?? []) {
2591
+ byName.set(feature.name, feature);
2592
+ }
2593
+ this.featureList = Array.from(byName.values());
2826
2594
  this.activeId = resolved.initialId;
2827
2595
  auditVhUsage(this.snapPointsRaw);
2828
2596
  this.auditDuplicateSnapIds(this.snapPointsRaw);
@@ -2859,6 +2627,7 @@ var BottomSheetEngine = class {
2859
2627
  scrollContainer: this.scrollContainer,
2860
2628
  getMaxAxisSize: () => this.snaps.getMaxAxisSize()
2861
2629
  });
2630
+ this.featureCtx = this.buildFeatureContext(opts.autoCollapseAfter);
2862
2631
  this.attach();
2863
2632
  this.registerInStack();
2864
2633
  if (this.snaps.getMaxAxisSize() === 0 && typeof IntersectionObserver !== "undefined" && typeof document !== "undefined") {
@@ -2867,12 +2636,7 @@ var BottomSheetEngine = class {
2867
2636
  if (!entry.isIntersecting) continue;
2868
2637
  if (this.destroyed) return;
2869
2638
  this.snaps.recompute();
2870
- const current = this.snaps.findById(this.activeId);
2871
- if (current) {
2872
- this.size = current.size;
2873
- this.applySize(this.size);
2874
- this.updateAriaSlider();
2875
- }
2639
+ this.healActiveSnap();
2876
2640
  io.disconnect();
2877
2641
  return;
2878
2642
  }
@@ -2895,46 +2659,77 @@ var BottomSheetEngine = class {
2895
2659
  if (resolved.radius !== void 0) this.setRadius(resolved.radius);
2896
2660
  if (resolved.maxHeight !== void 0) this.setMaxHeight(resolved.maxHeight);
2897
2661
  this.applyAutoAriaLabelledBy();
2898
- if (this.closeOnRouteChange) {
2899
- this.teardowns.add(
2900
- installRouteChange({
2901
- isDestroyed: () => this.destroyed,
2902
- getSize: () => this.size,
2903
- close: () => this.close()
2904
- })
2905
- );
2906
- }
2907
- if (this.persistKey) {
2908
- this.teardowns.add(installPersist(this, this.persistKey));
2662
+ this.runFeatureStage("post");
2663
+ this.warnOrphanFeatureOptions();
2664
+ if (this.size > 0) {
2665
+ this.handleOpen();
2909
2666
  }
2910
- this.teardowns.add(
2911
- installAutoCollapse({
2912
- ms: opts.autoCollapseAfter,
2913
- isDestroyed: () => this.destroyed,
2914
- isDragging: () => this.gesture?.isDragging ?? false,
2915
- getAllowedIds: () => this.snaps.getAllowedIds().slice(),
2916
- getActiveId: () => this.activeId,
2917
- resolveSnap: (id) => this.snaps.findById(id),
2918
- snapTo: (id) => {
2919
- void this.snapTo(id);
2920
- },
2921
- on: (event, fn) => this.on(event, fn)
2922
- })
2923
- );
2924
- this.teardowns.add(
2925
- installRoute({
2667
+ }
2668
+ buildFeatureContext(autoCollapseAfter) {
2669
+ return {
2670
+ element: this.element,
2671
+ scrollContainer: this.scrollContainer,
2672
+ sheetId: this.id,
2673
+ options: {
2926
2674
  routedTo: this.routedTo,
2927
2675
  closeOnBack: this.closeOnBack,
2928
- isTopSheet: () => this.isTopSheet,
2929
- getSize: () => this.size,
2930
- isDestroyed: () => this.destroyed,
2931
- close: () => this.close("back"),
2932
- on: (event, fn) => this.on(event, fn),
2933
- sheetId: this.id
2934
- })
2935
- );
2936
- if (this.size > 0) {
2937
- this.handleOpen();
2676
+ closeOnRouteChange: this.closeOnRouteChange,
2677
+ persistKey: this.persistKey,
2678
+ autoCollapseAfter
2679
+ },
2680
+ isDestroyed: () => this.destroyed,
2681
+ isDragging: () => this.gesture?.isDragging ?? false,
2682
+ isAnimating: () => this.animation.isAnimating,
2683
+ isTopSheet: () => this.isTopSheet,
2684
+ isVerticalAxis: () => this.mode === "bottom" || this.mode === "top",
2685
+ getSize: () => this.size,
2686
+ getActiveId: () => this.activeId,
2687
+ getAllowedIds: () => this.snaps.getAllowedIds().slice(),
2688
+ allowedIdsBySize: () => this.allowedIdsBySize(),
2689
+ getMaxAxisSize: () => this.snaps.getMaxAxisSize(),
2690
+ resolveSnap: (id) => this.snaps.findById(id),
2691
+ resolveActiveSnap: () => this.snaps.findById(this.activeId),
2692
+ setSize: (size) => {
2693
+ this.size = size;
2694
+ },
2695
+ setMaxAxisSize: (size) => {
2696
+ this.snaps.setMaxAxisSize(size);
2697
+ },
2698
+ applySize: (size) => this.applySize(size),
2699
+ recomputeSnaps: () => this.recomputeSnaps(),
2700
+ newCycle: () => {
2701
+ this.newCycle();
2702
+ },
2703
+ cancelInFlight: () => {
2704
+ this.animation.cancel();
2705
+ },
2706
+ resyncAfterResize: () => this.resyncAfterResize(),
2707
+ snapTo: (id) => {
2708
+ void this.snapTo(id);
2709
+ },
2710
+ close: (reason) => this.close(reason),
2711
+ on: (event, fn) => this.on(event, fn),
2712
+ addTeardown: (fn) => this.teardowns.add(fn)
2713
+ };
2714
+ }
2715
+ runFeatureStage(stage) {
2716
+ for (const feature of this.featureList) {
2717
+ if ((feature.stage ?? "post") !== stage) continue;
2718
+ const teardown = feature.install(this.featureCtx);
2719
+ if (teardown) this.teardowns.add(teardown);
2720
+ }
2721
+ }
2722
+ warnOrphanFeatureOptions() {
2723
+ const names = new Set(this.featureList.map((f) => f.name));
2724
+ if ((this.closeOnBack || this.routedTo !== void 0 || this.closeOnRouteChange) && !names.has("route")) {
2725
+ devWarn(
2726
+ "[BottomSheet] closeOnBack/routedTo/closeOnRouteChange need routeFeature() \u2014 option ignored"
2727
+ );
2728
+ }
2729
+ if (this.persistKey && !names.has("persist")) {
2730
+ devWarn(
2731
+ "[BottomSheet] persistKey needs persistFeature() \u2014 snaps will not be saved"
2732
+ );
2938
2733
  }
2939
2734
  }
2940
2735
  get state() {
@@ -3036,6 +2831,8 @@ var BottomSheetEngine = class {
3036
2831
  const vt = document.startViewTransition(() => {
3037
2832
  this.applySize(target.size);
3038
2833
  });
2834
+ vt.ready?.catch(() => {
2835
+ });
3039
2836
  this.currentViewTransition = vt;
3040
2837
  try {
3041
2838
  await vt.finished;
@@ -3287,11 +3084,16 @@ var BottomSheetEngine = class {
3287
3084
  this.maxHeight.clampTo();
3288
3085
  this.scrim.invalidateOpacityCache();
3289
3086
  if (this.gesture?.isDragging) return;
3087
+ this.healActiveSnap();
3088
+ }
3089
+ healActiveSnap() {
3290
3090
  const current = this.snaps.findById(this.activeId);
3291
- if (current) {
3292
- this.size = current.size;
3293
- this.applySize(this.size);
3294
- this.updateAriaSlider();
3091
+ if (!current) return;
3092
+ this.size = current.size;
3093
+ this.applySize(this.size);
3094
+ this.updateAriaSlider();
3095
+ if (this.size > 0 && !this.lifecycle.isInstalled && !this.opening && !this.animation.isAnimating) {
3096
+ this.emitOpenSequence(this.activeId);
3295
3097
  }
3296
3098
  }
3297
3099
  destroy() {
@@ -3470,18 +3272,7 @@ var BottomSheetEngine = class {
3470
3272
  }
3471
3273
  }
3472
3274
  attachFeatures() {
3473
- if (this.scrollContainer) {
3474
- this.teardowns.add(installContentSwipe({
3475
- container: this.scrollContainer,
3476
- isDragging: () => this.gesture?.isDragging ?? false,
3477
- isAnimating: () => this.animation.isAnimating,
3478
- getAllowedIds: () => this.allowedIdsBySize(),
3479
- getActiveId: () => this.activeId,
3480
- snapTo: (id) => {
3481
- void this.snapTo(id);
3482
- }
3483
- }));
3484
- }
3275
+ this.runFeatureStage("attach");
3485
3276
  if (typeof window !== "undefined") {
3486
3277
  this.teardowns.add(installResizeObserver({
3487
3278
  element: this.element,
@@ -3521,31 +3312,6 @@ var BottomSheetEngine = class {
3521
3312
  () => document.removeEventListener("keydown", onKey)
3522
3313
  );
3523
3314
  }
3524
- this.teardowns.add(installVisualViewport({
3525
- element: this.element,
3526
- isVerticalAxis: () => this.mode === "bottom" || this.mode === "top",
3527
- isDestroyed: () => this.destroyed,
3528
- isDragging: () => this.gesture?.isDragging ?? false,
3529
- recomputeSnaps: () => this.recomputeSnaps(),
3530
- resolveActiveSnap: () => this.snaps.findById(this.activeId),
3531
- getMaxAxisSize: () => this.snaps.getMaxAxisSize(),
3532
- getSize: () => this.size,
3533
- setMaxAxisSize: (size) => {
3534
- this.snaps.setMaxAxisSize(size);
3535
- },
3536
- setSize: (size) => {
3537
- this.size = size;
3538
- },
3539
- applySize: (size) => this.applySize(size),
3540
- cancelInFlight: () => {
3541
- this.animation.cancel();
3542
- },
3543
- newCycle: () => {
3544
- this.newCycle();
3545
- },
3546
- isAnimating: () => this.animation.isAnimating,
3547
- resyncAfterCancel: () => this.resyncAfterResize()
3548
- }));
3549
3315
  }
3550
3316
  this.teardowns.add(installSliderKeyboard({
3551
3317
  handle: this.handle,
@@ -3622,13 +3388,13 @@ var BottomSheetEngine = class {
3622
3388
  }
3623
3389
  }
3624
3390
  }
3625
- applySize(size) {
3391
+ applySize(size, skipTransform = false) {
3626
3392
  const cap = this.snaps.getMaxAxisSize();
3627
3393
  const clamped = cap > 0 && size > cap && !this.gesture?.isDragging && !this.allowOvershoot ? cap : size;
3628
3394
  this.size = clamped;
3629
3395
  const offset = cap - clamped;
3630
3396
  const style = this.element.style;
3631
- style.transform = this.transformTemplate(offset);
3397
+ if (!skipTransform) style.transform = this.transformTemplate(offset);
3632
3398
  if (this.sizeWriteSentinel.shouldWrite(clamped, SIZE_WRITE_EPSILON)) {
3633
3399
  style.setProperty("--bs-size", `${clamped}px`);
3634
3400
  if (this.scrimParent) {
@@ -3706,6 +3472,514 @@ var BottomSheetEngine = class {
3706
3472
  }
3707
3473
  };
3708
3474
 
3475
+ // src/core/features/history-coordinator.ts
3476
+ var entries = [];
3477
+ var subscribers = /* @__PURE__ */ new Set();
3478
+ var suppress = 0;
3479
+ var internalDepth = 0;
3480
+ var pendingRebrands = [];
3481
+ var listenerInstalled = false;
3482
+ var patchInstalled = false;
3483
+ var origPushState = null;
3484
+ var origReplaceState = null;
3485
+ var patchedPushState = null;
3486
+ var patchedReplaceState = null;
3487
+ var hasWindow = () => typeof window !== "undefined";
3488
+ var hasHistory = () => typeof history !== "undefined";
3489
+ var brandMarker = (surface) => ({
3490
+ ...surface.markerState(),
3491
+ __bs: true
3492
+ });
3493
+ var runInternal = (fn) => {
3494
+ internalDepth += 1;
3495
+ try {
3496
+ fn();
3497
+ } finally {
3498
+ internalDepth -= 1;
3499
+ }
3500
+ };
3501
+ var notifyRouteChange = () => {
3502
+ const snapshot = Array.from(subscribers);
3503
+ for (const fn of snapshot) fn();
3504
+ };
3505
+ var ensureListener = () => {
3506
+ if (listenerInstalled || !hasWindow()) return;
3507
+ window.addEventListener("popstate", onPopState);
3508
+ listenerInstalled = true;
3509
+ };
3510
+ var maybeTeardownListener = () => {
3511
+ if (!listenerInstalled) return;
3512
+ if (entries.length > 0 || subscribers.size > 0 || suppress > 0) return;
3513
+ if (hasWindow()) window.removeEventListener("popstate", onPopState);
3514
+ listenerInstalled = false;
3515
+ };
3516
+ var installPatch = () => {
3517
+ if (patchInstalled || !hasHistory()) return;
3518
+ origPushState = history.pushState;
3519
+ origReplaceState = history.replaceState;
3520
+ patchedPushState = function(...args) {
3521
+ origPushState.apply(this, args);
3522
+ if (internalDepth === 0) notifyRouteChange();
3523
+ };
3524
+ patchedReplaceState = function(...args) {
3525
+ origReplaceState.apply(this, args);
3526
+ if (internalDepth === 0) notifyRouteChange();
3527
+ };
3528
+ history.pushState = patchedPushState;
3529
+ history.replaceState = patchedReplaceState;
3530
+ patchInstalled = true;
3531
+ };
3532
+ var removePatch = () => {
3533
+ if (!patchInstalled) return;
3534
+ if (history.pushState === patchedPushState && origPushState) {
3535
+ history.pushState = origPushState;
3536
+ }
3537
+ if (history.replaceState === patchedReplaceState && origReplaceState) {
3538
+ history.replaceState = origReplaceState;
3539
+ }
3540
+ origPushState = null;
3541
+ origReplaceState = null;
3542
+ patchedPushState = null;
3543
+ patchedReplaceState = null;
3544
+ patchInstalled = false;
3545
+ };
3546
+ function onPopState() {
3547
+ if (suppress > 0) {
3548
+ suppress -= 1;
3549
+ const rebrand = pendingRebrands.shift();
3550
+ if (rebrand) rebrand();
3551
+ maybeTeardownListener();
3552
+ return;
3553
+ }
3554
+ if (entries.length === 0) {
3555
+ notifyRouteChange();
3556
+ maybeTeardownListener();
3557
+ return;
3558
+ }
3559
+ let target = null;
3560
+ let targetIdx = -1;
3561
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
3562
+ const entry = entries[i];
3563
+ if (!entry.surface.isOpen()) continue;
3564
+ if (!target) {
3565
+ target = entry;
3566
+ targetIdx = i;
3567
+ }
3568
+ if (entry.surface.isTop()) {
3569
+ target = entry;
3570
+ targetIdx = i;
3571
+ break;
3572
+ }
3573
+ }
3574
+ if (!target) {
3575
+ const removed = entries.pop();
3576
+ if (removed) removed.live = false;
3577
+ maybeTeardownListener();
3578
+ return;
3579
+ }
3580
+ target.live = false;
3581
+ entries.splice(targetIdx, 1);
3582
+ const settled = target;
3583
+ void Promise.resolve(settled.surface.close()).then(() => {
3584
+ if (settled.surface.isOpen()) restore(settled);
3585
+ });
3586
+ maybeTeardownListener();
3587
+ }
3588
+ function restore(entry) {
3589
+ if (!hasWindow() || !hasHistory()) return;
3590
+ entry.live = true;
3591
+ runInternal(() => {
3592
+ try {
3593
+ history.pushState(brandMarker(entry.surface), "", entry.surface.url);
3594
+ } catch {
3595
+ entry.live = false;
3596
+ }
3597
+ });
3598
+ if (entry.live) {
3599
+ entries.push(entry);
3600
+ ensureListener();
3601
+ }
3602
+ }
3603
+ function pushBackMarker(surface) {
3604
+ const entry = { surface, priorUrl: void 0, live: false };
3605
+ if (!hasWindow() || !hasHistory()) {
3606
+ return entry;
3607
+ }
3608
+ entry.priorUrl = surface.url !== void 0 ? location.href : void 0;
3609
+ runInternal(() => {
3610
+ try {
3611
+ history.pushState(brandMarker(surface), "", surface.url);
3612
+ entry.live = true;
3613
+ } catch {
3614
+ entry.live = false;
3615
+ }
3616
+ });
3617
+ if (entry.live) {
3618
+ entries.push(entry);
3619
+ ensureListener();
3620
+ }
3621
+ return entry;
3622
+ }
3623
+ function popBackMarker(handle) {
3624
+ if (!hasWindow() || !hasHistory()) return;
3625
+ const entry = handle;
3626
+ const idx = entries.indexOf(entry);
3627
+ if (idx === -1 || !entry.live) return;
3628
+ const last = entries.length - 1;
3629
+ entry.live = false;
3630
+ entries.splice(idx, 1);
3631
+ let rebrand = null;
3632
+ if (idx !== last) {
3633
+ const rebrandSurface = entries[entries.length - 1].surface;
3634
+ const rebrandUrl = entry.priorUrl;
3635
+ rebrand = () => {
3636
+ runInternal(() => {
3637
+ history.replaceState(
3638
+ brandMarker(rebrandSurface),
3639
+ "",
3640
+ rebrandUrl !== void 0 ? rebrandUrl : location.href
3641
+ );
3642
+ });
3643
+ };
3644
+ }
3645
+ pendingRebrands.push(rebrand);
3646
+ suppress += 1;
3647
+ runInternal(() => {
3648
+ try {
3649
+ history.back();
3650
+ } catch {
3651
+ suppress = Math.max(0, suppress - 1);
3652
+ pendingRebrands.pop();
3653
+ }
3654
+ });
3655
+ maybeTeardownListener();
3656
+ }
3657
+ function subscribeRouteChange(fn) {
3658
+ if (!hasWindow() || !hasHistory()) return () => {
3659
+ };
3660
+ subscribers.add(fn);
3661
+ if (subscribers.size === 1) installPatch();
3662
+ ensureListener();
3663
+ return () => {
3664
+ if (!subscribers.has(fn)) return;
3665
+ subscribers.delete(fn);
3666
+ if (subscribers.size === 0) removePatch();
3667
+ maybeTeardownListener();
3668
+ };
3669
+ }
3670
+
3671
+ // src/core/features/route.ts
3672
+ function installRoute(deps) {
3673
+ if (typeof window === "undefined") {
3674
+ return () => {
3675
+ };
3676
+ }
3677
+ if (!deps.routedTo && !deps.closeOnBack) {
3678
+ return () => {
3679
+ };
3680
+ }
3681
+ let handle = null;
3682
+ const onOpen = () => {
3683
+ if (handle) return;
3684
+ handle = pushBackMarker({
3685
+ isOpen: () => !deps.isDestroyed() && deps.getSize() > 0,
3686
+ isTop: deps.isTopSheet,
3687
+ close: deps.close,
3688
+ markerState: () => deps.routedTo !== void 0 ? { __bsRouted: deps.sheetId } : { __bsSheet: deps.sheetId },
3689
+ url: deps.routedTo
3690
+ });
3691
+ };
3692
+ const onClose = () => {
3693
+ if (!handle) return;
3694
+ popBackMarker(handle);
3695
+ handle = null;
3696
+ };
3697
+ const unsubscribeOpen = deps.on("open", onOpen);
3698
+ const unsubscribeClose = deps.on("close", onClose);
3699
+ if (deps.getSize() > 0) onOpen();
3700
+ return () => {
3701
+ unsubscribeOpen();
3702
+ unsubscribeClose();
3703
+ if (handle) {
3704
+ popBackMarker(handle);
3705
+ handle = null;
3706
+ }
3707
+ };
3708
+ }
3709
+ function installRouteChange(deps) {
3710
+ return subscribeRouteChange(() => {
3711
+ if (deps.isDestroyed() || deps.getSize() <= 0) return;
3712
+ void deps.close();
3713
+ });
3714
+ }
3715
+
3716
+ // src/core/features/persist.ts
3717
+ function installPersist(engine, key) {
3718
+ const unsubscribe = engine.on("snap", ({ id }) => {
3719
+ if (typeof window === "undefined") return;
3720
+ if (typeof localStorage === "undefined") return;
3721
+ try {
3722
+ localStorage.setItem(key, id);
3723
+ } catch {
3724
+ }
3725
+ });
3726
+ return unsubscribe;
3727
+ }
3728
+
3729
+ // src/core/features/auto-collapse.ts
3730
+ function installAutoCollapse(deps) {
3731
+ const { ms } = deps;
3732
+ if (ms === void 0 || ms <= 0) {
3733
+ return () => {
3734
+ };
3735
+ }
3736
+ let timer = null;
3737
+ const clearTimer = () => {
3738
+ if (timer !== null) {
3739
+ clearTimeout(timer);
3740
+ timer = null;
3741
+ }
3742
+ };
3743
+ const fire = () => {
3744
+ if (deps.isDestroyed()) return;
3745
+ if (deps.isDragging()) return;
3746
+ const active = deps.resolveSnap(deps.getActiveId());
3747
+ if (!active || active.size === 0) return;
3748
+ let target = null;
3749
+ for (const id of deps.getAllowedIds()) {
3750
+ const snap = deps.resolveSnap(id);
3751
+ if (!snap || snap.size <= 0) continue;
3752
+ if (!target || snap.size < target.size) target = snap;
3753
+ }
3754
+ if (!target) return;
3755
+ if (target.id === active.id || target.size >= active.size) return;
3756
+ deps.snapTo(target.id);
3757
+ };
3758
+ const reset = () => {
3759
+ clearTimer();
3760
+ if (deps.isDestroyed()) return;
3761
+ timer = setTimeout(() => {
3762
+ timer = null;
3763
+ fire();
3764
+ }, ms);
3765
+ };
3766
+ const unsubscribe = deps.on("snap", () => reset());
3767
+ if ((deps.resolveSnap(deps.getActiveId())?.size ?? 0) > 0) reset();
3768
+ return () => {
3769
+ clearTimer();
3770
+ unsubscribe();
3771
+ };
3772
+ }
3773
+
3774
+ // src/core/features/content-swipe.ts
3775
+ var CONTENT_PULL_THRESHOLD = 60;
3776
+ function installContentSwipe(deps) {
3777
+ const { container } = deps;
3778
+ let startY = 0;
3779
+ let startScrollTop = 0;
3780
+ let moved = false;
3781
+ const onTouchStart = (e) => {
3782
+ const t = e.touches[0];
3783
+ if (!t) return;
3784
+ startY = t.clientY;
3785
+ startScrollTop = container.scrollTop;
3786
+ moved = false;
3787
+ };
3788
+ const onTouchMove = (e) => {
3789
+ const t = e.touches[0];
3790
+ if (!t) return;
3791
+ if (Math.abs(t.clientY - startY) > CONTENT_PULL_THRESHOLD) moved = true;
3792
+ };
3793
+ const onTouchEnd = (e) => {
3794
+ if (!moved) return;
3795
+ if (deps.isDragging() || deps.isAnimating()) return;
3796
+ if (container.scrollTop !== startScrollTop) return;
3797
+ const t = e.changedTouches[0];
3798
+ if (!t) return;
3799
+ const deltaY = t.clientY - startY;
3800
+ const atTop = container.scrollTop <= 0;
3801
+ const allowed = deps.getAllowedIds();
3802
+ const idx = allowed.indexOf(deps.getActiveId());
3803
+ if (idx === -1) return;
3804
+ if (atTop && deltaY > CONTENT_PULL_THRESHOLD && idx > 0) {
3805
+ deps.snapTo(allowed[idx - 1]);
3806
+ } else if (deltaY < -CONTENT_PULL_THRESHOLD && idx < allowed.length - 1) {
3807
+ deps.snapTo(allowed[idx + 1]);
3808
+ }
3809
+ };
3810
+ container.addEventListener("touchstart", onTouchStart, { passive: true });
3811
+ container.addEventListener("touchmove", onTouchMove, { passive: true });
3812
+ container.addEventListener("touchend", onTouchEnd, { passive: true });
3813
+ return () => {
3814
+ container.removeEventListener("touchstart", onTouchStart);
3815
+ container.removeEventListener("touchmove", onTouchMove);
3816
+ container.removeEventListener("touchend", onTouchEnd);
3817
+ };
3818
+ }
3819
+
3820
+ // src/core/features/visual-viewport.ts
3821
+ var SAFETY_PAD = 8;
3822
+ function installVisualViewport(deps) {
3823
+ if (typeof window === "undefined" || !window.visualViewport) {
3824
+ return () => {
3825
+ };
3826
+ }
3827
+ const vv = window.visualViewport;
3828
+ let rafId = null;
3829
+ const onResize = () => {
3830
+ if (rafId !== null) return;
3831
+ rafId = requestAnimationFrame(() => {
3832
+ rafId = null;
3833
+ if (deps.isDestroyed()) return;
3834
+ deps.recomputeSnaps();
3835
+ const isVertical = deps.isVerticalAxis();
3836
+ const visible = isVertical ? vv.height : vv.width;
3837
+ const windowSize = isVertical ? window.innerHeight : window.innerWidth;
3838
+ const clamp = Math.max(0, Math.min(windowSize, visible) - SAFETY_PAD);
3839
+ if (clamp < deps.getMaxAxisSize()) {
3840
+ deps.setMaxAxisSize(clamp);
3841
+ }
3842
+ const wasAnimating = deps.isAnimating?.() ?? false;
3843
+ deps.cancelInFlight();
3844
+ deps.newCycle();
3845
+ const max = deps.getMaxAxisSize();
3846
+ deps.setSize(Math.min(deps.getSize(), max));
3847
+ const current = deps.resolveActiveSnap();
3848
+ if (current && !deps.isDragging()) {
3849
+ deps.applySize(Math.min(current.size, max));
3850
+ if (wasAnimating) deps.resyncAfterCancel?.();
3851
+ } else {
3852
+ deps.applySize(deps.getSize());
3853
+ }
3854
+ });
3855
+ };
3856
+ vv.addEventListener("resize", onResize);
3857
+ return () => {
3858
+ vv.removeEventListener("resize", onResize);
3859
+ if (rafId !== null) cancelAnimationFrame(rafId);
3860
+ };
3861
+ }
3862
+
3863
+ // src/core/features/engine-features.ts
3864
+ function routeFeature() {
3865
+ return {
3866
+ name: "route",
3867
+ install: (ctx) => {
3868
+ const teardowns = new TeardownStack();
3869
+ if (ctx.options.closeOnRouteChange) {
3870
+ teardowns.add(
3871
+ installRouteChange({
3872
+ isDestroyed: ctx.isDestroyed,
3873
+ getSize: ctx.getSize,
3874
+ close: () => ctx.close()
3875
+ })
3876
+ );
3877
+ }
3878
+ teardowns.add(
3879
+ installRoute({
3880
+ routedTo: ctx.options.routedTo,
3881
+ closeOnBack: ctx.options.closeOnBack,
3882
+ isTopSheet: ctx.isTopSheet,
3883
+ getSize: ctx.getSize,
3884
+ isDestroyed: ctx.isDestroyed,
3885
+ close: () => ctx.close("back"),
3886
+ on: ctx.on,
3887
+ sheetId: ctx.sheetId
3888
+ })
3889
+ );
3890
+ return () => teardowns.drain();
3891
+ }
3892
+ };
3893
+ }
3894
+ function persistFeature() {
3895
+ return {
3896
+ name: "persist",
3897
+ install: (ctx) => {
3898
+ const key = ctx.options.persistKey;
3899
+ if (!key) return;
3900
+ return installPersist({ on: ctx.on }, key);
3901
+ }
3902
+ };
3903
+ }
3904
+ function autoCollapseFeature() {
3905
+ return {
3906
+ name: "auto-collapse",
3907
+ install: (ctx) => installAutoCollapse({
3908
+ ms: ctx.options.autoCollapseAfter,
3909
+ isDestroyed: ctx.isDestroyed,
3910
+ isDragging: ctx.isDragging,
3911
+ getAllowedIds: ctx.getAllowedIds,
3912
+ getActiveId: ctx.getActiveId,
3913
+ resolveSnap: ctx.resolveSnap,
3914
+ snapTo: ctx.snapTo,
3915
+ on: ctx.on
3916
+ })
3917
+ };
3918
+ }
3919
+ function contentSwipeFeature() {
3920
+ return {
3921
+ name: "content-swipe",
3922
+ stage: "attach",
3923
+ install: (ctx) => {
3924
+ if (!ctx.scrollContainer) return;
3925
+ return installContentSwipe({
3926
+ container: ctx.scrollContainer,
3927
+ isDragging: ctx.isDragging,
3928
+ isAnimating: ctx.isAnimating,
3929
+ getAllowedIds: ctx.allowedIdsBySize,
3930
+ getActiveId: ctx.getActiveId,
3931
+ snapTo: ctx.snapTo
3932
+ });
3933
+ }
3934
+ };
3935
+ }
3936
+ function visualViewportFeature() {
3937
+ return {
3938
+ name: "visual-viewport",
3939
+ install: (ctx) => {
3940
+ if (typeof window === "undefined") return;
3941
+ return installVisualViewport({
3942
+ element: ctx.element,
3943
+ isVerticalAxis: ctx.isVerticalAxis,
3944
+ isDestroyed: ctx.isDestroyed,
3945
+ isDragging: ctx.isDragging,
3946
+ recomputeSnaps: ctx.recomputeSnaps,
3947
+ resolveActiveSnap: ctx.resolveActiveSnap,
3948
+ getMaxAxisSize: ctx.getMaxAxisSize,
3949
+ getSize: ctx.getSize,
3950
+ setMaxAxisSize: ctx.setMaxAxisSize,
3951
+ setSize: ctx.setSize,
3952
+ applySize: ctx.applySize,
3953
+ cancelInFlight: ctx.cancelInFlight,
3954
+ newCycle: ctx.newCycle,
3955
+ isAnimating: ctx.isAnimating,
3956
+ resyncAfterCancel: ctx.resyncAfterResize
3957
+ });
3958
+ }
3959
+ };
3960
+ }
3961
+
3962
+ // src/core/default-features.ts
3963
+ function defaultEngineFeatures() {
3964
+ return [
3965
+ contentSwipeFeature(),
3966
+ visualViewportFeature(),
3967
+ persistFeature(),
3968
+ autoCollapseFeature(),
3969
+ routeFeature()
3970
+ ];
3971
+ }
3972
+
3973
+ // src/core/BottomSheetEngine.ts
3974
+ var BottomSheetEngine = class extends BottomSheetCore {
3975
+ constructor(opts) {
3976
+ super({
3977
+ ...opts,
3978
+ features: [...defaultEngineFeatures(), ...opts.features ?? []]
3979
+ });
3980
+ }
3981
+ };
3982
+
3709
3983
  // src/core/features/teleport.ts
3710
3984
  var resolveTeleportTarget = (target) => {
3711
3985
  if (!target) return null;