@surdeddd/bottom-sheet 0.8.2 → 0.9.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 (46) hide show
  1. package/dist/{BottomSheetEngine-7BDK2OWX.d.cts → BottomSheetEngine-BcyruNP1.d.cts} +12 -6
  2. package/dist/{BottomSheetEngine-BFTb0-4Z.d.ts → BottomSheetEngine-CBRcBwV-.d.ts} +12 -6
  3. package/dist/element.cjs +530 -349
  4. package/dist/element.d.cts +2 -1
  5. package/dist/element.d.ts +2 -1
  6. package/dist/element.iife.global.js +2 -2
  7. package/dist/element.js +530 -349
  8. package/dist/index.cjs +438 -294
  9. package/dist/index.d.cts +3 -3
  10. package/dist/index.d.ts +3 -3
  11. package/dist/index.js +438 -294
  12. package/dist/integrations/formik.d.cts +1 -1
  13. package/dist/integrations/formik.d.ts +1 -1
  14. package/dist/integrations/react-hook-form.d.cts +1 -1
  15. package/dist/integrations/react-hook-form.d.ts +1 -1
  16. package/dist/overlay.cjs +160 -96
  17. package/dist/overlay.d.cts +2 -0
  18. package/dist/overlay.d.ts +2 -0
  19. package/dist/overlay.js +160 -96
  20. package/dist/preact.d.cts +1 -1
  21. package/dist/preact.d.ts +1 -1
  22. package/dist/qwik.cjs +387 -211
  23. package/dist/qwik.d.cts +4 -3
  24. package/dist/qwik.d.ts +4 -3
  25. package/dist/qwik.js +387 -211
  26. package/dist/react.cjs +472 -296
  27. package/dist/react.d.cts +3 -2
  28. package/dist/react.d.ts +3 -2
  29. package/dist/react.js +472 -296
  30. package/dist/solid.cjs +417 -215
  31. package/dist/solid.d.cts +21 -7
  32. package/dist/solid.d.ts +21 -7
  33. package/dist/solid.js +417 -215
  34. package/dist/solid.source.jsx +412 -209
  35. package/dist/svelte-core.cjs +328 -209
  36. package/dist/svelte-core.js +328 -209
  37. package/dist/svelte-src/BottomSheet.svelte +56 -0
  38. package/dist/svelte.cjs +365 -210
  39. package/dist/svelte.d.cts +4 -0
  40. package/dist/svelte.d.ts +4 -0
  41. package/dist/svelte.js +365 -210
  42. package/dist/vue.cjs +358 -211
  43. package/dist/vue.d.cts +6 -0
  44. package/dist/vue.d.ts +6 -0
  45. package/dist/vue.js +358 -211
  46. package/package.json +6 -6
package/dist/react.cjs CHANGED
@@ -274,18 +274,25 @@ var SnapResolver = class {
274
274
  __publicField(this, "resolved", []);
275
275
  __publicField(this, "maxAxisSize", 0);
276
276
  __publicField(this, "rangeCache", null);
277
+ __publicField(this, "notified", false);
277
278
  this.raw = raw;
278
279
  this.allowedIds = allowed ?? raw.map((p) => p.id);
279
280
  this.recompute();
280
281
  }
281
282
  recompute() {
282
- this.resolved = resolveSnapList(this.raw, this.mode, this.measureFit);
283
- this.maxAxisSize = this.resolved.reduce(
283
+ const measure = this.measureFit;
284
+ let measured;
285
+ const measureOnce = measure ? () => measured ?? (measured = measure()) : void 0;
286
+ this.resolved = resolveSnapList(this.raw, this.mode, measureOnce);
287
+ const nextMax = this.resolved.reduce(
284
288
  (m, s) => s.size > m ? s.size : m,
285
289
  0
286
290
  );
287
291
  this.rangeCache = null;
288
- this.onMaxAxisSizeChange?.(this.maxAxisSize);
292
+ const changed = nextMax !== this.maxAxisSize || !this.notified;
293
+ this.maxAxisSize = nextMax;
294
+ this.notified = true;
295
+ if (changed) this.onMaxAxisSizeChange?.(this.maxAxisSize);
289
296
  }
290
297
  setRaw(raw) {
291
298
  this.raw = raw;
@@ -438,7 +445,10 @@ var ensureHandler = () => {
438
445
  target.consumed = true;
439
446
  const idx = closers.indexOf(target);
440
447
  if (idx !== -1) closers.splice(idx, 1);
441
- target.close();
448
+ const settled = target;
449
+ void Promise.resolve(settled.close()).then(() => {
450
+ if (settled.isOpen()) settled.restore();
451
+ });
442
452
  });
443
453
  };
444
454
  var programmaticBack = () => {
@@ -474,16 +484,7 @@ function installRoute(deps) {
474
484
  pushed = false;
475
485
  }
476
486
  };
