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