477
- const onOpen = () => {
478
- if (closer) return;
479
- closer = {
480
- isOpen: () => !deps.isDestroyed() && deps.getSize() > 0,
481
- isTop: () => deps.isTopSheet(),
482
- close: () => void deps.close(),
483
- consumed: false
484
- };
485
- ensureHandler();
486
- closers.push(closer);
487
+ const pushEntry = () => {
487
488
  try {
488
489
  if (deps.routedTo !== void 0) {
489
490
  history.pushState({ __bsRouted: deps.sheetId }, "", deps.routedTo);
@@ -495,6 +496,25 @@ function installRoute(deps) {
495
496
  pushed = false;
496
497
  }
497
498
  };
499
+ const onOpen = () => {
500
+ if (closer) return;
501
+ const self = {
502
+ isOpen: () => !deps.isDestroyed() && deps.getSize() > 0,
503
+ isTop: () => deps.isTopSheet(),
504
+ close: () => deps.close(),
505
+ restore: () => {
506
+ if (deps.isDestroyed() || closer !== self) return;
507
+ self.consumed = false;
508
+ if (!closers.includes(self)) closers.push(self);
509
+ pushEntry();
510
+ },
511
+ consumed: false
512
+ };
513
+ closer = self;
514
+ ensureHandler();
515
+ closers.push(self);
516
+ pushEntry();
517
+ };
498
518
  const unsubscribeOpen = deps.on("open", onOpen);
499
519
  const unsubscribeClose = deps.on("close", teardownEntry);
500
520
  if (deps.getSize() > 0) onOpen();
@@ -541,11 +561,13 @@ var CONTENT_PULL_THRESHOLD = 60;
541
561
  function installContentSwipe(deps) {
542
562
  const { container } = deps;
543
563
  let startY = 0;
564
+ let startScrollTop = 0;
544
565
  let moved = false;
545
566
  const onTouchStart = (e) => {
546
567
  const t = e.touches[0];
547
568
  if (!t) return;
548
569
  startY = t.clientY;
570
+ startScrollTop = container.scrollTop;
549
571
  moved = false;
550
572
  };
551
573
  const onTouchMove = (e) => {
@@ -556,6 +578,7 @@ function installContentSwipe(deps) {
556
578
  const onTouchEnd = (e) => {
557
579
  if (!moved) return;
558
580
  if (deps.isDragging() || deps.isAnimating()) return;
581
+ if (container.scrollTop !== startScrollTop) return;
559
582
  const t = e.changedTouches[0];
560
583
  if (!t) return;
561
584
  const deltaY = t.clientY - startY;
@@ -667,6 +690,131 @@ function installResizeObserver(deps) {
667
690
  };
668
691
  }
669
692
 
693
+ // src/core/features/fit-observer.ts
694
+ var contentTargets = (scroller) => {
695
+ if (!scroller) return [];
696
+ const hasSlot = typeof HTMLSlotElement !== "undefined";
697
+ const out = [];
698
+ for (const child of Array.from(scroller.children)) {
699
+ if (hasSlot && child instanceof HTMLSlotElement) {
700
+ for (const el of child.assignedElements()) out.push(el);
701
+ } else {
702
+ out.push(child);
703
+ }
704
+ }
705
+ return out;
706
+ };
707
+ function installFitObserver(deps) {
708
+ if (typeof ResizeObserver === "undefined") return () => {
709
+ };
710
+ if (!deps.hasFitSnap()) return () => {
711
+ };
712
+ let raf = 0;
713
+ const schedule = () => {
714
+ if (raf) return;
715
+ raf = requestAnimationFrame(() => {
716
+ raf = 0;
717
+ if (deps.isDestroyed() || deps.isDragging()) return;
718
+ deps.recompute();
719
+ });
720
+ };
721
+ const ro = new ResizeObserver(schedule);
722
+ ro.observe(deps.handle);
723
+ const scroller = deps.scrollContainer;
724
+ const observed = /* @__PURE__ */ new Set();
725
+ const slots = /* @__PURE__ */ new Set();
726
+ const hasSlot = typeof HTMLSlotElement !== "undefined";
727
+ const onSlotChange = () => resync();
728
+ const trackSlots = () => {
729
+ if (!scroller || !hasSlot) return;
730
+ for (const child of Array.from(scroller.children)) {
731
+ if (child instanceof HTMLSlotElement && !slots.has(child)) {
732
+ child.addEventListener("slotchange", onSlotChange);
733
+ slots.add(child);
734
+ }
735
+ }
736
+ };
737
+ const resync = () => {
738
+ trackSlots();
739
+ const targets = contentTargets(scroller);
740
+ for (const el of observed) {
741
+ if (!targets.includes(el)) {
742
+ ro.unobserve(el);
743
+ observed.delete(el);
744
+ }
745
+ }
746
+ for (const el of targets) {
747
+ if (!observed.has(el)) {
748
+ ro.observe(el);
749
+ observed.add(el);
750
+ }
751
+ }
752
+ schedule();
753
+ };
754
+ let mo;
755
+ if (scroller) {
756
+ resync();
757
+ if (typeof MutationObserver !== "undefined") {
758
+ mo = new MutationObserver(resync);
759
+ mo.observe(scroller, { childList: true });
760
+ }
761
+ }
762
+ return () => {
763
+ ro.disconnect();
764
+ mo?.disconnect();
765
+ for (const slot of slots) {
766
+ slot.removeEventListener("slotchange", onSlotChange);
767
+ }
768
+ if (raf) cancelAnimationFrame(raf);
769
+ };
770
+ }
771
+
772
+ // src/core/features/fit-measurement.ts
773
+ var measureSheetNatural = (element, content, vertical) => {
774
+ if (!content) {
775
+ return vertical ? element.offsetHeight : element.offsetWidth;
776
+ }
777
+ const axis = vertical ? "height" : "width";
778
+ const sheetStyle = element.style;
779
+ const cs = content.style;
780
+ const prevSheet = sheetStyle[axis];
781
+ const prevFlex = cs.flex;
782
+ const prevContent = vertical ? cs.height : cs.width;
783
+ sheetStyle[axis] = "auto";
784
+ cs.flex = "none";
785
+ if (vertical) cs.height = "auto";
786
+ else cs.width = "auto";
787
+ const poked = vertical ? element.offsetHeight : element.offsetWidth;
788
+ const contentBox = vertical ? content.clientHeight : content.clientWidth;
789
+ const contentScroll = vertical ? content.scrollHeight : content.scrollWidth;
790
+ const natural = Math.max(poked, poked - contentBox + contentScroll);
791
+ sheetStyle[axis] = prevSheet;
792
+ cs.flex = prevFlex;
793
+ if (vertical) cs.height = prevContent;
794
+ else cs.width = prevContent;
795
+ return natural;
796
+ };
797
+ var containingExtent = (element, vertical) => {
798
+ const op = element.offsetParent;
799
+ if (op && op !== document.body && op !== document.documentElement) {
800
+ return vertical ? op.clientHeight : op.clientWidth;
801
+ }
802
+ return vertical ? window.innerHeight : window.innerWidth;
803
+ };
804
+ var measureFitSize = (deps) => {
805
+ const vertical = layoutAxis(deps.mode) === "height";
806
+ const natural = measureSheetNatural(
807
+ deps.element,
808
+ deps.scrollContainer,
809
+ vertical
810
+ );
811
+ const cap = deps.getMaxHeightCap();
812
+ const capped = cap !== void 0 ? Math.min(natural, cap) : natural;
813
+ if (typeof window === "undefined") return capped;
814
+ const viewport = containingExtent(deps.element, vertical);
815
+ return viewport > 0 ? Math.min(capped, viewport) : capped;
816
+ };
817
+
670
818
  // src/core/features/slider-keyboard.ts
671
819
  function installSliderKeyboard(deps) {
672
820
  const isVerticalAxis = deps.mode === "bottom" || deps.mode === "top";
@@ -1153,6 +1301,9 @@ var ScrimController = class {
1153
1301
  const s = this.screenComponent.style;
1154
1302
  s.opacity = "";
1155
1303
  s.display = "";
1304
+ s.position = "";
1305
+ s.inset = "";
1306
+ s.pointerEvents = "";
1156
1307
  }
1157
1308
  if (this.backdrop) {
1158
1309
  this.backdrop.style.opacity = "";
@@ -1401,7 +1552,12 @@ var AnimationRunner = class {
1401
1552
  this.currentSpring?.cancel();
1402
1553
  }
1403
1554
  async animateTo(target, velocityPxPerMs) {
1404
- if (target === this.getSize()) return;
1555
+ if (target === this.getSize()) {
1556
+ if (!this.isAnimating && !this.isDragging()) {
1557
+ this.element.style.willChange = "";
1558
+ }
1559
+ return;
1560
+ }
1405
1561
  this.currentTween?.cancel();
1406
1562
  this.currentSpring?.cancel();
1407
1563
  this.getRootEl()?.setAttribute("data-animating", "true");
@@ -1409,7 +1565,7 @@ var AnimationRunner = class {
1409
1565
  if (this.reducedMotion) {
1410
1566
  this.applySizeFn(target);
1411
1567
  this.getRootEl()?.removeAttribute("data-animating");
1412
- if (!this.isDragging()) this.element.style.willChange = "auto";
1568
+ if (!this.isDragging()) this.element.style.willChange = "";
1413
1569
  return;
1414
1570
  }
1415
1571
  if (this.animationKind === "spring") {
@@ -1437,7 +1593,7 @@ var AnimationRunner = class {
1437
1593
  }
1438
1594
  if (!this.isAnimating) {
1439
1595
  this.getRootEl()?.removeAttribute("data-animating");
1440
- if (!this.isDragging()) this.element.style.willChange = "auto";
1596
+ if (!this.isDragging()) this.element.style.willChange = "";
1441
1597
  }
1442
1598
  }
1443
1599
  destroy() {
@@ -1468,9 +1624,35 @@ var isVisible = (el) => {
1468
1624
  }
1469
1625
  return el.offsetParent !== null;
1470
1626
  };
1471
- var focusables = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter(
1627
+ var collectFocusables = (root) => {
1628
+ const out = Array.from(root.querySelectorAll(FOCUSABLE));
1629
+ if (typeof HTMLSlotElement !== "undefined") {
1630
+ for (const slot of Array.from(root.querySelectorAll("slot"))) {
1631
+ for (const assigned of slot.assignedElements()) {
1632
+ if (assigned.matches(FOCUSABLE)) out.push(assigned);
1633
+ out.push(...collectFocusables(assigned));
1634
+ }
1635
+ }
1636
+ }
1637
+ return out;
1638
+ };
1639
+ var focusables = (root) => collectFocusables(root).filter(
1472
1640
  (el) => !el.hasAttribute("inert") && isVisible(el) && !el.matches("[aria-hidden='true']")
1473
1641
  );
1642
+ var containsComposed = (container, node) => {
1643
+ let n = node;
1644
+ while (n) {
1645
+ if (n === container) return true;
1646
+ const slot = n.assignedSlot;
1647
+ if (slot) {
1648
+ n = slot;
1649
+ continue;
1650
+ }
1651
+ const parent = n.parentNode;
1652
+ n = parent instanceof ShadowRoot ? parent.host : parent;
1653
+ }
1654
+ return false;
1655
+ };
1474
1656
  var trapStack = [];
1475
1657
  var installFocusTrap = (container, options = {}) => {
1476
1658
  if (typeof document === "undefined") return () => {
@@ -1480,7 +1662,13 @@ var installFocusTrap = (container, options = {}) => {
1480
1662
  const isActive = () => trapStack[trapStack.length - 1] === token;
1481
1663
  const previouslyFocused = document.activeElement;
1482
1664
  const focusInitial = () => {
1483
- const initial = typeof options.initialFocus === "string" ? container.querySelector(options.initialFocus) : options.initialFocus ?? focusables(container)[0] ?? container;
1665
+ const spec = options.initialFocus;
1666
+ if (spec === false) {
1667
+ if (!container.hasAttribute("tabindex")) container.tabIndex = -1;
1668
+ container.focus({ preventScroll: true });
1669
+ return;
1670
+ }
1671
+ const initial = typeof spec === "string" ? container.querySelector(spec) : spec ?? focusables(container)[0] ?? container;
1484
1672
  initial?.focus({ preventScroll: true });
1485
1673
  };
1486
1674
  focusInitial();
@@ -1510,7 +1698,7 @@ var installFocusTrap = (container, options = {}) => {
1510
1698
  const handleFocusIn = (e) => {
1511
1699
  if (!isActive()) return;
1512
1700
  const target = e.target;
1513
- if (target && !container.contains(target)) {
1701
+ if (target && !containsComposed(container, target)) {
1514
1702
  const list = focusables(container);
1515
1703
  list[0]?.focus({ preventScroll: true });
1516
1704
  }
@@ -1547,8 +1735,14 @@ var lockBodyScroll = () => {
1547
1735
  position: body.style.position,
1548
1736
  top: body.style.top,
1549
1737
  width: body.style.width,
1738
+ paddingRight: body.style.paddingRight,
1550
1739
  scrollY: window.scrollY
1551
1740
  };
1741
+ const scrollbarGap = window.innerWidth - document.documentElement.clientWidth;
1742
+ if (scrollbarGap > 0) {
1743
+ const current = parseFloat(getComputedStyle(body).paddingRight) || 0;
1744
+ body.style.paddingRight = `${current + scrollbarGap}px`;
1745
+ }
1552
1746
  body.style.overflow = "hidden";
1553
1747
  body.style.position = "fixed";
1554
1748
  body.style.top = `-${savedStyles.scrollY}px`;
@@ -1566,6 +1760,7 @@ var lockBodyScroll = () => {
1566
1760
  body.style.position = savedStyles.position;
1567
1761
  body.style.top = savedStyles.top;
1568
1762
  body.style.width = savedStyles.width;
1763
+ body.style.paddingRight = savedStyles.paddingRight;
1569
1764
  window.scrollTo(0, savedStyles.scrollY);
1570
1765
  savedStyles = null;
1571
1766
  }
@@ -1778,12 +1973,14 @@ var installGestures = (handle, mode, callbacks) => {
1778
1973
  handle.addEventListener("pointermove", onPointerMove);
1779
1974
  handle.addEventListener("pointerup", finishGesture);
1780
1975
  handle.addEventListener("pointercancel", onPointerCancel);
1976
+ const prevTouchAction = handle.style.touchAction;
1781
1977
  handle.style.touchAction = isAxisVertical(mode) ? "pan-x" : "pan-y";
1782
1978
  return () => {
1783
1979
  handle.removeEventListener("pointerdown", onPointerDown);
1784
1980
  handle.removeEventListener("pointermove", onPointerMove);
1785
1981
  handle.removeEventListener("pointerup", finishGesture);
1786
1982
  handle.removeEventListener("pointercancel", onPointerCancel);
1983
+ handle.style.touchAction = prevTouchAction;
1787
1984
  if (activePointerId !== null && handle.hasPointerCapture(activePointerId)) {
1788
1985
  handle.releasePointerCapture(activePointerId);
1789
1986
  }
@@ -1910,7 +2107,7 @@ var GestureController = class {
1910
2107
  this.isDragging_ = false;
1911
2108
  this.keyboardDismissed = false;
1912
2109
  this.deps.getRoot()?.removeAttribute("data-dragging");
1913
- this.deps.element.style.willChange = "auto";
2110
+ this.deps.element.style.willChange = "";
1914
2111
  }
1915
2112
  };
1916
2113
 
@@ -2309,8 +2506,7 @@ function resolveEngineOptions(opts, extras) {
2309
2506
  };
2310
2507
  }
2311
2508
 
2312
- // src/core/BottomSheetEngine.ts
2313
- var HAPTIC_DURATION_MS = 8;
2509
+ // src/core/primitives/teardown-stack.ts
2314
2510
  var TeardownStack = class {
2315
2511
  constructor() {
2316
2512
  __publicField(this, "fns", []);
@@ -2330,6 +2526,9 @@ var TeardownStack = class {
2330
2526
  }
2331
2527
  }
2332
2528
  };
2529
+
2530
+ // src/core/BottomSheetEngine.ts
2531
+ var HAPTIC_DURATION_MS = 8;
2333
2532
  var BottomSheetEngine = class {
2334
2533
  constructor(opts) {
2335
2534
  __publicField(this, "id", nextInstanceId("bs"));
@@ -2354,6 +2553,7 @@ var BottomSheetEngine = class {
2354
2553
  __publicField(this, "disableDragFlag");
2355
2554
  __publicField(this, "closeOnRouteChange");
2356
2555
  __publicField(this, "maxHeightCap");
2556
+ __publicField(this, "maxHeightRaw");
2357
2557
  __publicField(this, "snapPointsRaw");
2358
2558
  __publicField(this, "snaps");
2359
2559
  __publicField(this, "activeId");
@@ -2389,6 +2589,7 @@ var BottomSheetEngine = class {
2389
2589
  __publicField(this, "detachScrimStages", null);
2390
2590
  __publicField(this, "stackEffectEnabled", false);
2391
2591
  __publicField(this, "stackEffectPrimed", false);
2592
+ __publicField(this, "fitDeps");
2392
2593
  __publicField(this, "transformTemplate");
2393
2594
  const resolved = resolveEngineOptions(opts, {
2394
2595
  shouldApplyInertSiblings: () => {
@@ -2461,12 +2662,19 @@ var BottomSheetEngine = class {
2461
2662
  }
2462
2663
  this.activeId = resolved.initialId;
2463
2664
  auditVhUsage(this.snapPointsRaw);
2665
+ this.auditDuplicateSnapIds(this.snapPointsRaw);
2464
2666
  this.rootEl = this.element.closest(".bs-root");
2667
+ this.fitDeps = {
2668
+ element: this.element,
2669
+ scrollContainer: this.scrollContainer,
2670
+ mode: this.mode,
2671
+ getMaxHeightCap: () => this.maxHeightCap
2672
+ };
2465
2673
  this.snaps = new SnapResolver(
2466
2674
  this.snapPointsRaw,
2467
2675
  resolved.initialAllowed,
2468
2676
  this.mode,
2469
- () => this.measureFitSize(),
2677
+ () => measureFitSize(this.fitDeps),
2470
2678
  (maxAxisSize) => {
2471
2679
  this.element.style[layoutAxis(this.mode)] = `${maxAxisSize}px`;
2472
2680
  }
@@ -2500,7 +2708,18 @@ var BottomSheetEngine = class {
2500
2708
  io.observe(this.element);
2501
2709
  this.teardowns.add(() => io.disconnect());
2502
2710
  }
2503
- this.installFitObserver();
2711
+ this.teardowns.add(
2712
+ installFitObserver({
2713
+ handle: this.handle,
2714
+ scrollContainer: this.scrollContainer,
2715
+ hasFitSnap: () => this.snapPointsRaw.some(
2716
+ (p) => p.size === "fit" || p.size === "content"
2717
+ ),
2718
+ isDestroyed: () => this.destroyed,
2719
+ isDragging: () => this.gesture?.isDragging ?? false,
2720
+ recompute: () => this.recompute()
2721
+ })
2722
+ );
2504
2723
  if (resolved.radius !== void 0) this.setRadius(resolved.radius);
2505
2724
  if (resolved.maxHeight !== void 0) this.setMaxHeight(resolved.maxHeight);
2506
2725
  this.applyAutoAriaLabelledBy();
@@ -2651,27 +2870,33 @@ var BottomSheetEngine = class {
2651
2870
  } else {
2652
2871
  await this.animation.animateTo(target.size, velocityPxPerMs);
2653
2872
  }
2654
- this.opening = false;
2655
2873
  externalSignal?.removeEventListener("abort", onExternalAbort);
2656
2874
  if (signal.aborted) return;
2657
- this.scrollCache.restore(id, previousSize, target.size);
2875
+ this.opening = false;
2876
+ this.completeSnap(id, target.size, previousSize, wasClosed, false);
2877
+ }
2878
+ completeSnap(id, targetSize, previousSize, wasClosed, withHaptic) {
2879
+ this.scrollCache.restore(id, previousSize, targetSize);
2658
2880
  this.updateAriaSlider();
2659
2881
  this.emit("snap", {
2660
2882
  id,
2661
2883
  size: this.size,
2662
2884
  progress: this.computeProgress(this.size)
2663
2885
  });
2664
- if (wasClosed && target.size > 0) {
2665
- this.emit("open", { id });
2666
- this.handleOpen();
2667
- notifyLinkedSheets(this.linkedSheets, this);
2668
- this.emit("opened", { id });
2669
- }
2670
- if (target.size === 0) {
2671
- this.emit("close", void 0);
2672
- this.handleClose();
2673
- this.emit("closed", void 0);
2674
- }
2886
+ if (withHaptic) this.haptic();
2887
+ if (wasClosed && targetSize > 0) this.emitOpenSequence(id);
2888
+ if (targetSize === 0) this.emitCloseSequence();
2889
+ }
2890
+ emitOpenSequence(id) {
2891
+ this.emit("open", { id });
2892
+ this.handleOpen();
2893
+ notifyLinkedSheets(this.linkedSheets, this);
2894
+ this.emit("opened", { id });
2895
+ }
2896
+ emitCloseSequence() {
2897
+ this.emit("close", void 0);
2898
+ this.handleClose();
2899
+ this.emit("closed", void 0);
2675
2900
  }
2676
2901
  async dragTo(targetSize, velocityOrOpts = 0) {
2677
2902
  if (this.destroyed) return;
@@ -2692,19 +2917,31 @@ var BottomSheetEngine = class {
2692
2917
  }
2693
2918
  open(id) {
2694
2919
  if (this.destroyed) return Promise.resolve();
2695
- const target = id ?? this.snaps.getAllowedIds().find((a) => a !== "closed") ?? this.activeId;
2920
+ const target = id ?? this.snaps.getAllowedIds().find((a) => (this.snaps.findById(a)?.size ?? 0) > 0) ?? this.activeId;
2696
2921
  return this.snapTo(target);
2697
2922
  }
2698
2923
  close(reason = "programmatic") {
2699
2924
  if (this.destroyed) return Promise.resolve();
2700
2925
  if (this.disableCloseFlag) return Promise.resolve();
2701
2926
  if (this.size > 0 && this.emitBeforeClose(reason)) return Promise.resolve();
2702
- const closedId = this.snapPointsRaw.find((p) => p.id === "closed")?.id ?? this.snaps.getAllowedIds()[0];
2927
+ const closedId = this.snapPointsRaw.find((p) => p.id === "closed")?.id ?? this.snaps.getAllowedIds().find((a) => this.snaps.findById(a)?.size === 0) ?? this.snaps.getAllowedIds()[0];
2703
2928
  return this.snapTo(closedId ?? this.activeId);
2704
2929
  }
2705
2930
  canDismiss() {
2706
2931
  return !this.persistent && !this.disableCloseFlag && !this.destroyed;
2707
2932
  }
2933
+ setPersistent(value) {
2934
+ if (this.destroyed) return;
2935
+ this.persistent = value;
2936
+ }
2937
+ setDisableClose(value) {
2938
+ if (this.destroyed) return;
2939
+ this.disableCloseFlag = value;
2940
+ }
2941
+ setDisableDrag(value) {
2942
+ if (this.destroyed) return;
2943
+ this.disableDragFlag = value;
2944
+ }
2708
2945
  isTop() {
2709
2946
  return this.isTopSheet;
2710
2947
  }
@@ -2741,18 +2978,23 @@ var BottomSheetEngine = class {
2741
2978
  axis === "height" ? "max-height" : "max-width",
2742
2979
  value
2743
2980
  );
2744
- if (typeof h === "number") {
2745
- this.maxHeightCap = h;
2746
- this.recompute();
2747
- } else if (typeof window !== "undefined") {
2748
- const probe2 = document.createElement("div");
2749
- probe2.style.cssText = `position:absolute;visibility:hidden;pointer-events:none;${axis === "height" ? "height" : "width"}:${value};`;
2750
- document.body.appendChild(probe2);
2751
- const measured = axis === "height" ? probe2.offsetHeight : probe2.offsetWidth;
2752
- probe2.remove();
2753
- this.maxHeightCap = measured > 0 ? measured : void 0;
2754
- this.recompute();
2981
+ this.maxHeightRaw = h;
2982
+ this.resolveMaxHeightCap();
2983
+ this.recompute();
2984
+ }
2985
+ resolveMaxHeightCap() {
2986
+ const raw = this.maxHeightRaw;
2987
+ if (raw === void 0) {
2988
+ this.maxHeightCap = void 0;
2989
+ return;
2755
2990
  }
2991
+ if (typeof raw === "number") {
2992
+ this.maxHeightCap = raw;
2993
+ return;
2994
+ }
2995
+ if (typeof window === "undefined") return;
2996
+ const measured = resolveSnap(raw, this.mode);
2997
+ this.maxHeightCap = measured > 0 ? measured : void 0;
2756
2998
  }
2757
2999
  clampToMaxHeight() {
2758
3000
  if (this.maxHeightCap === void 0) return;
@@ -2883,6 +3125,7 @@ var BottomSheetEngine = class {
2883
3125
  this.newCycle();
2884
3126
  this.snapPointsRaw = points;
2885
3127
  auditVhUsage(points);
3128
+ this.auditDuplicateSnapIds(points);
2886
3129
  this.snaps.setRaw(points);
2887
3130
  if (allowed) this.snaps.setAllowedIds(allowed);
2888
3131
  this.scrollCache.clear();
@@ -2895,6 +3138,7 @@ var BottomSheetEngine = class {
2895
3138
  }
2896
3139
  recompute() {
2897
3140
  if (this.destroyed) return;
3141
+ this.resolveMaxHeightCap();
2898
3142
  this.snaps.recompute();
2899
3143
  this.clampToMaxHeight();
2900
3144
  this.scrim.invalidateOpacityCache();
@@ -2906,129 +3150,6 @@ var BottomSheetEngine = class {
2906
3150
  this.updateAriaSlider();
2907
3151
  }
2908
3152
  }
2909
- contentTargets() {
2910
- const scroller = this.scrollContainer;
2911
- if (!scroller) return [];
2912
- const hasSlot = typeof HTMLSlotElement !== "undefined";
2913
- const out = [];
2914
- for (const child of Array.from(scroller.children)) {
2915
- if (hasSlot && child instanceof HTMLSlotElement) {
2916
- for (const el of child.assignedElements()) out.push(el);
2917
- } else {
2918
- out.push(child);
2919
- }
2920
- }
2921
- return out;
2922
- }
2923
- measureSheetNatural(vertical) {
2924
- const el = this.element;
2925
- const content = this.scrollContainer;
2926
- if (!content) {
2927
- return vertical ? el.offsetHeight : el.offsetWidth;
2928
- }
2929
- const axis = vertical ? "height" : "width";
2930
- const sheetStyle = el.style;
2931
- const cs = content.style;
2932
- const prevSheet = sheetStyle[axis];
2933
- const prevFlex = cs.flex;
2934
- const prevContent = vertical ? cs.height : cs.width;
2935
- sheetStyle[axis] = "auto";
2936
- cs.flex = "none";
2937
- if (vertical) cs.height = "auto";
2938
- else cs.width = "auto";
2939
- const poked = vertical ? el.offsetHeight : el.offsetWidth;
2940
- const contentBox = vertical ? content.clientHeight : content.clientWidth;
2941
- const contentScroll = vertical ? content.scrollHeight : content.scrollWidth;
2942
- const natural = Math.max(poked, poked - contentBox + contentScroll);
2943
- sheetStyle[axis] = prevSheet;
2944
- cs.flex = prevFlex;
2945
- if (vertical) cs.height = prevContent;
2946
- else cs.width = prevContent;
2947
- return natural;
2948
- }
2949
- measureFitSize() {
2950
- const vertical = layoutAxis(this.mode) === "height";
2951
- const natural = this.measureSheetNatural(vertical);
2952
- const capped = this.maxHeightCap !== void 0 ? Math.min(natural, this.maxHeightCap) : natural;
2953
- if (typeof window === "undefined") return capped;
2954
- const viewport = this.containingExtent(vertical);
2955
- return viewport > 0 ? Math.min(capped, viewport) : capped;
2956
- }
2957
- // The fit cap is the sheet's containing block, not always the window: a
2958
- // position:fixed sheet nested under a transform/filter/will-change ancestor
2959
- // is clipped to that ancestor, so capping by window.innerHeight oversizes the
2960
- // sheet and the content/footer get clipped. offsetParent resolves to that
2961
- // ancestor when present, and is null (→ viewport) for a plain fixed sheet.
2962
- containingExtent(vertical) {
2963
- const op = this.element.offsetParent;
2964
- if (op && op !== document.body && op !== document.documentElement) {
2965
- return vertical ? op.clientHeight : op.clientWidth;
2966
- }
2967
- return vertical ? window.innerHeight : window.innerWidth;
2968
- }
2969
- installFitObserver() {
2970
- if (typeof ResizeObserver === "undefined") return;
2971
- if (!this.snapPointsRaw.some((p) => p.size === "fit" || p.size === "content"))
2972
- return;
2973
- let raf = 0;
2974
- const schedule = () => {
2975
- if (raf) return;
2976
- raf = requestAnimationFrame(() => {
2977
- raf = 0;
2978
- if (this.destroyed || this.gesture?.isDragging) return;
2979
- this.recompute();
2980
- });
2981
- };
2982
- const ro = new ResizeObserver(schedule);
2983
- ro.observe(this.handle);
2984
- const scroller = this.scrollContainer;
2985
- const observed = /* @__PURE__ */ new Set();
2986
- const slots = /* @__PURE__ */ new Set();
2987
- const hasSlot = typeof HTMLSlotElement !== "undefined";
2988
- const onSlotChange = () => resync();
2989
- const trackSlots = () => {
2990
- if (!scroller || !hasSlot) return;
2991
- for (const child of Array.from(scroller.children)) {
2992
- if (child instanceof HTMLSlotElement && !slots.has(child)) {
2993
- child.addEventListener("slotchange", onSlotChange);
2994
- slots.add(child);
2995
- }
2996
- }
2997
- };
2998
- const resync = () => {
2999
- trackSlots();
3000
- const targets = this.contentTargets();
3001
- for (const el of observed) {
3002
- if (!targets.includes(el)) {
3003
- ro.unobserve(el);
3004
- observed.delete(el);
3005
- }
3006
- }
3007
- for (const el of targets) {
3008
- if (!observed.has(el)) {
3009
- ro.observe(el);
3010
- observed.add(el);
3011
- }
3012
- }
3013
- schedule();
3014
- };
3015
- let mo;
3016
- if (scroller) {
3017
- resync();
3018
- if (typeof MutationObserver !== "undefined") {
3019
- mo = new MutationObserver(resync);
3020
- mo.observe(scroller, { childList: true });
3021
- }
3022
- }
3023
- this.teardowns.add(() => {
3024
- ro.disconnect();
3025
- mo?.disconnect();
3026
- for (const slot of slots) {
3027
- slot.removeEventListener("slotchange", onSlotChange);
3028
- }
3029
- if (raf) cancelAnimationFrame(raf);
3030
- });
3031
- }
3032
3153
  destroy() {
3033
3154
  this.destroyed = true;
3034
3155
  this.opening = false;
@@ -3046,9 +3167,10 @@ var BottomSheetEngine = class {
3046
3167
  this.backdrop.style.opacity = "";
3047
3168
  this.backdrop.style.pointerEvents = "";
3048
3169
  }
3049
- if (this.anchorHost) {
3050
- this.anchorHost.style.removeProperty("--bs-size");
3051
- this.anchorHost.style.removeProperty("--bs-progress");
3170
+ for (const host of [this.anchorHost, this.scrimParent, this.rootEl]) {
3171
+ if (!host) continue;
3172
+ host.style.removeProperty("--bs-size");
3173
+ host.style.removeProperty("--bs-progress");
3052
3174
  }
3053
3175
  if (this.screenComponent) {
3054
3176
  this.screenComponent.style.opacity = "";
@@ -3062,6 +3184,7 @@ var BottomSheetEngine = class {
3062
3184
  }
3063
3185
  newCycle() {
3064
3186
  this.allowOvershoot = false;
3187
+ this.opening = false;
3065
3188
  this.currentAbort.abort();
3066
3189
  this.currentAbort = new AbortController();
3067
3190
  return this.currentAbort.signal;
@@ -3117,8 +3240,14 @@ var BottomSheetEngine = class {
3117
3240
  this.element.setAttribute("aria-labelledby", titled.id);
3118
3241
  }
3119
3242
  recomputeSnaps() {
3243
+ this.resolveMaxHeightCap();
3244
+ this.snaps.setRaw(this.snapPointsRaw);
3245
+ this.clampToMaxHeight();
3246
+ this.scrim.invalidateOpacityCache();
3247
+ }
3248
+ auditDuplicateSnapIds(points) {
3120
3249
  const seen = /* @__PURE__ */ new Set();
3121
- for (const p of this.snapPointsRaw) {
3250
+ for (const p of points) {
3122
3251
  if (seen.has(p.id)) {
3123
3252
  console.warn(
3124
3253
  `[BottomSheet] duplicate snap id "${p.id}" \u2014 only the first occurrence is reachable.`
@@ -3126,9 +3255,6 @@ var BottomSheetEngine = class {
3126
3255
  }
3127
3256
  seen.add(p.id);
3128
3257
  }
3129
- this.snaps.setRaw(this.snapPointsRaw);
3130
- this.clampToMaxHeight();
3131
- this.scrim.invalidateOpacityCache();
3132
3258
  }
3133
3259
  getAllowedRange() {
3134
3260
  return this.snaps.getAllowedRange();
@@ -3316,9 +3442,15 @@ var BottomSheetEngine = class {
3316
3442
  flickVelocity: this.flickVelocity,
3317
3443
  dragThreshold: this.dragThreshold
3318
3444
  });
3319
- if (!target) return;
3445
+ if (!target) {
3446
+ this.clearWillChangeIfIdle();
3447
+ return;
3448
+ }
3320
3449
  const previousId = this.activeId;
3321
- if (target.id === previousId && target.size === this.size) return;
3450
+ if (target.id === previousId && target.size === this.size) {
3451
+ this.clearWillChangeIfIdle();
3452
+ return;
3453
+ }
3322
3454
  if (this.emitBeforeSnap(target, previousId)) {
3323
3455
  const restore = this.snaps.findById(previousId);
3324
3456
  if (restore) {
@@ -3341,27 +3473,19 @@ var BottomSheetEngine = class {
3341
3473
  if (signal.aborted) return;
3342
3474
  this.allowOvershoot = false;
3343
3475
  this.opening = false;
3344
- this.scrollCache.restore(target.id, previousSize, target.size);
3345
- this.updateAriaSlider();
3346
- this.emit("snap", {
3347
- id: target.id,
3348
- size: this.size,
3349
- progress: this.computeProgress(this.size)
3350
- });
3351
- this.haptic();
3352
- if (previousSnapSize === 0 && target.size > 0) {
3353
- this.emit("open", { id: target.id });
3354
- this.handleOpen();
3355
- notifyLinkedSheets(this.linkedSheets, this);
3356
- this.emit("opened", { id: target.id });
3357
- }
3358
- if (target.size === 0) {
3359
- this.emit("close", void 0);
3360
- this.handleClose();
3361
- this.emit("closed", void 0);
3362
- }
3476
+ this.completeSnap(
3477
+ target.id,
3478
+ target.size,
3479
+ previousSize,
3480
+ previousSnapSize === 0,
3481
+ true
3482
+ );
3363
3483
  });
3364
3484
  }
3485
+ clearWillChangeIfIdle() {
3486
+ if (this.animation.isAnimating || this.gesture?.isDragging) return;
3487
+ this.element.style.willChange = "";
3488
+ }
3365
3489
  haptic() {
3366
3490
  if (typeof navigator !== "undefined" && "vibrate" in navigator) {
3367
3491
  try {
@@ -3442,14 +3566,9 @@ var BottomSheetEngine = class {
3442
3566
  progress: this.computeProgress(this.size)
3443
3567
  });
3444
3568
  if (target.size > 0 && !this.lifecycle.isInstalled) {
3445
- this.emit("open", { id: target.id });
3446
- this.handleOpen();
3447
- notifyLinkedSheets(this.linkedSheets, this);
3448
- this.emit("opened", { id: target.id });
3569
+ this.emitOpenSequence(target.id);
3449
3570
  } else if (target.size === 0 && this.lifecycle.isInstalled) {
3450
- this.emit("close", void 0);
3451
- this.handleClose();
3452
- this.emit("closed", void 0);
3571
+ this.emitCloseSequence();
3453
3572
  }
3454
3573
  }
3455
3574
  handleClose() {
@@ -3546,6 +3665,9 @@ function useBottomSheet(opts) {
3546
3665
  refresh();
3547
3666
  optsRef.current.onSnap?.(payload.id);
3548
3667
  }),
3668
+ engine.on("before-snap", (payload) => {
3669
+ optsRef.current.onBeforeSnap?.(payload);
3670
+ }),
3549
3671
  engine.on("before-close", (payload) => {
3550
3672
  optsRef.current.onBeforeClose?.(payload);
3551
3673
  }),
@@ -3576,12 +3698,26 @@ function useBottomSheet(opts) {
3576
3698
  const allowedKey = (opts.allowed ?? opts.snapPoints.map((p) => p.id)).join(
3577
3699
  "\0"
3578
3700
  );
3579
- const snapKey = opts.snapPoints.map((p) => p.id).join("\0");
3701
+ const defKey = opts.snapPoints.map((p) => `${p.id}${String(p.size)}`).join("\0");
3702
+ const defApplied = react.useRef(false);
3703
+ react.useEffect(() => {
3704
+ if (!defApplied.current) {
3705
+ defApplied.current = true;
3706
+ return;
3707
+ }
3708
+ const engine = engineRef.current;
3709
+ if (!engine) return;
3710
+ const current = optsRef.current;
3711
+ engine.setSnapPoints(
3712
+ current.snapPoints,
3713
+ current.allowed ? Array.from(current.allowed) : void 0
3714
+ );
3715
+ }, [defKey]);
3580
3716
  react.useEffect(() => {
3581
3717
  if (!engineRef.current) return;
3582
3718
  const ids = opts.allowed ? Array.from(opts.allowed) : opts.snapPoints.map((p) => p.id);
3583
3719
  engineRef.current.setAllowed(ids);
3584
- }, [allowedKey, snapKey]);
3720
+ }, [allowedKey]);
3585
3721
  react.useEffect(() => {
3586
3722
  if (!engineRef.current) return;
3587
3723
  if (opts.backdropColor !== void 0) {
@@ -3606,6 +3742,21 @@ function useBottomSheet(opts) {
3606
3742
  engineRef.current.setMaxHeight(opts.maxHeight);
3607
3743
  }
3608
3744
  }, [opts.maxHeight]);
3745
+ react.useEffect(() => {
3746
+ if (opts.persistent !== void 0) {
3747
+ engineRef.current?.setPersistent(opts.persistent);
3748
+ }
3749
+ }, [opts.persistent]);
3750
+ react.useEffect(() => {
3751
+ if (opts.disableClose !== void 0) {
3752
+ engineRef.current?.setDisableClose(opts.disableClose);
3753
+ }
3754
+ }, [opts.disableClose]);
3755
+ react.useEffect(() => {
3756
+ if (opts.disableDrag !== void 0) {
3757
+ engineRef.current?.setDisableDrag(opts.disableDrag);
3758
+ }
3759
+ }, [opts.disableDrag]);
3609
3760
  const subscribe = react.useCallback((fn) => {
3610
3761
  subscribersRef.current.add(fn);
3611
3762
  return () => subscribersRef.current.delete(fn);
@@ -3941,6 +4092,95 @@ var BottomSheet = react.forwardRef(
3941
4092
  }
3942
4093
  );
3943
4094
 
4095
+ // src/core/primitives/overlay-transforms.ts
4096
+ var closedTransform = (edge) => CLOSED_TRANSFORM[edge];
4097
+ var closedTransformFor = (edge, animation, peekPx) => {
4098
+ switch (animation) {
4099
+ case "fade":
4100
+ return "translate3d(0, 0, 0)";
4101
+ case "scale":
4102
+ return "translate3d(0, 0, 0) scale(0.94)";
4103
+ case "slide":
4104
+ default:
4105
+ if (peekPx !== void 0 && edge === "bottom") {
4106
+ return `translate3d(0, calc(100% - ${peekPx}px), 0)`;
4107
+ }
4108
+ return closedTransform(edge);
4109
+ }
4110
+ };
4111
+ var openTransformFor = (animation) => animation === "scale" ? "translate3d(0, 0, 0) scale(1)" : "translate3d(0, 0, 0)";
4112
+
4113
+ // src/core/features/overlay-swipe.ts
4114
+ function installOverlaySwipe(deps) {
4115
+ const el = deps.element;
4116
+ const edge = deps.edge;
4117
+ const isVertical = edge === "bottom" || edge === "top";
4118
+ const sign = edge === "bottom" || edge === "right" ? 1 : -1;
4119
+ const SLOP = 8;
4120
+ let startX = 0;
4121
+ let startY = 0;
4122
+ let startT = 0;
4123
+ let tracking = false;
4124
+ let dragging = false;
4125
+ let pointerId = null;
4126
+ const previousTouchAction = el.style.touchAction;
4127
+ el.style.touchAction = isVertical ? "pan-x" : "pan-y";
4128
+ const onDown = (e) => {
4129
+ if (e.button !== void 0 && e.button !== 0) return;
4130
+ pointerId = e.pointerId;
4131
+ startX = e.clientX;
4132
+ startY = e.clientY;
4133
+ startT = performance.now();
4134
+ tracking = true;
4135
+ dragging = false;
4136
+ };
4137
+ const onMove = (e) => {
4138
+ if (!tracking || e.pointerId !== pointerId) return;
4139
+ const delta = isVertical ? e.clientY - startY : e.clientX - startX;
4140
+ if (!dragging) {
4141
+ if (Math.abs(delta) < SLOP) return;
4142
+ dragging = true;
4143
+ el.style.transition = "none";
4144
+ try {
4145
+ el.setPointerCapture(e.pointerId);
4146
+ } catch {
4147
+ }
4148
+ }
4149
+ const offset = Math.max(0, sign * delta);
4150
+ el.style.transform = isVertical ? `translate3d(0, ${sign * offset}px, 0)` : `translate3d(${sign * offset}px, 0, 0)`;
4151
+ };
4152
+ const finish = (e) => {
4153
+ if (!tracking || e.pointerId !== pointerId) return;
4154
+ tracking = false;
4155
+ pointerId = null;
4156
+ if (!dragging) return;
4157
+ dragging = false;
4158
+ const dt = performance.now() - startT || 1;
4159
+ const delta = isVertical ? e.clientY - startY : e.clientX - startX;
4160
+ const offset = Math.max(0, sign * delta);
4161
+ const velocity = sign * delta / dt;
4162
+ const size = isVertical ? el.offsetHeight : el.offsetWidth;
4163
+ const past = offset > size * deps.getThreshold() || velocity > deps.getVelocityThreshold();
4164
+ el.style.transition = deps.getExitTransition();
4165
+ if (past) {
4166
+ deps.close();
4167
+ } else {
4168
+ el.style.transform = deps.getOpenTransform();
4169
+ }
4170
+ };
4171
+ el.addEventListener("pointerdown", onDown);
4172
+ el.addEventListener("pointermove", onMove);
4173
+ el.addEventListener("pointerup", finish);
4174
+ el.addEventListener("pointercancel", finish);
4175
+ return () => {
4176
+ el.style.touchAction = previousTouchAction;
4177
+ el.removeEventListener("pointerdown", onDown);
4178
+ el.removeEventListener("pointermove", onMove);
4179
+ el.removeEventListener("pointerup", finish);
4180
+ el.removeEventListener("pointercancel", finish);
4181
+ };
4182
+ }
4183
+
3944
4184
  // src/core/overlay.ts
3945
4185
  var OVERLAY_PRESETS = Object.freeze({
3946
4186
  sheet: Object.freeze({
@@ -3972,22 +4212,6 @@ var OVERLAY_PRESETS = Object.freeze({
3972
4212
  swipeToClose: false
3973
4213
  })
3974
4214
  });
3975
- var closedTransform = (edge) => CLOSED_TRANSFORM[edge];
3976
- var closedTransformFor = (edge, animation, peekPx) => {
3977
- switch (animation) {
3978
- case "fade":
3979
- return "translate3d(0, 0, 0)";
3980
- case "scale":
3981
- return "translate3d(0, 0, 0) scale(0.94)";
3982
- case "slide":
3983
- default:
3984
- if (peekPx !== void 0 && edge === "bottom") {
3985
- return `translate3d(0, calc(100% - ${peekPx}px), 0)`;
3986
- }
3987
- return closedTransform(edge);
3988
- }
3989
- };
3990
- var openTransformFor = (animation) => animation === "scale" ? "translate3d(0, 0, 0) scale(1)" : "translate3d(0, 0, 0)";
3991
4215
  var OverlayEngine = class {
3992
4216
  constructor(opts) {
3993
4217
  __publicField(this, "id", nextInstanceId("ovl"));
@@ -4120,6 +4344,7 @@ var OverlayEngine = class {
4120
4344
  if (this.backdrop) {
4121
4345
  this.backdrop.style.transition = `opacity ${dur}ms ease-out`;
4122
4346
  }
4347
+ this.setWillChange(true);
4123
4348
  const cycle = ++this.cycleNonce;
4124
4349
  return new Promise((resolve) => {
4125
4350
  requestAnimationFrame(() => {
@@ -4142,6 +4367,7 @@ var OverlayEngine = class {
4142
4367
  this.releaseInteractiveListeners();
4143
4368
  this.isOpen_ = false;
4144
4369
  this.isAnimating_ = false;
4370
+ this.setWillChange(false);
4145
4371
  this.element.style.transform = closedTransformFor(
4146
4372
  this.edge,
4147
4373
  this.enterAnimation,
@@ -4172,6 +4398,7 @@ var OverlayEngine = class {
4172
4398
  ).then(() => {
4173
4399
  if (this.destroyed || cycle !== this.cycleNonce) return resolve();
4174
4400
  this.isAnimating_ = false;
4401
+ this.setWillChange(false);
4175
4402
  this.element.setAttribute("data-state", "open");
4176
4403
  this.emit("open", void 0);
4177
4404
  this.onOpenCb?.();
@@ -4195,6 +4422,7 @@ var OverlayEngine = class {
4195
4422
  if (this.backdrop) {
4196
4423
  this.backdrop.style.transition = `opacity ${dur}ms ease-in`;
4197
4424
  }
4425
+ this.setWillChange(true);
4198
4426
  const cycle = ++this.cycleNonce;
4199
4427
  return new Promise((resolve) => {
4200
4428
  requestAnimationFrame(() => {
@@ -4220,6 +4448,7 @@ var OverlayEngine = class {
4220
4448
  ).then(() => {
4221
4449
  if (this.destroyed || cycle !== this.cycleNonce) return resolve();
4222
4450
  this.isAnimating_ = false;
4451
+ this.setWillChange(false);
4223
4452
  if (this.peekPx === void 0) {
4224
4453
  this.element.setAttribute("hidden", "");
4225
4454
  }
@@ -4242,6 +4471,7 @@ var OverlayEngine = class {
4242
4471
  this.destroyed = true;
4243
4472
  this.cycleNonce++;
4244
4473
  this.bus.clear();
4474
+ this.setWillChange(false);
4245
4475
  this.releaseInteractiveListeners();
4246
4476
  this.releaseStackEntry?.();
4247
4477
  this.releaseStackEntry = null;
@@ -4467,7 +4697,6 @@ var OverlayEngine = class {
4467
4697
  this.originalNextSibling = null;
4468
4698
  }
4469
4699
  applyClosedStyles() {
4470
- this.element.style.willChange = "transform, opacity";
4471
4700
  this.element.style.transform = closedTransformFor(
4472
4701
  this.edge,
4473
4702
  this.enterAnimation,
@@ -4481,7 +4710,12 @@ var OverlayEngine = class {
4481
4710
  if (this.backdrop) {
4482
4711
  this.backdrop.style.opacity = "0";
4483
4712
  this.backdrop.style.pointerEvents = "none";
4484
- this.backdrop.style.willChange = "opacity";
4713
+ }
4714
+ }
4715
+ setWillChange(on) {
4716
+ this.element.style.willChange = on ? "transform, opacity" : "";
4717
+ if (this.backdrop) {
4718
+ this.backdrop.style.willChange = on ? "opacity" : "";
4485
4719
  }
4486
4720
  }
4487
4721
  awaitTransition(cycle, property = "transform") {
@@ -4567,73 +4801,15 @@ var OverlayEngine = class {
4567
4801
  this.lifecycle.release();
4568
4802
  }
4569
4803
  installSwipeToClose() {
4570
- const el = this.element;
4571
- const edge = this.edge;
4572
- const isVertical = edge === "bottom" || edge === "top";
4573
- const sign = edge === "bottom" || edge === "right" ? 1 : -1;
4574
- const SLOP = 8;
4575
- let startX = 0;
4576
- let startY = 0;
4577
- let startT = 0;
4578
- let tracking = false;
4579
- let dragging = false;
4580
- let pointerId = null;
4581
- const previousTouchAction = el.style.touchAction;
4582
- el.style.touchAction = isVertical ? "pan-x" : "pan-y";
4583
- const onDown = (e) => {
4584
- if (e.button !== void 0 && e.button !== 0) return;
4585
- pointerId = e.pointerId;
4586
- startX = e.clientX;
4587
- startY = e.clientY;
4588
- startT = performance.now();
4589
- tracking = true;
4590
- dragging = false;
4591
- };
4592
- const onMove = (e) => {
4593
- if (!tracking || e.pointerId !== pointerId) return;
4594
- const delta = isVertical ? e.clientY - startY : e.clientX - startX;
4595
- if (!dragging) {
4596
- if (Math.abs(delta) < SLOP) return;
4597
- dragging = true;
4598
- el.style.transition = "none";
4599
- try {
4600
- el.setPointerCapture(e.pointerId);
4601
- } catch {
4602
- }
4603
- }
4604
- const offset = Math.max(0, sign * delta);
4605
- el.style.transform = isVertical ? `translate3d(0, ${sign * offset}px, 0)` : `translate3d(${sign * offset}px, 0, 0)`;
4606
- };
4607
- const finish = (e) => {
4608
- if (!tracking || e.pointerId !== pointerId) return;
4609
- tracking = false;
4610
- pointerId = null;
4611
- if (!dragging) return;
4612
- dragging = false;
4613
- const dt = performance.now() - startT || 1;
4614
- const delta = isVertical ? e.clientY - startY : e.clientX - startX;
4615
- const offset = Math.max(0, sign * delta);
4616
- const velocity = sign * delta / dt;
4617
- const size = isVertical ? el.offsetHeight : el.offsetWidth;
4618
- const past = offset > size * this.swipeThreshold || velocity > this.swipeVelocityThreshold;
4619
- el.style.transition = `transform ${this.effectiveDuration()}ms ${this.exitEasing}`;
4620
- if (past) {
4621
- void this.close("swipe");
4622
- } else {
4623
- el.style.transform = openTransformFor(this.enterAnimation);
4624
- }
4625
- };
4626
- el.addEventListener("pointerdown", onDown);
4627
- el.addEventListener("pointermove", onMove);
4628
- el.addEventListener("pointerup", finish);
4629
- el.addEventListener("pointercancel", finish);
4630
- return () => {
4631
- el.style.touchAction = previousTouchAction;
4632
- el.removeEventListener("pointerdown", onDown);
4633
- el.removeEventListener("pointermove", onMove);
4634
- el.removeEventListener("pointerup", finish);
4635
- el.removeEventListener("pointercancel", finish);
4636
- };
4804
+ return installOverlaySwipe({
4805
+ element: this.element,
4806
+ edge: this.edge,
4807
+ getThreshold: () => this.swipeThreshold,
4808
+ getVelocityThreshold: () => this.swipeVelocityThreshold,
4809
+ getExitTransition: () => `transform ${this.effectiveDuration()}ms ${this.exitEasing}`,
4810
+ getOpenTransform: () => openTransformFor(this.enterAnimation),
4811
+ close: () => void this.close("swipe")
4812
+ });
4637
4813
  }
4638
4814
  registerInStack() {
4639
4815
  this.releaseStackEntry = sheetStack.push({