@surdeddd/bottom-sheet 0.1.0 → 0.3.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 (57) hide show
  1. package/README.md +57 -21
  2. package/dist/BottomSheetEngine-CAymrH-P.d.cts +285 -0
  3. package/dist/BottomSheetEngine-DcqcuudN.d.ts +285 -0
  4. package/dist/element.cjs +775 -537
  5. package/dist/element.d.cts +9 -57
  6. package/dist/element.d.ts +9 -57
  7. package/dist/element.iife.global.js +2 -2
  8. package/dist/element.js +775 -537
  9. package/dist/index.cjs +760 -600
  10. package/dist/index.d.cts +18 -85
  11. package/dist/index.d.ts +18 -85
  12. package/dist/index.js +758 -601
  13. package/dist/integrations/formik.d.cts +2 -8
  14. package/dist/integrations/formik.d.ts +2 -8
  15. package/dist/integrations/react-hook-form.d.cts +2 -11
  16. package/dist/integrations/react-hook-form.d.ts +2 -11
  17. package/dist/overlay.cjs +168 -122
  18. package/dist/overlay.d.cts +14 -137
  19. package/dist/overlay.d.ts +14 -137
  20. package/dist/overlay.js +168 -122
  21. package/dist/preact.d.cts +2 -2
  22. package/dist/preact.d.ts +2 -2
  23. package/dist/preact.js +0 -1
  24. package/dist/qwik.cjs +667 -519
  25. package/dist/qwik.d.cts +3 -24
  26. package/dist/qwik.d.ts +3 -24
  27. package/dist/qwik.js +668 -520
  28. package/dist/react.cjs +873 -648
  29. package/dist/react.d.cts +34 -159
  30. package/dist/react.d.ts +34 -159
  31. package/dist/react.js +873 -648
  32. package/dist/solid.cjs +703 -560
  33. package/dist/solid.d.cts +264 -32
  34. package/dist/solid.d.ts +264 -32
  35. package/dist/solid.js +704 -561
  36. package/dist/solid.source.jsx +3215 -0
  37. package/dist/styles.css +37 -112
  38. package/dist/svelte-core.cjs +3149 -0
  39. package/dist/svelte-core.js +3147 -0
  40. package/dist/svelte-src/BottomSheet.svelte +210 -0
  41. package/dist/svelte-src/index.js +2 -0
  42. package/dist/svelte.cjs +718 -547
  43. package/dist/svelte.d.cts +100 -0
  44. package/dist/svelte.d.ts +15 -4
  45. package/dist/svelte.js +719 -548
  46. package/dist/themes/ios.css +0 -3
  47. package/dist/themes/material.css +0 -3
  48. package/dist/themes/vercel.css +0 -3
  49. package/dist/{transform-Brkf-wzo.d.cts → transform-jVfAikE9.d.cts} +0 -1
  50. package/dist/{transform-Brkf-wzo.d.ts → transform-jVfAikE9.d.ts} +0 -1
  51. package/dist/vue.cjs +677 -526
  52. package/dist/vue.d.cts +78 -0
  53. package/dist/vue.d.ts +12 -2
  54. package/dist/vue.js +678 -527
  55. package/package.json +105 -45
  56. package/dist/BottomSheetEngine-CvSVbo6c.d.cts +0 -704
  57. package/dist/BottomSheetEngine-DbO8rW_3.d.ts +0 -704
package/dist/svelte.cjs CHANGED
@@ -46,6 +46,18 @@ var SheetStack = class {
46
46
  this.entries.splice(idx, 1);
47
47
  this.recompute();
48
48
  }
49
+ promote(id) {
50
+ const idx = this.entries.findIndex((e) => e.id === id);
51
+ if (idx === -1) return;
52
+ if (idx !== this.entries.length - 1) {
53
+ const [entry] = this.entries.splice(idx, 1);
54
+ this.entries.push(entry);
55
+ }
56
+ this.recompute();
57
+ }
58
+ update() {
59
+ this.recompute();
60
+ }
49
61
  size() {
50
62
  return this.entries.length;
51
63
  }
@@ -53,9 +65,24 @@ var SheetStack = class {
53
65
  this.entries = [];
54
66
  }
55
67
  recompute() {
68
+ let top = null;
69
+ const open = [];
70
+ for (const entry of this.entries) {
71
+ if (!entry.isOpen || entry.isOpen()) {
72
+ top = entry;
73
+ open.push(entry);
74
+ }
75
+ }
76
+ if (!top && this.entries.length > 0) {
77
+ top = this.entries[this.entries.length - 1];
78
+ }
56
79
  this.entries.forEach((entry, i) => {
57
80
  entry.setZIndex(BASE_Z + i * STEP);
58
- entry.setIsTop(i === this.entries.length - 1);
81
+ entry.setIsTop(entry === top);
82
+ if (entry.setDepth) {
83
+ const openIdx = open.indexOf(entry);
84
+ entry.setDepth(openIdx === -1 ? 0 : open.length - 1 - openIdx);
85
+ }
59
86
  });
60
87
  }
61
88
  };
@@ -254,11 +281,6 @@ var SnapResolver = class {
254
281
  this.allowedIds = allowed ?? raw.map((p) => p.id);
255
282
  this.recompute();
256
283
  }
257
- /**
258
- * Re-resolve raw → resolved against the current viewport / fit-measure,
259
- * recompute maxAxisSize, invalidate range cache, notify engine. Called on
260
- * resize, orientationchange, visualViewport changes, and snap-list swaps.
261
- */
262
284
  recompute() {
263
285
  this.resolved = resolveSnapList(this.raw, this.mode, this.measureFit);
264
286
  this.maxAxisSize = this.resolved.reduce(
@@ -268,15 +290,10 @@ var SnapResolver = class {
268
290
  this.rangeCache = null;
269
291
  this.onMaxAxisSizeChange?.(this.maxAxisSize);
270
292
  }
271
- /** Replace the raw snap list and recompute. Engine calls on `setSnapPoints`. */
272
293
  setRaw(raw) {
273
294
  this.raw = raw;
274
295
  this.recompute();
275
296
  }
276
- /**
277
- * Replace the allow-list. Range cache depends on `(resolvedSnaps ×
278
- * allowedIds)` so flipping ids alone invalidates without a full recompute.
279
- */
280
297
  setAllowedIds(ids) {
281
298
  this.allowedIds = ids;
282
299
  this.rangeCache = null;
@@ -284,11 +301,6 @@ var SnapResolver = class {
284
301
  findById(id) {
285
302
  return findById(id, this.resolved);
286
303
  }
287
- /**
288
- * Pure drag-settle target resolution. Engine passes the gesture-specific
289
- * fields (delta, velocity, pointerKind, flickVelocity, dragThreshold) plus
290
- * `size`/`activeId` since those are still engine-owned.
291
- */
292
304
  findDragSettleTarget(args) {
293
305
  return findDragSettleTarget({
294
306
  ...args,
@@ -297,18 +309,9 @@ var SnapResolver = class {
297
309
  maxAxisSize: this.maxAxisSize
298
310
  });
299
311
  }
300
- /**
301
- * Memoised allowed-range query. Twice-per-drag-frame (gesture onMove +
302
- * applySize→computeProgress); cache collapses 2× iteration of resolvedSnaps
303
- * to one recompute per geometry/allow-list change.
304
- */
305
312
  getAllowedRange() {
306
313
  return this.rangeCache ?? (this.rangeCache = allowedRange(this.resolved, this.allowedIds));
307
314
  }
308
- /**
309
- * Read-only view of resolved snaps. Returned as `readonly` to discourage
310
- * accidental mutation by feature install sites that close over this list.
311
- */
312
315
  getResolvedSnaps() {
313
316
  return this.resolved;
314
317
  }
@@ -318,18 +321,6 @@ var SnapResolver = class {
318
321
  getMaxAxisSize() {
319
322
  return this.maxAxisSize;
320
323
  }
321
- /**
322
- * External clamp override. The resize-observer feature compares
323
- * `getMaxAxisSize()` against the live viewport and shrinks if the largest
324
- * resolved snap exceeds it (mobile address-bar collapse / soft-keyboard
325
- * window resize). The next `recompute()` overwrites this clamp from the
326
- * resolved-snap reduction.
327
- *
328
- * Does NOT invalidate the range cache — viewport clamping doesn't change
329
- * `(resolvedSnaps × allowedIds) → range`. DOES fire `onMaxAxisSizeChange`
330
- * so the resolver owns the layout-axis style write end-to-end (symmetric
331
- * with `recompute()` — feature call sites no longer duplicate the write).
332
- */
333
324
  setMaxAxisSize(n) {
334
325
  this.maxAxisSize = n;
335
326
  this.onMaxAxisSizeChange?.(n);
@@ -376,14 +367,17 @@ function installAutoCollapse(deps) {
376
367
  const fire = () => {
377
368
  if (deps.isDestroyed()) return;
378
369
  if (deps.isDragging()) return;
379
- const allowed = deps.getAllowedIds();
380
- const target = allowed.find((id) => {
370
+ const active = deps.resolveSnap(deps.getActiveId());
371
+ if (!active || active.size === 0) return;
372
+ let target = null;
373
+ for (const id of deps.getAllowedIds()) {
381
374
  const snap = deps.resolveSnap(id);
382
- return snap !== null && snap.size > 0;
383
- });
375
+ if (!snap || snap.size <= 0) continue;
376
+ if (!target || snap.size < target.size) target = snap;
377
+ }
384
378
  if (!target) return;
385
- if (target === deps.getActiveId()) return;
386
- deps.snapTo(target);
379
+ if (target.id === active.id || target.size >= active.size) return;
380
+ deps.snapTo(target.id);
387
381
  };
388
382
  const reset2 = () => {
389
383
  clearTimer();
@@ -394,6 +388,7 @@ function installAutoCollapse(deps) {
394
388
  }, ms);
395
389
  };
396
390
  const unsubscribe = deps.on("snap", () => reset2());
391
+ if ((deps.resolveSnap(deps.getActiveId())?.size ?? 0) > 0) reset2();
397
392
  return () => {
398
393
  clearTimer();
399
394
  unsubscribe();
@@ -406,6 +401,7 @@ function notifyLinkedSheets(sheets, self) {
406
401
  for (const linked of sheets) {
407
402
  if (linked === self) continue;
408
403
  try {
404
+ if (linked.state.size === 0) continue;
409
405
  const ids = linked.getAllowedIds();
410
406
  const target = ids.find((id) => id !== "closed");
411
407
  if (target && target !== linked.state.activeId) {
@@ -452,7 +448,7 @@ function installRoute(deps) {
452
448
  const onPop = (event) => {
453
449
  if (deps.isDestroyed()) return;
454
450
  const state2 = event.state;
455
- if (state2 && state2.__bsRouted === target && deps.getSize() > 0) {
451
+ if ((!state2 || state2.__bsRouted !== target) && deps.getSize() > 0) {
456
452
  routePushed = false;
457
453
  void deps.close();
458
454
  }
@@ -606,13 +602,17 @@ function installResizeObserver(deps) {
606
602
  if (viewportSize2 > 0 && viewportSize2 < max) {
607
603
  deps.setMaxAxisSize(viewportSize2);
608
604
  }
605
+ const wasAnimating = deps.isAnimating?.() ?? false;
609
606
  deps.cancelInFlight();
610
607
  deps.newCycle();
611
608
  const current = deps.resolveActiveSnap();
612
609
  const newMax = deps.getMaxAxisSize();
613
610
  const targetSize = current ? Math.min(current.size, newMax) : Math.min(deps.getSize(), newMax);
614
611
  deps.setSize(targetSize);
615
- if (current && !deps.isDragging()) deps.applySize(targetSize);
612
+ if (current && !deps.isDragging()) {
613
+ deps.applySize(targetSize);
614
+ if (wasAnimating) deps.resyncAfterCancel?.();
615
+ }
616
616
  };
617
617
  let resizeObserver = null;
618
618
  if (typeof ResizeObserver !== "undefined") {
@@ -704,73 +704,130 @@ var WriteSentinel = class {
704
704
  constructor() {
705
705
  __publicField(this, "last", -1);
706
706
  }
707
- /**
708
- * Returns `true` (and updates the cached value) if `next` differs from the
709
- * cached `last` by more than `epsilon`. Returns `false` (and leaves the
710
- * cache untouched) when the delta is within epsilon — caller skips the
711
- * CSSOM write.
712
- */
713
707
  shouldWrite(next, epsilon) {
714
708
  if (Math.abs(next - this.last) <= epsilon) return false;
715
709
  this.last = next;
716
710
  return true;
717
711
  }
718
- /**
719
- * Force the next `shouldWrite` to pass. Resets the cache to `-1` so any
720
- * non-negative `next` exceeds the epsilon comparison. Called from setters
721
- * that mutate state under the cache (range/mode/enabled flips that change
722
- * the progress→opacity mapping but don't move `progress` itself).
723
- */
724
712
  invalidate() {
725
713
  this.last = -1;
726
714
  }
727
- /**
728
- * Record `value` as the last-written without performing a write. Used
729
- * when a setter writes a literal value through a different path (e.g.
730
- * `setScrimMode("off")` writes `opacity: "0"` inline) and wants future
731
- * `shouldWrite` calls to short-circuit against that known cached state.
732
- */
733
715
  setLastWritten(value) {
734
716
  this.last = value;
735
717
  }
736
- /** Read-only accessor for diagnostic / dedup-aware peek. */
737
718
  get value() {
738
719
  return this.last;
739
720
  }
740
721
  };
741
722
 
723
+ // src/core/primitives/devWarn.ts
724
+ function devWarn(...args) {
725
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && typeof console !== "undefined") {
726
+ console.warn(...args);
727
+ }
728
+ }
729
+
730
+ // src/core/primitives/overlay-position.ts
731
+ function resolveSheetAnchoredStyle(mode, position, inset) {
732
+ const sizeOffset = `calc(var(--bs-size, 0px) + ${inset})`;
733
+ switch (mode) {
734
+ case "bottom":
735
+ if (position === "sheet-top-left") return { bottom: sizeOffset, left: inset };
736
+ if (position === "sheet-top-right") return { bottom: sizeOffset, right: inset };
737
+ return { bottom: sizeOffset, left: "50%", transform: "translateX(-50%)" };
738
+ case "top":
739
+ if (position === "sheet-top-left") return { top: sizeOffset, left: inset };
740
+ if (position === "sheet-top-right") return { top: sizeOffset, right: inset };
741
+ return { top: sizeOffset, left: "50%", transform: "translateX(-50%)" };
742
+ case "left":
743
+ if (position === "sheet-top-left") return { left: sizeOffset, top: inset };
744
+ if (position === "sheet-top-right") return { left: sizeOffset, bottom: inset };
745
+ return { left: sizeOffset, top: "50%", transform: "translateY(-50%)" };
746
+ case "right":
747
+ if (position === "sheet-top-left") return { right: sizeOffset, top: inset };
748
+ if (position === "sheet-top-right") return { right: sizeOffset, bottom: inset };
749
+ return { right: sizeOffset, top: "50%", transform: "translateY(-50%)" };
750
+ }
751
+ }
752
+ function applyOverlayPosition(ws, mode, position, inset) {
753
+ switch (position) {
754
+ case "top-left":
755
+ ws.top = inset;
756
+ ws.left = inset;
757
+ break;
758
+ case "top-center":
759
+ ws.top = inset;
760
+ ws.left = "50%";
761
+ ws.transform = "translateX(-50%)";
762
+ break;
763
+ case "top-right":
764
+ ws.top = inset;
765
+ ws.right = inset;
766
+ break;
767
+ case "center-left":
768
+ ws.top = "50%";
769
+ ws.left = inset;
770
+ ws.transform = "translateY(-50%)";
771
+ break;
772
+ case "center":
773
+ ws.top = "50%";
774
+ ws.left = "50%";
775
+ ws.transform = "translate(-50%, -50%)";
776
+ break;
777
+ case "center-right":
778
+ ws.top = "50%";
779
+ ws.right = inset;
780
+ ws.transform = "translateY(-50%)";
781
+ break;
782
+ case "bottom-left":
783
+ ws.bottom = inset;
784
+ ws.left = inset;
785
+ break;
786
+ case "bottom-center":
787
+ ws.bottom = inset;
788
+ ws.left = "50%";
789
+ ws.transform = "translateX(-50%)";
790
+ break;
791
+ case "bottom-right":
792
+ ws.bottom = inset;
793
+ ws.right = inset;
794
+ break;
795
+ case "sheet-top-left":
796
+ case "sheet-top-center":
797
+ case "sheet-top-right": {
798
+ const style = resolveSheetAnchoredStyle(mode, position, inset);
799
+ if (style.top !== void 0) ws.top = style.top;
800
+ if (style.bottom !== void 0) ws.bottom = style.bottom;
801
+ if (style.left !== void 0) ws.left = style.left;
802
+ if (style.right !== void 0) ws.right = style.right;
803
+ if (style.transform !== void 0) ws.transform = style.transform;
804
+ break;
805
+ }
806
+ }
807
+ }
808
+
742
809
  // src/core/controllers/scrim-controller.ts
810
+ var VALID_CSS_LENGTH = /^-?\d+(?:\.\d+)?(?:px|em|rem|%)$/;
743
811
  var ScrimController = class {
744
812
  constructor(deps, opts) {
745
- // ── DOM refs (mirror engine's; cached for hot path) ─────────────────
746
813
  __publicField(this, "screenComponent");
747
814
  __publicField(this, "backdrop");
748
815
  __publicField(this, "mode");
749
- // ── Engine callbacks ────────────────────────────────────────────────
750
816
  __publicField(this, "isDestroyed");
751
817
  __publicField(this, "isTopSheet");
752
818
  __publicField(this, "getAllowedIds");
753
819
  __publicField(this, "getResolvedSnaps");
754
820
  __publicField(this, "snapToFn");
755
- // ── Mode + state ────────────────────────────────────────────────────
821
+ __publicField(this, "closeFn");
822
+ __publicField(this, "scrimInteractive", false);
756
823
  __publicField(this, "scrimMode");
757
824
  __publicField(this, "backdropRange");
758
825
  __publicField(this, "screenRange");
759
- // setScrimEnabled toggle — gated through `scrimEnabled` rather than nuking
760
- // ranges so re-enable can restore prior values without a remount. Public
761
- // so tests / introspection can read it without an `as any` cast.
762
826
  __publicField(this, "scrimEnabled", true);
763
827
  __publicField(this, "savedScrimRanges", null);
764
- // ── Tap-to-close ────────────────────────────────────────────────────
765
828
  __publicField(this, "scrimTapToCloseEnabled", false);
766
829
  __publicField(this, "detachScrimTap", null);
767
- // ── Scrim overlay slot (positioned floating element) ────────────────
768
830
  __publicField(this, "scrimOverlayTeardown", null);
769
- // ── Hot-path opacity dedup caches (per-frame writes) ────────────────
770
- // Numeric sentinels migrated to WriteSentinel primitive — single
771
- // invalidation surface, shared with the engine's size/progress sentinels.
772
- // The two string-typed sentinels (`Pointer`, `Display`) keep their inline
773
- // pattern: equality on string union, no epsilon, different reset semantics.
774
831
  __publicField(this, "backdropOpacitySentinel", new WriteSentinel());
775
832
  __publicField(this, "screenOpacitySentinel", new WriteSentinel());
776
833
  __publicField(this, "lastBackdropPointer", "");
@@ -783,6 +840,7 @@ var ScrimController = class {
783
840
  this.getAllowedIds = deps.getAllowedIds;
784
841
  this.getResolvedSnaps = deps.getResolvedSnaps;
785
842
  this.snapToFn = deps.snapTo;
843
+ this.closeFn = deps.close;
786
844
  this.scrimMode = opts.scrimMode ?? "full";
787
845
  const preset = opts.scrimPreset ? SCRIM_PRESETS[opts.scrimPreset] : null;
788
846
  this.backdropRange = opts.backdropRange ?? preset?.range ?? [0, 1];
@@ -805,41 +863,15 @@ var ScrimController = class {
805
863
  }
806
864
  }
807
865
  }
808
- /**
809
- * Invalidate the opacity dedup caches. Called from any setter that mutates
810
- * range/mode/enabled — opacity is a pure function of (progress, range,
811
- * enabled), so a config change makes the cached values stale even if
812
- * `progress` itself didn't move. Next `applyOpacity(progress, false)` call
813
- * sees the -1 sentinel and re-runs the math instead of short-circuiting.
814
- *
815
- * Public for the engine: `setSnapPoints` mutates resolved geometry which
816
- * shifts `progress→opacity` mapping under the cache; engine calls this to
817
- * keep the next render frame correct on a static sheet.
818
- * @internal
819
- */
820
866
  invalidateOpacityCache() {
821
867
  this.backdropOpacitySentinel.invalidate();
822
868
  this.screenOpacitySentinel.invalidate();
823
869
  }
824
- /** Engine calls this when its `attach()` runs — installs scrim-tap-click if requested. */
825
870
  attach() {
826
871
  if (this.scrimTapToCloseEnabled) {
827
872
  this.setScrimTapToClose(true);
828
873
  }
829
874
  }
830
- // ── Hot-path: opacity computation called per-frame from engine.applySize ──
831
- /**
832
- * Per-frame opacity writer. Computes backdrop + screen opacity from the
833
- * sheet's progress (0..1 within allowed range), gated by sub-pixel dedup
834
- * caches so steady-state frames don't trigger style recalc.
835
- *
836
- * `progressChanged` is the engine's already-computed dedup gate — when
837
- * `false`, opacity is by construction a pure function of `progress` so
838
- * cached values are still valid; we short-circuit the Math.min/max +
839
- * division work entirely. Setters that mutate ranges/mode/enabled
840
- * invalidate the cache by writing -1 sentinels so the next call re-runs
841
- * the math.
842
- */
843
875
  applyOpacity(progress, progressChanged) {
844
876
  if (!progressChanged && this.backdropOpacitySentinel.value !== -1 && this.screenOpacitySentinel.value !== -1) {
845
877
  return;
@@ -885,80 +917,30 @@ var ScrimController = class {
885
917
  }
886
918
  }
887
919
  }
888
- // ── Public setters (engine forwards these 1:1) ─────────────────────
889
- /**
890
- * Replace the [start, end] progress window in which the backdrop fades 0→1.
891
- * @param range - Tuple of two progress values in 0..1 (start <= end).
892
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
893
- * @example
894
- * // Backdrop only starts darkening after the sheet crosses 30% open.
895
- * engine.setBackdropRange([0.3, 1]);
896
- */
897
920
  setBackdropRange(range, applySize) {
898
921
  if (this.isDestroyed()) return;
899
922
  this.backdropRange = range;
900
923
  this.invalidateOpacityCache();
901
924
  applySize();
902
925
  }
903
- /**
904
- * Replace the [start, end] progress window in which the screen scrim fades 0→1.
905
- * @param range - Tuple of two progress values in 0..1 (start <= end).
906
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
907
- * @example
908
- * // Scrim is fully opaque only when the sheet is past 50% — softer feel.
909
- * engine.setScreenRange([0, 0.5]);
910
- */
911
926
  setScreenRange(range, applySize) {
912
927
  if (this.isDestroyed()) return;
913
928
  this.screenRange = range;
914
929
  this.invalidateOpacityCache();
915
930
  applySize();
916
931
  }
917
- /**
918
- * Set the scrim background color. Pass `null`/`undefined` to clear the
919
- * inline write and let consumer CSS take over.
920
- * @param color - Any valid CSS color string, or `null`/`undefined` to clear.
921
- * @example
922
- * engine.setScrimColor("rgba(0, 0, 0, 0.6)");
923
- * engine.setScrimColor(null); // hand back to consumer CSS
924
- */
925
932
  setScrimColor(color) {
926
933
  if (this.isDestroyed()) return;
927
934
  this.applyScrimStyles({ color: color === void 0 ? null : color });
928
935
  }
929
- /**
930
- * Set the scrim backdrop-filter blur radius. Pass `null`/`undefined` to clear.
931
- * @param blur - CSS length (e.g. `"8px"`) consumed as `blur(<value>)`, or
932
- * `null`/`undefined` to remove the filter.
933
- * @example
934
- * engine.setScrimBlur("12px"); // frosted-glass effect
935
- * engine.setScrimBlur(null); // disable blur
936
- */
937
936
  setScrimBlur(blur) {
938
937
  if (this.isDestroyed()) return;
939
938
  this.applyScrimStyles({ blur: blur === void 0 ? null : blur });
940
939
  }
941
- /**
942
- * Toggle whether the scrim layer captures pointer events.
943
- * @param interactive - `true` → `pointer-events: auto`; `false` → `none`.
944
- * @example
945
- * engine.setScrimInteractive(true); // scrim swallows clicks
946
- * engine.setScrimInteractive(false); // clicks pass through to content below
947
- */
948
940
  setScrimInteractive(interactive) {
949
941
  if (this.isDestroyed()) return;
950
942
  this.applyScrimStyles({ interactive });
951
943
  }
952
- /**
953
- * Switch scrim positioning at runtime.
954
- * @param mode - `"full"` (consumer CSS), `"above-sheet"` (engine-pinned inset
955
- * so only the area NOT covered by the sheet is dimmed), or
956
- * `"off"` (display:none + opacity:0 — no remount needed).
957
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
958
- * @example
959
- * engine.setScrimMode("above-sheet"); // dim only the area above the sheet
960
- * engine.setScrimMode("off"); // disable scrim layer
961
- */
962
944
  setScrimMode(mode, applySize) {
963
945
  if (this.isDestroyed()) return;
964
946
  if (mode === this.scrimMode) return;
@@ -988,15 +970,6 @@ var ScrimController = class {
988
970
  }
989
971
  applySize();
990
972
  }
991
- /**
992
- * Enable or disable click-on-scrim → snap to first non-zero allowed snap.
993
- * Auto-promotes `pointer-events: auto` when scrim is non-interactive so the
994
- * click listener actually fires.
995
- * @param enabled - `true` installs the click listener; `false` removes it.
996
- * @example
997
- * engine.setScrimTapToClose(true); // tap outside sheet collapses it
998
- * engine.setScrimTapToClose(false); // require explicit close button
999
- */
1000
973
  setScrimTapToClose(enabled) {
1001
974
  if (this.isDestroyed()) return;
1002
975
  if (!this.screenComponent) return;
@@ -1009,11 +982,15 @@ var ScrimController = class {
1009
982
  }
1010
983
  const onClick = () => {
1011
984
  if (this.isDestroyed()) return;
985
+ if (this.closeFn) {
986
+ this.closeFn();
987
+ return;
988
+ }
1012
989
  const allowed = this.getAllowedIds();
1013
990
  const snaps = this.getResolvedSnaps();
1014
991
  const fallback = allowed.find((id) => {
1015
992
  const snap = findById(id, snaps);
1016
- return snap !== null && snap.size > 0;
993
+ return snap !== null && snap.size === 0;
1017
994
  });
1018
995
  if (fallback) this.snapToFn(fallback);
1019
996
  };
@@ -1026,20 +1003,11 @@ var ScrimController = class {
1026
1003
  this.scrimTapToCloseEnabled = false;
1027
1004
  }
1028
1005
  }
1029
- /**
1030
- * Toggle the scrim layer without remounting. Disabling stashes current
1031
- * `screenRange`/`backdropRange` and degenerates them to `[1, 1]` (opacity
1032
- * formula → 0 across the whole range); re-enabling restores the saved values.
1033
- * @param enabled - `false` hides scrim, `true` restores prior ranges.
1034
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
1035
- * @example
1036
- * engine.setScrimEnabled(false); // hide scrim, ranges remembered
1037
- * engine.setScrimEnabled(true); // restore prior ranges (no remount)
1038
- */
1039
1006
  setScrimEnabled(enabled, applySize) {
1040
1007
  if (this.isDestroyed()) return;
1041
1008
  if (enabled === this.scrimEnabled) return;
1042
1009
  this.scrimEnabled = enabled;
1010
+ this.invalidateOpacityCache();
1043
1011
  if (!enabled) {
1044
1012
  this.savedScrimRanges = {
1045
1013
  screen: this.screenRange,
@@ -1054,20 +1022,6 @@ var ScrimController = class {
1054
1022
  }
1055
1023
  applySize();
1056
1024
  }
1057
- /**
1058
- * Batch-update multiple scrim fields with a single applySize() at the end.
1059
- * If `preset` is set, it seeds color/blur/interactive/range first; explicit
1060
- * fields override.
1061
- * @param opts - Partial scrim config: preset, mode, enabled, tapToClose,
1062
- * color, blur, interactive, range.
1063
- * @param applySize - Engine-supplied recompute hook; runs ONCE per call.
1064
- * @example
1065
- * engine.setScrim({
1066
- * preset: "frosted",
1067
- * mode: "above-sheet",
1068
- * tapToClose: true,
1069
- * });
1070
- */
1071
1025
  setScrim(opts, applySize) {
1072
1026
  if (this.isDestroyed()) return;
1073
1027
  if (opts.preset) {
@@ -1077,7 +1031,12 @@ var ScrimController = class {
1077
1031
  blur: cfg.blur ?? null,
1078
1032
  interactive: cfg.interactive
1079
1033
  });
1080
- this.screenRange = cfg.range;
1034
+ if (!this.scrimEnabled && this.savedScrimRanges) {
1035
+ this.savedScrimRanges = { screen: cfg.range, backdrop: cfg.range };
1036
+ } else {
1037
+ this.screenRange = cfg.range;
1038
+ this.backdropRange = cfg.range;
1039
+ }
1081
1040
  }
1082
1041
  const noop = () => {
1083
1042
  };
@@ -1090,27 +1049,18 @@ var ScrimController = class {
1090
1049
  interactive: opts.interactive
1091
1050
  });
1092
1051
  if (opts.range !== void 0) {
1093
- this.screenRange = opts.range;
1052
+ if (!this.scrimEnabled && this.savedScrimRanges) {
1053
+ this.savedScrimRanges = {
1054
+ screen: opts.range,
1055
+ backdrop: this.savedScrimRanges.backdrop
1056
+ };
1057
+ } else {
1058
+ this.screenRange = opts.range;
1059
+ }
1094
1060
  }
1095
1061
  this.invalidateOpacityCache();
1096
1062
  applySize();
1097
1063
  }
1098
- /**
1099
- * Inject a positioned floating element into the scrim layer (e.g. a close
1100
- * button anchored top-right). Replaces any prior overlay so repeated calls
1101
- * (reactive effects re-running) don't accumulate wrappers.
1102
- * @param opts - Children element, optional `inset` (default `"16px"`),
1103
- * `position` (9-way grid, default `"top-right"`), and
1104
- * `interactive` flag (default `true`).
1105
- * @returns Teardown function that removes the wrapper from the DOM.
1106
- * @example
1107
- * const teardown = engine.setScrimOverlay({
1108
- * children: closeBtn,
1109
- * position: "top-right",
1110
- * inset: "20px",
1111
- * });
1112
- * // later: teardown(); // remove the floating button
1113
- */
1114
1064
  setScrimOverlay(opts) {
1115
1065
  if (this.isDestroyed()) return () => {
1116
1066
  };
@@ -1118,61 +1068,38 @@ var ScrimController = class {
1118
1068
  };
1119
1069
  this.scrimOverlayTeardown?.();
1120
1070
  const screen = this.screenComponent;
1121
- const inset = opts.inset ?? "16px";
1071
+ const insetRaw = opts.inset ?? "16px";
1072
+ const inset = VALID_CSS_LENGTH.test(insetRaw) ? insetRaw : "16px";
1073
+ if (inset !== insetRaw) {
1074
+ devWarn(
1075
+ `[bottom-sheet] setScrimOverlay: invalid inset ${JSON.stringify(insetRaw)}; expected a CSS length like "16px" / "1rem" / "5%". Falling back to "16px".`
1076
+ );
1077
+ }
1078
+ if (opts.children.ownerDocument && opts.children.ownerDocument !== screen.ownerDocument) {
1079
+ devWarn(
1080
+ "[bottom-sheet] setScrimOverlay: children belong to a different document than the scrim. Cross-document adoption can drop event listeners; construct children via the same document as the sheet root."
1081
+ );
1082
+ }
1122
1083
  const position = opts.position ?? "top-right";
1123
1084
  const interactive = opts.interactive ?? true;
1124
1085
  const wrapper = screen.ownerDocument.createElement("div");
1125
1086
  wrapper.className = "bs-scrim-overlay";
1126
1087
  const ws = wrapper.style;
1127
1088
  ws.position = "absolute";
1128
- switch (position) {
1129
- case "top-left":
1130
- ws.top = inset;
1131
- ws.left = inset;
1132
- break;
1133
- case "top-center":
1134
- ws.top = inset;
1135
- ws.left = "50%";
1136
- ws.transform = "translateX(-50%)";
1137
- break;
1138
- case "top-right":
1139
- ws.top = inset;
1140
- ws.right = inset;
1141
- break;
1142
- case "center-left":
1143
- ws.top = "50%";
1144
- ws.left = inset;
1145
- ws.transform = "translateY(-50%)";
1146
- break;
1147
- case "center":
1148
- ws.top = "50%";
1149
- ws.left = "50%";
1150
- ws.transform = "translate(-50%, -50%)";
1151
- break;
1152
- case "center-right":
1153
- ws.top = "50%";
1154
- ws.right = inset;
1155
- ws.transform = "translateY(-50%)";
1156
- break;
1157
- case "bottom-left":
1158
- ws.bottom = inset;
1159
- ws.left = inset;
1160
- break;
1161
- case "bottom-center":
1162
- ws.bottom = inset;
1163
- ws.left = "50%";
1164
- ws.transform = "translateX(-50%)";
1165
- break;
1166
- case "bottom-right":
1167
- ws.bottom = inset;
1168
- ws.right = inset;
1169
- break;
1170
- }
1089
+ applyOverlayPosition(ws, this.mode, position, inset);
1171
1090
  if (interactive) {
1172
1091
  ws.pointerEvents = "auto";
1173
1092
  }
1174
1093
  wrapper.appendChild(opts.children);
1175
- screen.appendChild(wrapper);
1094
+ const host = screen.parentElement;
1095
+ if (!host) {
1096
+ devWarn(
1097
+ "[bottom-sheet] setScrimOverlay: scrim element has no parent, skipping overlay injection. Mount the sheet root before calling setScrimOverlay."
1098
+ );
1099
+ return () => {
1100
+ };
1101
+ }
1102
+ host.appendChild(wrapper);
1176
1103
  const teardown = () => {
1177
1104
  if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
1178
1105
  if (this.scrimOverlayTeardown === teardown) {
@@ -1182,7 +1109,6 @@ var ScrimController = class {
1182
1109
  this.scrimOverlayTeardown = teardown;
1183
1110
  return teardown;
1184
1111
  }
1185
- /** Tear down toggle-state listeners + remove any injected overlay wrapper. */
1186
1112
  destroy() {
1187
1113
  this.detachScrimTap?.();
1188
1114
  this.detachScrimTap = null;
@@ -1201,8 +1127,6 @@ var ScrimController = class {
1201
1127
  this.lastBackdropPointer = "";
1202
1128
  this.lastScreenDisplay = "";
1203
1129
  }
1204
- // ── Private helpers ────────────────────────────────────────────────
1205
- /** Centralized scrim style writer — atomic 3-property write across all setters. */
1206
1130
  applyScrimStyles(opts) {
1207
1131
  if (!this.screenComponent) return;
1208
1132
  const s = this.screenComponent.style;
@@ -1221,10 +1145,10 @@ var ScrimController = class {
1221
1145
  }
1222
1146
  }
1223
1147
  if (opts.interactive !== void 0) {
1148
+ this.scrimInteractive = opts.interactive;
1224
1149
  s.pointerEvents = opts.interactive ? "auto" : "none";
1225
1150
  }
1226
1151
  }
1227
- /** Pin the screen overlay to the area NOT occupied by the sheet. */
1228
1152
  applyAboveSheetInset() {
1229
1153
  if (!this.screenComponent) return;
1230
1154
  const insetByMode = {
@@ -1236,7 +1160,7 @@ var ScrimController = class {
1236
1160
  const s = this.screenComponent.style;
1237
1161
  s.position = "fixed";
1238
1162
  s.inset = insetByMode[this.mode];
1239
- s.pointerEvents = "none";
1163
+ s.pointerEvents = this.scrimInteractive || this.scrimTapToCloseEnabled ? "auto" : "none";
1240
1164
  }
1241
1165
  };
1242
1166
 
@@ -1290,6 +1214,10 @@ var tween = ({
1290
1214
  promise
1291
1215
  };
1292
1216
  };
1217
+ var prefersReducedMotion = () => {
1218
+ if (typeof window === "undefined" || !window.matchMedia) return false;
1219
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1220
+ };
1293
1221
 
1294
1222
  // src/core/animation/spring.ts
1295
1223
  var DEFAULT_SPRING = {
@@ -1320,14 +1248,20 @@ var runSpring = ({
1320
1248
  if (cancelled) return;
1321
1249
  const dt = Math.min((now - lastTime) / 1e3, MAX_DT);
1322
1250
  lastTime = now;
1323
- const subDt = Math.min(dt, 1 / 240);
1324
- let stepsLeft = Math.max(1, Math.ceil(dt / subDt));
1251
+ const steps = Math.max(1, Math.ceil(dt * 240));
1252
+ const subDt = dt / steps;
1253
+ let stepsLeft = steps;
1325
1254
  while (stepsLeft-- > 0) {
1326
1255
  const force = -cfg.stiffness * (x - to) - cfg.damping * v;
1327
1256
  const accel = force / cfg.mass;
1328
1257
  v += accel * subDt;
1329
1258
  x += v * subDt;
1330
1259
  }
1260
+ if (!Number.isFinite(x) || !Number.isFinite(v)) {
1261
+ onUpdate(to, 0);
1262
+ resolve();
1263
+ return;
1264
+ }
1331
1265
  onUpdate(x, v);
1332
1266
  const isAtRest = Math.abs(to - x) < (cfg.restDelta ?? 0.5) && Math.abs(v) < (cfg.restSpeed ?? 0.5);
1333
1267
  if (isAtRest) {
@@ -1391,13 +1325,9 @@ var AnimationRunner = class {
1391
1325
  __publicField(this, "duration");
1392
1326
  __publicField(this, "easing");
1393
1327
  __publicField(this, "respectReducedMotion");
1394
- /** Live-tracked via mq listener so animateTo skips matchMedia per snap. */
1395
1328
  __publicField(this, "reducedMotion", false);
1396
- /** Disposes the matchMedia change listener — drained from destroy(). */
1397
1329
  __publicField(this, "detachReducedMotion", null);
1398
- /** Computed once — both inputs (option + API presence) stable for life. */
1399
1330
  __publicField(this, "viewTransitionsAvailable");
1400
- /** In-flight handles. Public for tests / debugging only — engine drives via cancel(). */
1401
1331
  __publicField(this, "currentTween", null);
1402
1332
  __publicField(this, "currentSpring", null);
1403
1333
  this.element = deps.element;
@@ -1428,26 +1358,13 @@ var AnimationRunner = class {
1428
1358
  }
1429
1359
  }
1430
1360
  }
1431
- /**
1432
- * Live derived flag — `true` whenever a spring/tween handle is in flight.
1433
- * Reading the canonical handles (instead of mirroring into a separate
1434
- * boolean) eliminates the reduced-motion early-return latent bug where
1435
- * the flag stayed true forever, and removes the cross-controller
1436
- * setIsAnimating callback.
1437
- */
1438
1361
  get isAnimating() {
1439
1362
  return this.currentTween !== null || this.currentSpring !== null;
1440
1363
  }
1441
- /** Cancel any in-flight tween/spring without touching engine state. */
1442
1364
  cancel() {
1443
1365
  this.currentTween?.cancel();
1444
1366
  this.currentSpring?.cancel();
1445
1367
  }
1446
- /**
1447
- * Animate from current size to `target`. Resolves when the animation
1448
- * completes; resolves early if cancelled mid-flight (caller checks an
1449
- * abort signal post-await for cycle-invariant correctness).
1450
- */
1451
1368
  async animateTo(target, velocityPxPerMs) {
1452
1369
  if (target === this.getSize()) return;
1453
1370
  this.currentTween?.cancel();
@@ -1488,7 +1405,6 @@ var AnimationRunner = class {
1488
1405
  if (!this.isDragging()) this.element.style.willChange = "auto";
1489
1406
  }
1490
1407
  }
1491
- /** Tear down the matchMedia listener. Engine calls from destroy(). */
1492
1408
  destroy() {
1493
1409
  this.detachReducedMotion?.();
1494
1410
  this.detachReducedMotion = null;
@@ -1520,9 +1436,13 @@ var isVisible = (el) => {
1520
1436
  var focusables = (root2) => Array.from(root2.querySelectorAll(FOCUSABLE)).filter(
1521
1437
  (el) => !el.hasAttribute("inert") && isVisible(el) && !el.matches("[aria-hidden='true']")
1522
1438
  );
1439
+ var trapStack = [];
1523
1440
  var installFocusTrap = (container, options = {}) => {
1524
1441
  if (typeof document === "undefined") return () => {
1525
1442
  };
1443
+ const token = {};
1444
+ trapStack.push(token);
1445
+ const isActive = () => trapStack[trapStack.length - 1] === token;
1526
1446
  const previouslyFocused = document.activeElement;
1527
1447
  const focusInitial = () => {
1528
1448
  const initial = typeof options.initialFocus === "string" ? container.querySelector(options.initialFocus) : options.initialFocus ?? focusables(container)[0] ?? container;
@@ -1530,6 +1450,7 @@ var installFocusTrap = (container, options = {}) => {
1530
1450
  };
1531
1451
  focusInitial();
1532
1452
  const handleKey = (e) => {
1453
+ if (!isActive()) return;
1533
1454
  if (e.key === "Escape" && options.onEscape) {
1534
1455
  options.onEscape();
1535
1456
  return;
@@ -1552,6 +1473,7 @@ var installFocusTrap = (container, options = {}) => {
1552
1473
  }
1553
1474
  };
1554
1475
  const handleFocusIn = (e) => {
1476
+ if (!isActive()) return;
1555
1477
  const target = e.target;
1556
1478
  if (target && !container.contains(target)) {
1557
1479
  const list = focusables(container);
@@ -1564,6 +1486,8 @@ var installFocusTrap = (container, options = {}) => {
1564
1486
  return () => {
1565
1487
  if (released) return;
1566
1488
  released = true;
1489
+ const idx = trapStack.indexOf(token);
1490
+ if (idx !== -1) trapStack.splice(idx, 1);
1567
1491
  document.removeEventListener("keydown", handleKey, true);
1568
1492
  document.removeEventListener("focusin", handleFocusIn, true);
1569
1493
  if (previouslyFocused && document.contains(previouslyFocused)) {
@@ -1619,11 +1543,17 @@ function createInertSiblings(rootProvider) {
1619
1543
  return {
1620
1544
  apply() {
1621
1545
  if (typeof document === "undefined") return;
1622
- const root2 = rootProvider();
1623
- const parent = root2.parentElement ?? document.body;
1546
+ let top = rootProvider();
1547
+ while (top.parentElement && top.parentElement !== document.body) {
1548
+ top = top.parentElement;
1549
+ }
1550
+ const parent = top.parentElement ?? document.body;
1624
1551
  for (const child2 of Array.from(parent.children)) {
1625
- if (child2 === root2) continue;
1552
+ if (child2 === top) continue;
1626
1553
  if (child2.hasAttribute("inert")) continue;
1554
+ if (child2.classList.contains("bs-backdrop") || child2.classList.contains("bs-screen") || child2.classList.contains("bs-root")) {
1555
+ continue;
1556
+ }
1627
1557
  child2.setAttribute("inert", "");
1628
1558
  tracked.push(child2);
1629
1559
  }
@@ -1639,7 +1569,6 @@ function createInertSiblings(rootProvider) {
1639
1569
  var LifecycleController = class {
1640
1570
  constructor(deps, opts) {
1641
1571
  __publicField(this, "element");
1642
- __publicField(this, "closeFn");
1643
1572
  __publicField(this, "focusTrapEnabled");
1644
1573
  __publicField(this, "initialFocus");
1645
1574
  __publicField(this, "closeOnEscape");
@@ -1648,16 +1577,11 @@ var LifecycleController = class {
1648
1577
  __publicField(this, "inertSiblings");
1649
1578
  __publicField(this, "shouldApplyInertSiblings");
1650
1579
  __publicField(this, "returnFocus");
1651
- /** Toggle-state teardowns — fire on close, NOT in destroy-only stack. */
1652
1580
  __publicField(this, "releaseFocusTrap", null);
1653
1581
  __publicField(this, "releaseScrollLock", null);
1654
- /** True between install() and release(). Guards release()/destroy() from
1655
- * re-running side effects (notably `returnFocus.focus()`) on already-
1656
- * released or never-installed instances. */
1657
1582
  __publicField(this, "installed", false);
1658
1583
  __publicField(this, "destroyed", false);
1659
1584
  this.element = deps.element;
1660
- this.closeFn = deps.close;
1661
1585
  this.focusTrapEnabled = opts.focusTrap ?? false;
1662
1586
  this.initialFocus = opts.initialFocus;
1663
1587
  this.closeOnEscape = opts.closeOnEscape ?? true;
@@ -1667,14 +1591,9 @@ var LifecycleController = class {
1667
1591
  this.returnFocus = opts.returnFocus;
1668
1592
  this.inertSiblings = createInertSiblings(() => this.element);
1669
1593
  }
1670
- /**
1671
- * Run the open-time install steps. Idempotent: each step checks its
1672
- * existing teardown ref so a re-open during a half-cancelled close
1673
- * doesn't double-install. Sets `installed=true` BEFORE the first side
1674
- * effect so a partial throw (detached / shadow-DOM target) leaves the
1675
- * caller's `release()` rollback path armed to undo whatever did succeed.
1676
- * No-op when destroyed.
1677
- */
1594
+ get isInstalled() {
1595
+ return this.installed;
1596
+ }
1678
1597
  install() {
1679
1598
  if (this.destroyed) return;
1680
1599
  this.installed = true;
@@ -1683,30 +1602,17 @@ var LifecycleController = class {
1683
1602
  }
1684
1603
  if (this.focusTrapEnabled && !this.releaseFocusTrap) {
1685
1604
  this.releaseFocusTrap = installFocusTrap(this.element, {
1686
- initialFocus: this.initialFocus,
1687
- onEscape: this.closeOnEscape ? () => void this.closeFn() : void 0
1605
+ initialFocus: this.initialFocus
1688
1606
  });
1689
1607
  }
1690
1608
  if (this.inertSiblingsEnabled && this.shouldApplyInertSiblings()) {
1691
1609
  this.inertSiblings.apply();
1692
1610
  }
1693
1611
  }
1694
- /**
1695
- * Update the post-release focus target. Overlay re-binds this when the
1696
- * consumer changes `returnFocus` after construction; the new value applies
1697
- * on the next `release()`. No-op when destroyed.
1698
- */
1699
1612
  setReturnFocus(target) {
1700
1613
  if (this.destroyed) return;
1701
1614
  this.returnFocus = target;
1702
1615
  }
1703
- /**
1704
- * Release all install-time teardowns. Short-circuits when not installed,
1705
- * so `close() → destroy()` (each routing through here) only restores
1706
- * focus once. The `installed` guard lets `inertSiblings.remove()` and
1707
- * the focus-trap/scroll-lock teardown nullings stay simple — they were
1708
- * already idempotent, but `returnFocus.focus()` was firing on every call.
1709
- */
1710
1616
  release() {
1711
1617
  if (!this.installed) return;
1712
1618
  this.installed = false;
@@ -1720,12 +1626,6 @@ var LifecycleController = class {
1720
1626
  target?.focus?.();
1721
1627
  }
1722
1628
  }
1723
- /**
1724
- * Final teardown — releases any still-installed listeners and seals the
1725
- * controller. After destroy(), install() is a no-op and setReturnFocus()
1726
- * is rejected, so post-destroy mutations can't queue up a stale focus
1727
- * restore.
1728
- */
1729
1629
  destroy() {
1730
1630
  this.release();
1731
1631
  this.destroyed = true;
@@ -1735,12 +1635,10 @@ var LifecycleController = class {
1735
1635
  // src/core/gestures.ts
1736
1636
  var isAxisVertical = (mode) => mode === "bottom" || mode === "top";
1737
1637
  var SampleRing = class {
1738
- // current count, ≤ capacity
1739
1638
  constructor(capacity) {
1740
1639
  __publicField(this, "slots");
1741
1640
  __publicField(this, "capacity");
1742
1641
  __publicField(this, "head", 0);
1743
- // next write index
1744
1642
  __publicField(this, "size", 0);
1745
1643
  this.capacity = capacity;
1746
1644
  this.slots = new Array(capacity);
@@ -1753,12 +1651,6 @@ var SampleRing = class {
1753
1651
  this.head = (this.head + 1) % this.capacity;
1754
1652
  if (this.size < this.capacity) this.size++;
1755
1653
  }
1756
- /**
1757
- * Drop oldest entries whose timestamp is below `cutoff`, but never shrink
1758
- * below `minKeep`. Mirrors the original
1759
- * `while (samples.length > minKeep && samples[0].t < cutoff) samples.shift()`
1760
- * but without copying — just advances the logical tail.
1761
- */
1762
1654
  pruneBefore(cutoff, minKeep) {
1763
1655
  while (this.size > minKeep) {
1764
1656
  const oldestIdx = (this.head - this.size + this.capacity) % this.capacity;
@@ -1864,10 +1756,11 @@ var installGestures = (handle, mode, callbacks) => {
1864
1756
  };
1865
1757
 
1866
1758
  // src/core/primitives/rubber-band.ts
1867
- var RUBBER_BAND_FACTOR = 0.55;
1868
1759
  function rubberBand(overshoot, maxAxisSize) {
1869
1760
  const cap = Math.min(maxAxisSize * 0.15, 80);
1870
- return Math.sign(overshoot) * Math.pow(Math.abs(overshoot), RUBBER_BAND_FACTOR) * Math.min(6, cap / Math.max(Math.abs(overshoot), 1));
1761
+ if (cap <= 0) return 0;
1762
+ const abs = Math.abs(overshoot);
1763
+ return Math.sign(overshoot) * (cap * abs / (abs + cap));
1871
1764
  }
1872
1765
 
1873
1766
  // src/core/features/soft-keyboard.ts
@@ -1894,28 +1787,14 @@ var GestureController = class {
1894
1787
  __publicField(this, "detach", null);
1895
1788
  __publicField(this, "isDragging_", false);
1896
1789
  __publicField(this, "dragStartSize", 0);
1897
- // Only "touch" gestures are eligible for soft-keyboard dismissal —
1898
- // mouse/trackpad users don't have a keyboard to dismiss.
1899
1790
  __publicField(this, "currentPointerKind", "touch");
1900
- // One blur per drag — flipping focus every move event races the keyboard's
1901
- // own animations on iOS and makes the page jitter.
1902
1791
  __publicField(this, "keyboardDismissed", false);
1903
- // Reused payload object for the "drag" event — mutated each onMove instead
1904
- // of allocating a fresh `{size, delta}` per frame. At 60-120Hz a long drag
1905
- // would otherwise churn tens of KB through the GC. Mirrors browser
1906
- // PointerEvent semantics: consumers must NOT retain the reference across
1907
- // frames (clone if needed). See SheetEventMap.drag JSDoc.
1908
1792
  __publicField(this, "dragPayload", { size: 0, delta: 0 });
1909
1793
  this.deps = deps;
1910
1794
  }
1911
- /** True between `dragstart` and `dragend`/`onCancel`. */
1912
1795
  get isDragging() {
1913
1796
  return this.isDragging_;
1914
1797
  }
1915
- /**
1916
- * Bind pointer listeners on the handle and return a teardown. Engine pushes
1917
- * the teardown into its TeardownStack so destroy() drains it once.
1918
- */
1919
1798
  install() {
1920
1799
  if (this.detach) return this.detach;
1921
1800
  this.detach = installGestures(this.deps.handle, this.deps.mode, {
@@ -1966,17 +1845,16 @@ var GestureController = class {
1966
1845
  this.isDragging_ = false;
1967
1846
  this.keyboardDismissed = false;
1968
1847
  this.deps.getRoot()?.removeAttribute("data-dragging");
1848
+ this.deps.emit("dragend", {
1849
+ size: this.deps.getDragContext().size,
1850
+ velocity: 0
1851
+ });
1969
1852
  const restoreTo = this.dragStartSize;
1970
1853
  void this.deps.animateTo(restoreTo, 0);
1971
1854
  }
1972
1855
  });
1973
1856
  return this.detach;
1974
1857
  }
1975
- /**
1976
- * Force-clear drag visual state. Engine calls from destroy() when destroyed
1977
- * mid-drag — gesture detach won't synthesize an onCancel, so we have to
1978
- * roll back the data-attribute + will-change ourselves.
1979
- */
1980
1858
  forceClearDragState() {
1981
1859
  if (!this.isDragging_) return;
1982
1860
  this.isDragging_ = false;
@@ -1995,11 +1873,6 @@ var AriaSliderWriter = class {
1995
1873
  mode === "bottom" || mode === "top" ? "vertical" : "horizontal"
1996
1874
  );
1997
1875
  }
1998
- /**
1999
- * Update slider position. `allowedIds` length determines `aria-valuemax`;
2000
- * `activeId` indexes into it for `aria-valuenow` + `aria-valuetext`.
2001
- * Empty list → strip every dynamic attr so AT skips the handle entirely.
2002
- */
2003
1876
  setValue(allowedIds, activeId) {
2004
1877
  if (allowedIds.length === 0) {
2005
1878
  this.handle.removeAttribute("aria-valuemin");
@@ -2049,6 +1922,287 @@ function createScrollCache(deps) {
2049
1922
  };
2050
1923
  }
2051
1924
 
1925
+ // src/core/primitives/anchor-animations.ts
1926
+ var DEFAULT_DURATION3 = 200;
1927
+ var DEFAULT_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
1928
+ var PRESET_FRAMES = {
1929
+ fade: {
1930
+ enter: [{ opacity: 0 }, { opacity: 1 }],
1931
+ exit: [{ opacity: 1 }, { opacity: 0 }]
1932
+ },
1933
+ scale: {
1934
+ enter: [
1935
+ { opacity: 0, transform: "scale(0.85)" },
1936
+ { opacity: 1, transform: "scale(1)" }
1937
+ ],
1938
+ exit: [
1939
+ { opacity: 1, transform: "scale(1)" },
1940
+ { opacity: 0, transform: "scale(0.85)" }
1941
+ ]
1942
+ },
1943
+ slide: {
1944
+ enter: [
1945
+ { opacity: 0, transform: "translateY(10px)" },
1946
+ { opacity: 1, transform: "translateY(0)" }
1947
+ ],
1948
+ exit: [
1949
+ { opacity: 1, transform: "translateY(0)" },
1950
+ { opacity: 0, transform: "translateY(10px)" }
1951
+ ]
1952
+ },
1953
+ pop: {
1954
+ enter: [
1955
+ { opacity: 0, transform: "scale(0.6)", offset: 0 },
1956
+ { opacity: 1, transform: "scale(1.06)", offset: 0.7 },
1957
+ { opacity: 1, transform: "scale(1)", offset: 1 }
1958
+ ],
1959
+ exit: [
1960
+ { opacity: 1, transform: "scale(1)" },
1961
+ { opacity: 0, transform: "scale(0.6)" }
1962
+ ]
1963
+ }
1964
+ };
1965
+ var INSTANT = {
1966
+ finished: Promise.resolve(),
1967
+ cancel: () => {
1968
+ }
1969
+ };
1970
+ function resolveTransitionFrames(spec, phase) {
1971
+ const fallback = { frames: null, duration: 0, easing: DEFAULT_EASING };
1972
+ if (spec === "none") return fallback;
1973
+ if (spec === void 0 || typeof spec === "string") {
1974
+ const preset2 = PRESET_FRAMES[spec ?? "fade"];
1975
+ return {
1976
+ frames: preset2[phase],
1977
+ duration: DEFAULT_DURATION3,
1978
+ easing: DEFAULT_EASING
1979
+ };
1980
+ }
1981
+ const custom = phase === "enter" ? spec.enter : spec.exit;
1982
+ if (custom) {
1983
+ return {
1984
+ frames: custom,
1985
+ duration: spec.duration ?? DEFAULT_DURATION3,
1986
+ easing: spec.easing ?? DEFAULT_EASING
1987
+ };
1988
+ }
1989
+ if (spec.preset === "none") return fallback;
1990
+ const preset = PRESET_FRAMES[spec.preset ?? "fade"];
1991
+ return {
1992
+ frames: preset[phase],
1993
+ duration: spec.duration ?? DEFAULT_DURATION3,
1994
+ easing: spec.easing ?? DEFAULT_EASING
1995
+ };
1996
+ }
1997
+ function runAnchorTransition(el, spec, phase) {
1998
+ const respectReduced = typeof spec === "object" ? spec.respectReducedMotion !== false : true;
1999
+ if (respectReduced && prefersReducedMotion()) return INSTANT;
2000
+ const { frames, duration, easing } = resolveTransitionFrames(spec, phase);
2001
+ if (!frames || duration <= 0) return INSTANT;
2002
+ if (typeof el.animate !== "function") return INSTANT;
2003
+ const animation = el.animate(frames, {
2004
+ duration,
2005
+ easing,
2006
+ fill: "both"
2007
+ });
2008
+ const finished = animation.finished.then(() => void 0).catch(() => void 0);
2009
+ return {
2010
+ finished,
2011
+ cancel: () => {
2012
+ try {
2013
+ animation.cancel();
2014
+ } catch {
2015
+ return;
2016
+ }
2017
+ }
2018
+ };
2019
+ }
2020
+
2021
+ // src/core/features/sheet-anchors.ts
2022
+ function attachAnchor(deps, opts) {
2023
+ const doc = deps.host.ownerDocument;
2024
+ const wrapper = doc.createElement("div");
2025
+ wrapper.className = "bs-anchor";
2026
+ const interactive = opts.interactive ?? true;
2027
+ const position = opts.position ?? "sheet-top-right";
2028
+ const isDock = position === "dock-bottom" || position === "dock-top";
2029
+ const ws = wrapper.style;
2030
+ ws.position = "fixed";
2031
+ if (isDock) {
2032
+ ws.left = "0";
2033
+ ws.right = "0";
2034
+ if (position === "dock-bottom") {
2035
+ ws.bottom = "0";
2036
+ } else {
2037
+ ws.top = "0";
2038
+ }
2039
+ } else {
2040
+ applyOverlayPosition(ws, deps.mode, position, opts.inset ?? "16px");
2041
+ }
2042
+ if (opts.fadeRange) {
2043
+ const [r0, r1] = opts.fadeRange;
2044
+ const span = Math.max(r1 - r0, 1e-4);
2045
+ ws.opacity = `clamp(0, calc((var(--bs-progress, 0) - ${r0}) / ${span}), 1)`;
2046
+ }
2047
+ wrapper.appendChild(opts.element);
2048
+ deps.host.appendChild(wrapper);
2049
+ let visible = null;
2050
+ let inFlight = null;
2051
+ const shouldShow = (state2) => {
2052
+ if (typeof opts.showOn === "function") return opts.showOn(state2);
2053
+ if (Array.isArray(opts.showOn)) return opts.showOn.includes(state2.activeId);
2054
+ return isDock || state2.size > 0;
2055
+ };
2056
+ const applyVisibility = (next, animate) => {
2057
+ if (next === visible) return;
2058
+ visible = next;
2059
+ inFlight?.cancel();
2060
+ inFlight = null;
2061
+ wrapper.style.pointerEvents = next && interactive ? "auto" : "none";
2062
+ if (next) {
2063
+ wrapper.style.visibility = "";
2064
+ if (animate) {
2065
+ inFlight = runAnchorTransition(opts.element, opts.animation, "enter");
2066
+ }
2067
+ return;
2068
+ }
2069
+ if (!animate) {
2070
+ wrapper.style.visibility = "hidden";
2071
+ return;
2072
+ }
2073
+ const handle = runAnchorTransition(opts.element, opts.animation, "exit");
2074
+ inFlight = handle;
2075
+ void handle.finished.then(() => {
2076
+ if (inFlight === handle && visible === false) {
2077
+ wrapper.style.visibility = "hidden";
2078
+ }
2079
+ });
2080
+ };
2081
+ const evaluate = (animate) => {
2082
+ if (deps.isDestroyed()) return;
2083
+ applyVisibility(shouldShow(deps.getState()), animate);
2084
+ };
2085
+ const offs = [
2086
+ deps.on("snap", () => evaluate(true)),
2087
+ deps.on("open", () => evaluate(true)),
2088
+ deps.on("close", () => evaluate(true))
2089
+ ];
2090
+ evaluate(false);
2091
+ return {
2092
+ wrapper,
2093
+ detach: () => {
2094
+ offs.forEach((off) => off());
2095
+ inFlight?.cancel();
2096
+ inFlight = null;
2097
+ if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
2098
+ },
2099
+ syncZ: (z) => {
2100
+ wrapper.style.zIndex = String(z);
2101
+ }
2102
+ };
2103
+ }
2104
+
2105
+ // src/core/features/scrim-stages.ts
2106
+ var matchesId = (def, activeId) => {
2107
+ if (def.for === void 0) return false;
2108
+ return Array.isArray(def.for) ? def.for.includes(activeId) : def.for === activeId;
2109
+ };
2110
+ var matchesRange = (def, progress) => {
2111
+ if (!def.forRange) return false;
2112
+ const [from, to] = def.forRange;
2113
+ return progress >= from && progress <= to;
2114
+ };
2115
+ function installScrimStages(deps, opts) {
2116
+ const doc = deps.host.ownerDocument;
2117
+ const entries = opts.stages.map((def) => {
2118
+ const wrapper = doc.createElement("div");
2119
+ wrapper.className = "bs-scrim-stage";
2120
+ const ws = wrapper.style;
2121
+ ws.position = "absolute";
2122
+ applyOverlayPosition(
2123
+ ws,
2124
+ deps.mode,
2125
+ def.position ?? opts.position ?? "center",
2126
+ def.inset ?? opts.inset ?? "16px"
2127
+ );
2128
+ ws.visibility = "hidden";
2129
+ ws.pointerEvents = "none";
2130
+ wrapper.appendChild(def.element);
2131
+ deps.host.appendChild(wrapper);
2132
+ return { def, wrapper, inFlight: null };
2133
+ });
2134
+ let active = null;
2135
+ const resolveActive = () => {
2136
+ const state2 = deps.getState();
2137
+ if (state2.size === 0) {
2138
+ return entries.find((e) => matchesId(e.def, state2.activeId)) ?? null;
2139
+ }
2140
+ return entries.find((e) => matchesId(e.def, state2.activeId)) ?? entries.find((e) => matchesRange(e.def, state2.progress)) ?? null;
2141
+ };
2142
+ const hide = (entry, animate) => {
2143
+ entry.inFlight?.cancel();
2144
+ entry.wrapper.style.pointerEvents = "none";
2145
+ if (!animate) {
2146
+ entry.inFlight = null;
2147
+ entry.wrapper.style.visibility = "hidden";
2148
+ return;
2149
+ }
2150
+ const handle = runAnchorTransition(
2151
+ entry.def.element,
2152
+ entry.def.animation ?? opts.animation,
2153
+ "exit"
2154
+ );
2155
+ entry.inFlight = handle;
2156
+ void handle.finished.then(() => {
2157
+ if (entry.inFlight === handle && active !== entry) {
2158
+ entry.wrapper.style.visibility = "hidden";
2159
+ }
2160
+ });
2161
+ };
2162
+ const show = (entry, animate) => {
2163
+ entry.inFlight?.cancel();
2164
+ entry.inFlight = null;
2165
+ entry.wrapper.style.visibility = "";
2166
+ entry.wrapper.style.pointerEvents = entry.def.interactive ?? opts.interactive ?? false ? "auto" : "none";
2167
+ if (animate) {
2168
+ entry.inFlight = runAnchorTransition(
2169
+ entry.def.element,
2170
+ entry.def.animation ?? opts.animation,
2171
+ "enter"
2172
+ );
2173
+ }
2174
+ };
2175
+ const update = (animate) => {
2176
+ if (deps.isDestroyed()) return;
2177
+ const next = resolveActive();
2178
+ if (next === active) return;
2179
+ const prev = active;
2180
+ active = next;
2181
+ if (prev) hide(prev, animate);
2182
+ if (next) show(next, animate);
2183
+ };
2184
+ const offs = [
2185
+ deps.on("snap", () => update(true)),
2186
+ deps.on("open", () => update(true)),
2187
+ deps.on("close", () => update(true))
2188
+ ];
2189
+ if (entries.some((e) => e.def.forRange)) {
2190
+ offs.push(deps.on("progress", () => update(true)));
2191
+ }
2192
+ update(false);
2193
+ return () => {
2194
+ offs.forEach((off) => off());
2195
+ for (const entry of entries) {
2196
+ entry.inFlight?.cancel();
2197
+ entry.inFlight = null;
2198
+ if (entry.wrapper.parentNode) {
2199
+ entry.wrapper.parentNode.removeChild(entry.wrapper);
2200
+ }
2201
+ }
2202
+ active = null;
2203
+ };
2204
+ }
2205
+
2052
2206
  // src/core/primitives/engine-options.ts
2053
2207
  var DEFAULT_FLICK_VELOCITY = 0.65;
2054
2208
  var DEFAULT_DRAG_THRESHOLD = 18;
@@ -2073,9 +2227,6 @@ function resolveEngineOptions(opts, extras) {
2073
2227
  scrimMode: opts.scrimMode,
2074
2228
  scrimColor: opts.scrimColor,
2075
2229
  scrimBlur: opts.scrimBlur,
2076
- // Auto-promote pointer-events when tap-to-close is on so the click
2077
- // listener actually receives events. Caller can still force `false`
2078
- // explicitly to opt out — `??` only kicks in for `undefined`.
2079
2230
  scrimInteractive: opts.scrimInteractive ?? (opts.scrimTapToClose ? true : void 0),
2080
2231
  scrimTapToClose: opts.scrimTapToClose,
2081
2232
  scrimPreset: opts.scrimPreset,
@@ -2096,10 +2247,6 @@ function resolveEngineOptions(opts, extras) {
2096
2247
  closeOnEscape: opts.closeOnEscape,
2097
2248
  lockBodyScroll: opts.lockBodyScroll,
2098
2249
  inertSiblings: opts.inertSiblings,
2099
- // Folded in from `extras` so the lifecycle bag is fully assembled at
2100
- // the call site — engine no longer needs a spread+override at
2101
- // controller construction. When `extras` is omitted, the field stays
2102
- // undefined and the controller uses its own default predicate.
2103
2250
  shouldApplyInertSiblings: extras?.shouldApplyInertSiblings
2104
2251
  }
2105
2252
  };
@@ -2134,6 +2281,7 @@ var BottomSheetEngine = class {
2134
2281
  __publicField(this, "scrollContainer");
2135
2282
  __publicField(this, "backdrop");
2136
2283
  __publicField(this, "screenComponent");
2284
+ __publicField(this, "scrimParent", null);
2137
2285
  __publicField(this, "mode");
2138
2286
  __publicField(this, "flickVelocity");
2139
2287
  __publicField(this, "dragThreshold");
@@ -2144,40 +2292,22 @@ var BottomSheetEngine = class {
2144
2292
  __publicField(this, "lifecycle");
2145
2293
  __publicField(this, "closeOnBack");
2146
2294
  __publicField(this, "routedTo");
2147
- // Engine retains `snapPointsRaw` for the "find a closed-id" query in
2148
- // close() (raw input, not resolved geometry). Resolver owns geometry.
2149
2295
  __publicField(this, "snapPointsRaw");
2150
2296
  __publicField(this, "snaps");
2151
2297
  __publicField(this, "activeId");
2152
2298
  __publicField(this, "size", 0);
2153
- // Constructed in attach(); a plugin's install() could read `state` before
2154
- // attach completes if the constructor throws partway through. State getter
2155
- // null-guards via `this.gesture?.isDragging ?? false`.
2156
2299
  __publicField(this, "gesture");
2157
2300
  __publicField(this, "rootEl", null);
2158
2301
  __publicField(this, "destroyed", false);
2159
- // In-flight ViewTransition handle. A second snapTo while a VT is in flight
2160
- // would otherwise let Chromium reject the second's promise and flicker —
2161
- // newCycle() calls skipTransition() to abort cleanly.
2162
2302
  __publicField(this, "currentViewTransition", null);
2163
- // Each cycle captures the signal at entry; when a newer cycle aborts the
2164
- // controller, the captured signal flips to `aborted=true` and post-await
2165
- // guards short-circuit (prevents stale snap/open/close emits).
2166
2303
  __publicField(this, "currentAbort", new AbortController());
2167
- // Sub-pixel `--bs-size` deltas and <1% `--bs-progress` deltas are not
2168
- // visually distinguishable; gating CSSOM writes drops 30-50% of writes
2169
- // during the spring's settle tail.
2170
2304
  __publicField(this, "sizeWriteSentinel", new WriteSentinel());
2171
2305
  __publicField(this, "progressWriteSentinel", new WriteSentinel());
2172
2306
  __publicField(this, "isTopSheet", true);
2173
- // Pooled payload — identity documented in SheetEventMap.progress JSDoc;
2174
- // consumers must not retain across frames.
2175
2307
  __publicField(this, "progressPayload", {
2176
2308
  value: 0,
2177
2309
  size: 0
2178
2310
  });
2179
- // Pooled buffer for `getDragContext()` — onMove calls this 60-120×/s.
2180
- // Inner `range` object identity is also stable (mutated in place).
2181
2311
  __publicField(this, "dragContextBuf", {
2182
2312
  size: 0,
2183
2313
  maxAxisSize: 0,
@@ -2188,13 +2318,13 @@ var BottomSheetEngine = class {
2188
2318
  __publicField(this, "persistKey");
2189
2319
  __publicField(this, "linkedSheets", []);
2190
2320
  __publicField(this, "scrollCache");
2191
- // Single LIFO stack for ALL destroy-only teardowns. Plugins (added last)
2192
- // tear down first before the infrastructure they depend on. Toggle-semantic
2193
- // listeners (focus-trap / scroll-lock / scrim tap-to-close) stay as named
2194
- // fields on their controllers.
2195
2321
  __publicField(this, "teardowns", new TeardownStack());
2196
- // Pre-computed transform template — built once per engine since `mode` is
2197
- // fixed for its lifetime. Avoids the 4-arm switch at 60-120 Hz.
2322
+ __publicField(this, "anchors", []);
2323
+ __publicField(this, "anchorHost", null);
2324
+ __publicField(this, "stackZ", 100);
2325
+ __publicField(this, "detachScrimStages", null);
2326
+ __publicField(this, "stackEffectEnabled", false);
2327
+ __publicField(this, "stackEffectPrimed", false);
2198
2328
  __publicField(this, "transformTemplate");
2199
2329
  const resolved = resolveEngineOptions(opts, {
2200
2330
  shouldApplyInertSiblings: () => {
@@ -2209,7 +2339,8 @@ var BottomSheetEngine = class {
2209
2339
  this.handle = opts.handle ?? opts.element;
2210
2340
  this.scrollContainer = opts.scrollContainer;
2211
2341
  this.backdrop = opts.backdrop;
2212
- this.screenComponent = opts.screenComponent;
2342
+ this.screenComponent = opts.scrim ?? opts.screenComponent;
2343
+ this.scrimParent = this.screenComponent?.parentElement ?? null;
2213
2344
  this.mode = resolved.mode;
2214
2345
  this.transformTemplate = buildTransformTemplate(this.mode);
2215
2346
  this.aria = new AriaSliderWriter(this.handle, this.mode);
@@ -2227,6 +2358,9 @@ var BottomSheetEngine = class {
2227
2358
  getResolvedSnaps: () => this.snaps.getResolvedSnaps().slice(),
2228
2359
  snapTo: (id) => {
2229
2360
  void this.snapTo(id);
2361
+ },
2362
+ close: () => {
2363
+ void this.close();
2230
2364
  }
2231
2365
  },
2232
2366
  resolved.scrim
@@ -2237,8 +2371,6 @@ var BottomSheetEngine = class {
2237
2371
  getRootEl: () => this.rootEl,
2238
2372
  applySize: (size) => this.applySize(size),
2239
2373
  getSize: () => this.size,
2240
- // Optional chain protects synthetic test paths that drive animateTo
2241
- // before attach() (gesture is constructed there).
2242
2374
  isDragging: () => this.gesture?.isDragging ?? false
2243
2375
  },
2244
2376
  resolved.animation
@@ -2252,6 +2384,7 @@ var BottomSheetEngine = class {
2252
2384
  );
2253
2385
  this.closeOnBack = resolved.closeOnBack;
2254
2386
  this.routedTo = opts.routedTo;
2387
+ this.stackEffectEnabled = opts.stackEffect ?? false;
2255
2388
  this.snapPointsRaw = opts.snapPoints;
2256
2389
  this.persistKey = opts.persistKey;
2257
2390
  if (opts.linkedSheets) {
@@ -2264,7 +2397,7 @@ var BottomSheetEngine = class {
2264
2397
  this.snapPointsRaw,
2265
2398
  resolved.initialAllowed,
2266
2399
  this.mode,
2267
- () => this.handle.offsetHeight,
2400
+ () => layoutAxis(this.mode) === "width" ? this.handle.offsetWidth : this.handle.offsetHeight,
2268
2401
  (maxAxisSize) => {
2269
2402
  this.element.style[layoutAxis(this.mode)] = `${maxAxisSize}px`;
2270
2403
  }
@@ -2343,21 +2476,6 @@ var BottomSheetEngine = class {
2343
2476
  on(event, fn) {
2344
2477
  return this.bus.on(event, fn);
2345
2478
  }
2346
- /**
2347
- * Register a plugin. `install` runs synchronously; if it returns a function,
2348
- * it's captured for teardown during `destroy()`. No-op if already destroyed
2349
- * — installing into a torn-down engine would leak listeners.
2350
- *
2351
- * Throws from `plugin.install` are caught and rethrown via `queueMicrotask`
2352
- * so a buggy plugin can't take the engine down with it: the engine stays
2353
- * usable, sibling plugins still install, and the error surfaces in dev tools
2354
- * on the next tick instead of being swallowed. Mirrors the per-fn isolation
2355
- * model used for teardowns.
2356
- *
2357
- * Plugins that accept the second `scope` argument get transactional cleanup:
2358
- * push partial-install cleanups via `scope.add(fn)` and the engine drains
2359
- * them on throw, OR merges them into destroy-time teardowns on success.
2360
- */
2361
2479
  use(plugin) {
2362
2480
  if (this.destroyed) {
2363
2481
  console.warn(
@@ -2367,9 +2485,6 @@ var BottomSheetEngine = class {
2367
2485
  }
2368
2486
  const scoped = [];
2369
2487
  const scope = {
2370
- // typeof check — not just truthiness — so a JS-only plugin pushing a
2371
- // bogus value is silently rejected instead of queueing a confusing
2372
- // TypeError on destroy() drain.
2373
2488
  add: (fn) => {
2374
2489
  if (typeof fn === "function") scoped.push(fn);
2375
2490
  }
@@ -2398,36 +2513,13 @@ var BottomSheetEngine = class {
2398
2513
  }
2399
2514
  return this;
2400
2515
  }
2401
- /**
2402
- * Replace the linked-sheets list at runtime. Use when peer engines aren't
2403
- * available at construction time (mounted later in the tree). The wiring
2404
- * to the `open` event is independent of this list — it reads `linkedSheets`
2405
- * lazily on emit, so this setter is a simple field swap.
2406
- */
2407
2516
  setLinkedSheets(sheets) {
2408
2517
  if (this.destroyed) return;
2409
2518
  this.linkedSheets = sheets;
2410
2519
  }
2411
- /** Read-only view of the current allow-list, for cross-sheet target negotiation. */
2412
2520
  getAllowedIds() {
2413
2521
  return this.snaps.getAllowedIds().slice();
2414
2522
  }
2415
- /**
2416
- * Animate to a snap point by id. Resolves when the animation completes.
2417
- *
2418
- * Two call shapes:
2419
- * ```ts
2420
- * engine.snapTo("full"); // default velocity
2421
- * engine.snapTo("full", 0.5); // px/ms initial velocity
2422
- * engine.snapTo("full", { velocity: 0.5, signal }); // abortable
2423
- * ```
2424
- *
2425
- * When `opts.signal` is provided, calling `controller.abort()` cancels the
2426
- * in-flight tween/spring and the returned promise resolves without firing
2427
- * `snap`. The captured `AbortSignal` is checked after each await so external
2428
- * aborts ride the same cycle invariant as internal ones (resize, destroy,
2429
- * concurrent snapTo).
2430
- */
2431
2523
  async snapTo(id, velocityOrOpts = 0, _skipBeforeSnap = false) {
2432
2524
  if (this.destroyed) return;
2433
2525
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2486,16 +2578,6 @@ var BottomSheetEngine = class {
2486
2578
  this.handleClose();
2487
2579
  }
2488
2580
  }
2489
- /**
2490
- * Drive the sheet to an arbitrary px size, bypassing snap matching.
2491
- * Useful for preview scrubs, animation chaining, or programmatic gestures.
2492
- * Animates with the configured spring/tween. Resolves when the animation
2493
- * completes. Does NOT update `activeId` — call `snapTo(id)` after to commit.
2494
- *
2495
- * Unlike `snapTo`, this does not fire `before-snap` (it isn't a snap commit)
2496
- * nor `snap` (the sheet isn't settling to a known snap). `progress` events
2497
- * still fire via applySize. Target is clamped to `[0, maxAxisSize]`.
2498
- */
2499
2581
  async dragTo(targetSize, velocityOrOpts = 0) {
2500
2582
  if (this.destroyed) return;
2501
2583
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2525,28 +2607,13 @@ var BottomSheetEngine = class {
2525
2607
  }
2526
2608
  setAllowed(ids, snap) {
2527
2609
  if (this.destroyed) return;
2528
- const willSnap = snap !== void 0 && ids.includes(snap) || !ids.includes(this.activeId) && ids.length > 0;
2529
- if (!willSnap) {
2530
- this.animation.cancel();
2531
- this.newCycle();
2532
- }
2533
2610
  this.snaps.setAllowedIds(ids);
2534
2611
  this.updateAriaSlider();
2535
2612
  if (snap && ids.includes(snap)) {
2536
- void this.snapTo(
2537
- snap,
2538
- 0,
2539
- /* internal */
2540
- true
2541
- );
2613
+ void this.snapTo(snap, 0, true);
2542
2614
  } else if (!ids.includes(this.activeId)) {
2543
2615
  const fallback = ids[0];
2544
- if (fallback) void this.snapTo(
2545
- fallback,
2546
- 0,
2547
- /* internal */
2548
- true
2549
- );
2616
+ if (fallback) void this.snapTo(fallback, 0, true);
2550
2617
  }
2551
2618
  }
2552
2619
  setBackdropRange(range) {
@@ -2564,21 +2631,6 @@ var BottomSheetEngine = class {
2564
2631
  setScrimInteractive(interactive) {
2565
2632
  this.scrim.setScrimInteractive(interactive);
2566
2633
  }
2567
- /**
2568
- * Batch-update multiple scrim fields in one call. Apply order is:
2569
- * 1. `preset` — fills baseline color/blur/range/interactive.
2570
- * 2. `mode` — re-positions the scrim (fixed inset / clear / hide).
2571
- * 3. `enabled` — flips the opacity contribution on/off.
2572
- * 4. `tapToClose` — installs/tears the click listener.
2573
- * 5. `color` / `blur` / `interactive` / `range` — explicit overrides.
2574
- * 6. ONE `applySize` so the per-frame opacity stays consistent.
2575
- *
2576
- * Performance: collecting changes here (vs calling individual setters)
2577
- * avoids repeated `applySize` calls during a multi-field swap. Setters
2578
- * called here suppress their own internal `applySize` either by passing
2579
- * through fast-paths (mode/enabled no-op when value unchanged) or by
2580
- * being visual-only (color/blur/interactive don't influence opacity).
2581
- */
2582
2634
  setScrim(opts) {
2583
2635
  this.scrim.setScrim(opts, () => this.applySize(this.size));
2584
2636
  }
@@ -2594,11 +2646,79 @@ var BottomSheetEngine = class {
2594
2646
  setScrimOverlay(opts) {
2595
2647
  return this.scrim.setScrimOverlay(opts);
2596
2648
  }
2597
- /**
2598
- * Read-only snapshot of scrim state. Use the `setScrim*` setters to mutate.
2599
- * Stable public API — internals (e.g. cached opacity sentinels) are NOT
2600
- * exposed so consumers can't accidentally couple to ScrimController shape.
2601
- */
2649
+ addAnchor(opts) {
2650
+ if (this.destroyed || typeof document === "undefined") return () => {
2651
+ };
2652
+ if (!this.anchorHost) {
2653
+ this.anchorHost = this.rootEl ?? this.element.parentElement ?? document.body;
2654
+ this.anchorHost.style.setProperty("--bs-size", `${this.size}px`);
2655
+ this.anchorHost.style.setProperty(
2656
+ "--bs-progress",
2657
+ String(this.computeProgress(this.size))
2658
+ );
2659
+ }
2660
+ const handle = attachAnchor(
2661
+ {
2662
+ mode: this.mode,
2663
+ host: this.anchorHost,
2664
+ getState: () => ({
2665
+ activeId: this.activeId,
2666
+ size: this.size,
2667
+ progress: this.computeProgress(this.size)
2668
+ }),
2669
+ on: (event, fn) => this.on(event, fn),
2670
+ isDestroyed: () => this.destroyed
2671
+ },
2672
+ opts
2673
+ );
2674
+ handle.syncZ(this.stackZ + 1);
2675
+ this.anchors.push(handle);
2676
+ const detach = () => {
2677
+ const idx = this.anchors.indexOf(handle);
2678
+ if (idx === -1) return;
2679
+ this.anchors.splice(idx, 1);
2680
+ handle.detach();
2681
+ };
2682
+ this.teardowns.add(detach);
2683
+ return detach;
2684
+ }
2685
+ setScrimStages(opts) {
2686
+ if (this.destroyed || typeof document === "undefined") return () => {
2687
+ };
2688
+ this.detachScrimStages?.();
2689
+ this.detachScrimStages = null;
2690
+ if (!opts) return () => {
2691
+ };
2692
+ const host = this.screenComponent?.parentElement;
2693
+ if (!host) {
2694
+ console.warn(
2695
+ "[BottomSheet] setScrimStages: no scrim element mounted \u2014 pass `scrim` to the engine first."
2696
+ );
2697
+ return () => {
2698
+ };
2699
+ }
2700
+ const detachInstalled = installScrimStages(
2701
+ {
2702
+ mode: this.mode,
2703
+ host,
2704
+ getState: () => ({
2705
+ activeId: this.activeId,
2706
+ size: this.size,
2707
+ progress: this.computeProgress(this.size)
2708
+ }),
2709
+ on: (event, fn) => this.on(event, fn),
2710
+ isDestroyed: () => this.destroyed
2711
+ },
2712
+ opts
2713
+ );
2714
+ const detach = () => {
2715
+ if (this.detachScrimStages === detach) this.detachScrimStages = null;
2716
+ detachInstalled();
2717
+ };
2718
+ this.detachScrimStages = detach;
2719
+ this.teardowns.add(() => this.detachScrimStages?.());
2720
+ return detach;
2721
+ }
2602
2722
  getScrimState() {
2603
2723
  return { mode: this.scrim.scrimMode, enabled: this.scrim.scrimEnabled };
2604
2724
  }
@@ -2644,24 +2764,11 @@ var BottomSheetEngine = class {
2644
2764
  emit(event, payload) {
2645
2765
  this.bus.emit(event, payload);
2646
2766
  }
2647
- /**
2648
- * Abort the current cycle's signal and replace the controller. Returns the
2649
- * fresh signal so the caller can capture it locally before any await — when
2650
- * a future newCycle() / destroy aborts our controller, the captured signal
2651
- * flips to aborted and post-await guards short-circuit.
2652
- */
2653
2767
  newCycle() {
2654
2768
  this.currentAbort.abort();
2655
2769
  this.currentAbort = new AbortController();
2656
2770
  return this.currentAbort.signal;
2657
2771
  }
2658
- /**
2659
- * Fire `before-snap` and report whether a listener cancelled it. The
2660
- * `cancel()` callback exposed to listeners is frozen after the synchronous
2661
- * emit phase so async callers can't silently miss — they get a console
2662
- * warning. Returns `true` when cancelled so callers can early-return /
2663
- * spring-back without re-implementing the cancel/frozen pattern.
2664
- */
2665
2772
  emitBeforeSnap(target, previousId) {
2666
2773
  let cancelled = false;
2667
2774
  let frozen = false;
@@ -2695,59 +2802,54 @@ var BottomSheetEngine = class {
2695
2802
  this.snaps.setRaw(this.snapPointsRaw);
2696
2803
  this.scrim.invalidateOpacityCache();
2697
2804
  }
2698
- /**
2699
- * Memoised `allowedRange()` — delegates to SnapResolver which owns the cache.
2700
- * Engine wrapper kept so existing call sites don't all need updating;
2701
- * resolver-level memoization survives across method invocations.
2702
- */
2703
2805
  getAllowedRange() {
2704
2806
  return this.snaps.getAllowedRange();
2705
2807
  }
2706
- /**
2707
- * Write ARIA slider value attributes onto the handle so the engine itself
2708
- * is the source of truth for screen-reader announcements. Called from the
2709
- * constructor and from every code path that mutates `activeId` /
2710
- * `allowedIds`. WAI-ARIA 1.2 requires `aria-valuenow` for `role="slider"` —
2711
- * without it screen readers can't announce drag/snap progress to keyboard
2712
- * + AT users (WCAG 4.1.2 Name, Role, Value).
2713
- *
2714
- * Engine does NOT set `role="slider"` itself — that's the adapter's or
2715
- * consumer markup's responsibility. We only decorate with dynamic values.
2716
- *
2717
- * Empty allowed-list is malformed input (settable via `setAllowed([])`);
2718
- * short-circuit to remove ARIA value attributes so screen readers see a
2719
- * no-value slider rather than a degenerate `min=max=now=0` (WAI-ARIA 1.2
2720
- * requires `valuemax > valuemin` for `role="slider"`).
2721
- */
2808
+ allowedIdsBySize() {
2809
+ return this.snaps.getAllowedIds().slice().sort(
2810
+ (a, b) => (this.snaps.findById(a)?.size ?? 0) - (this.snaps.findById(b)?.size ?? 0)
2811
+ );
2812
+ }
2722
2813
  updateAriaSlider() {
2723
- this.aria.setValue(this.snaps.getAllowedIds(), this.activeId);
2814
+ this.aria.setValue(this.allowedIdsBySize(), this.activeId);
2724
2815
  }
2725
2816
  registerInStack() {
2726
2817
  this.teardowns.add(sheetStack.push({
2727
2818
  id: this.id,
2728
2819
  setZIndex: (z) => {
2820
+ this.stackZ = z;
2729
2821
  this.element.style.zIndex = String(z);
2730
2822
  if (this.backdrop) this.backdrop.style.zIndex = String(z - 1);
2823
+ for (const anchor of this.anchors) anchor.syncZ(z + 1);
2731
2824
  },
2732
2825
  setIsTop: (isTop) => {
2733
2826
  this.isTopSheet = isTop;
2734
2827
  if (this.backdrop) {
2735
2828
  this.backdrop.style.display = isTop ? "" : "none";
2736
2829
  }
2737
- }
2830
+ },
2831
+ isOpen: () => this.size > 0,
2832
+ setDepth: (depth) => this.applyStackDepth(depth)
2738
2833
  }));
2739
2834
  }
2835
+ applyStackDepth(depth) {
2836
+ if (!this.stackEffectEnabled) return;
2837
+ this.element.setAttribute("data-stack-depth", String(depth));
2838
+ if (!this.stackEffectPrimed) {
2839
+ this.stackEffectPrimed = true;
2840
+ const isVertical = this.mode === "bottom" || this.mode === "top";
2841
+ this.element.style.transformOrigin = isVertical ? "50% 100%" : "0% 50%";
2842
+ this.element.style.transition = "scale 320ms cubic-bezier(0.29, 1.04, 0.84, 0.99)";
2843
+ }
2844
+ const scale = Math.max(1 - depth * 0.04, 0.86);
2845
+ this.element.style.scale = scale === 1 ? "" : String(scale);
2846
+ }
2740
2847
  attach() {
2741
2848
  this.gesture = new GestureController({
2742
2849
  handle: this.handle,
2743
2850
  element: this.element,
2744
2851
  mode: this.mode,
2745
2852
  getRoot: () => this.rootEl,
2746
- // Single coalesced reader — narrows the DI surface and removes 3 getter
2747
- // dispatches per onMove frame at 60-120Hz. Returns a SHARED buffer
2748
- // (this.dragContextBuf) — see field JSDoc for the retention contract.
2749
- // Inner `range` object is mutated in place too so callers caching its
2750
- // identity between frames stay valid.
2751
2853
  getDragContext: () => {
2752
2854
  const buf = this.dragContextBuf;
2753
2855
  const r = this.getAllowedRange();
@@ -2801,7 +2903,7 @@ var BottomSheetEngine = class {
2801
2903
  container: this.scrollContainer,
2802
2904
  isDragging: () => this.gesture?.isDragging ?? false,
2803
2905
  isAnimating: () => this.animation.isAnimating,
2804
- getAllowedIds: () => this.snaps.getAllowedIds(),
2906
+ getAllowedIds: () => this.allowedIdsBySize(),
2805
2907
  getActiveId: () => this.activeId,
2806
2908
  snapTo: (id) => {
2807
2909
  void this.snapTo(id);
@@ -2830,11 +2932,13 @@ var BottomSheetEngine = class {
2830
2932
  },
2831
2933
  newCycle: () => {
2832
2934
  this.newCycle();
2833
- }
2935
+ },
2936
+ isAnimating: () => this.animation.isAnimating,
2937
+ resyncAfterCancel: () => this.resyncAfterResize()
2834
2938
  }));
2835
2939
  if (this.lifecycle.closeOnEscape) {
2836
2940
  const onKey = (e) => {
2837
- if (this.destroyed) return;
2941
+ if (this.destroyed || e.defaultPrevented) return;
2838
2942
  if (e.key === "Escape" && this.isTopSheet && this.size > 0) {
2839
2943
  void this.close();
2840
2944
  }
@@ -2866,7 +2970,7 @@ var BottomSheetEngine = class {
2866
2970
  handle: this.handle,
2867
2971
  mode: this.mode,
2868
2972
  isDestroyed: () => this.destroyed,
2869
- getAllowedIds: () => this.snaps.getAllowedIds(),
2973
+ getAllowedIds: () => this.allowedIdsBySize(),
2870
2974
  getActiveId: () => this.activeId,
2871
2975
  snapTo: (id) => {
2872
2976
  void this.snapTo(id);
@@ -2887,6 +2991,7 @@ var BottomSheetEngine = class {
2887
2991
  });
2888
2992
  if (!target) return;
2889
2993
  const previousId = this.activeId;
2994
+ if (target.id === previousId && target.size === this.size) return;
2890
2995
  if (this.emitBeforeSnap(target, previousId)) {
2891
2996
  const restore = this.snaps.findById(previousId);
2892
2997
  if (restore) {
@@ -2895,6 +3000,7 @@ var BottomSheetEngine = class {
2895
3000
  return;
2896
3001
  }
2897
3002
  const previousSize = this.size;
3003
+ const previousSnapSize = this.snaps.findById(previousId)?.size;
2898
3004
  this.scrollCache.cache(previousId, previousSize, target.size);
2899
3005
  this.activeId = target.id;
2900
3006
  void this.animation.animateTo(target.size, velocity).then(() => {
@@ -2903,9 +3009,10 @@ var BottomSheetEngine = class {
2903
3009
  this.updateAriaSlider();
2904
3010
  this.emit("snap", { id: target.id, size: target.size });
2905
3011
  this.haptic();
2906
- if (previousId === "closed" && target.size > 0) {
3012
+ if (previousSnapSize === 0 && target.size > 0) {
2907
3013
  this.emit("open", { id: target.id });
2908
3014
  this.handleOpen();
3015
+ notifyLinkedSheets(this.linkedSheets, this);
2909
3016
  }
2910
3017
  if (target.size === 0) {
2911
3018
  this.emit("close", void 0);
@@ -2913,7 +3020,6 @@ var BottomSheetEngine = class {
2913
3020
  }
2914
3021
  });
2915
3022
  }
2916
- /** Trigger a brief haptic tick (iOS Safari + Android Chrome support). */
2917
3023
  haptic() {
2918
3024
  if (typeof navigator !== "undefined" && "vibrate" in navigator) {
2919
3025
  try {
@@ -2929,6 +3035,12 @@ var BottomSheetEngine = class {
2929
3035
  style.transform = this.transformTemplate(offset);
2930
3036
  if (this.sizeWriteSentinel.shouldWrite(size, SIZE_WRITE_EPSILON)) {
2931
3037
  style.setProperty("--bs-size", `${size}px`);
3038
+ if (this.scrimParent) {
3039
+ this.scrimParent.style.setProperty("--bs-size", `${size}px`);
3040
+ }
3041
+ if (this.anchorHost && this.anchorHost !== this.scrimParent) {
3042
+ this.anchorHost.style.setProperty("--bs-size", `${size}px`);
3043
+ }
2932
3044
  }
2933
3045
  const progress = this.computeProgress(size);
2934
3046
  const progressChanged = this.progressWriteSentinel.shouldWrite(
@@ -2937,6 +3049,9 @@ var BottomSheetEngine = class {
2937
3049
  );
2938
3050
  if (progressChanged) {
2939
3051
  style.setProperty("--bs-progress", String(progress));
3052
+ if (this.anchorHost) {
3053
+ this.anchorHost.style.setProperty("--bs-progress", String(progress));
3054
+ }
2940
3055
  }
2941
3056
  this.scrim.applyOpacity(progress, progressChanged);
2942
3057
  if (progressChanged && this.bus.listenerCount("progress") > 0) {
@@ -2951,6 +3066,7 @@ var BottomSheetEngine = class {
2951
3066
  return Math.min(Math.max((size - min) / (max - min), 0), 1);
2952
3067
  }
2953
3068
  handleOpen() {
3069
+ sheetStack.promote(this.id);
2954
3070
  try {
2955
3071
  this.lifecycle.install();
2956
3072
  } catch (err) {
@@ -2964,8 +3080,24 @@ var BottomSheetEngine = class {
2964
3080
  });
2965
3081
  }
2966
3082
  }
3083
+ resyncAfterResize() {
3084
+ if (this.destroyed) return;
3085
+ const target = this.snaps.findById(this.activeId);
3086
+ if (!target) return;
3087
+ this.updateAriaSlider();
3088
+ this.emit("snap", { id: target.id, size: target.size });
3089
+ if (target.size > 0 && !this.lifecycle.isInstalled) {
3090
+ this.emit("open", { id: target.id });
3091
+ this.handleOpen();
3092
+ notifyLinkedSheets(this.linkedSheets, this);
3093
+ } else if (target.size === 0 && this.lifecycle.isInstalled) {
3094
+ this.emit("close", void 0);
3095
+ this.handleClose();
3096
+ }
3097
+ }
2967
3098
  handleClose() {
2968
3099
  this.lifecycle.release();
3100
+ sheetStack.update();
2969
3101
  }
2970
3102
  };
2971
3103
 
@@ -2983,17 +3115,18 @@ function createBottomSheet(opts) {
2983
3115
  const attach = (refs) => {
2984
3116
  if (engine) engine.destroy();
2985
3117
  const { onSnap, ...engineOpts } = opts;
2986
- engine = new BottomSheetEngine({ ...engineOpts, ...refs });
3118
+ const created = new BottomSheetEngine({ ...engineOpts, ...refs });
3119
+ engine = created;
2987
3120
  if (onSnap) {
2988
- engine.on("snap", (payload) => onSnap(payload.id));
3121
+ created.on("snap", (payload) => onSnap(payload.id));
2989
3122
  }
2990
3123
  for (const entry of pending) {
2991
- entry.engineUnsub = engine.on(entry.event, entry.fn);
3124
+ entry.engineUnsub = created.on(entry.event, entry.fn);
2992
3125
  }
2993
3126
  pending.length = 0;
2994
3127
  return () => {
2995
- engine?.destroy();
2996
- engine = null;
3128
+ created.destroy();
3129
+ if (engine === created) engine = null;
2997
3130
  };
2998
3131
  };
2999
3132
  return {
@@ -3017,17 +3150,25 @@ function createBottomSheet(opts) {
3017
3150
  open: (id) => engine?.open(id) ?? Promise.resolve(),
3018
3151
  close: () => engine?.close() ?? Promise.resolve(),
3019
3152
  setAllowed: (ids, snap) => engine?.setAllowed(ids, snap),
3153
+ setSnapPoints: (points, allowed) => engine?.setSnapPoints(points, allowed),
3154
+ setScrim: (scrimOpts) => engine?.setScrim(scrimOpts),
3155
+ setScrimOverlay: (overlayOpts) => engine?.setScrimOverlay(overlayOpts) ?? (() => {
3156
+ }),
3157
+ addAnchor: (anchorOpts) => engine?.addAnchor(anchorOpts) ?? (() => {
3158
+ }),
3159
+ setScrimStages: (stagesOpts) => engine?.setScrimStages(stagesOpts) ?? (() => {
3160
+ }),
3020
3161
  destroy: () => {
3021
3162
  engine?.destroy();
3022
3163
  engine = null;
3023
- }
3164
+ },
3165
+ getEngine: () => engine
3024
3166
  };
3025
3167
  }
3026
3168
  var root_1 = $2__namespace.from_html(`<div class="bs-backdrop" aria-hidden="true"></div>`);
3027
- var root_2 = $2__namespace.from_html(`<div class="bs-screen"><!></div>`);
3028
3169
  var root_3 = $2__namespace.from_html(`<div class="bs-button-slot" data-side="left"><!></div>`);
3029
3170
  var root_4 = $2__namespace.from_html(`<div class="bs-button-slot" data-side="right"><!></div>`);
3030
- var root = $2__namespace.from_html(`<div class="bs-root"><!> <!> <section class="bs-sheet" role="dialog"><!> <!> <div class="bs-handle" role="slider" tabindex="0" aria-label="Resize sheet" aria-valuemin="0" aria-valuemax="0" aria-valuenow="0"><!></div> <div class="bs-content" tabindex="0" role="region" aria-label="Sheet content"><!></div> <span class="bs-sr-only" role="status" aria-live="polite"> </span></section></div>`);
3171
+ var root = $2__namespace.from_html(`<div class="bs-root"><!> <div class="bs-screen"><!></div> <section class="bs-sheet" role="dialog"><!> <!> <div class="bs-handle" role="slider" tabindex="0" aria-label="Resize sheet"><!></div> <div class="bs-content" tabindex="0" role="region" aria-label="Sheet content"><!></div> <span class="bs-sr-only" role="status" aria-live="polite"> </span></section></div>`);
3031
3172
  function BottomSheet($$anchor, $$props) {
3032
3173
  $2__namespace.push($$props, true);
3033
3174
  let mode = $2__namespace.prop($$props, "mode", 3, "bottom"), animation = $2__namespace.prop($$props, "animation", 3, "spring"), focusTrap = $2__namespace.prop($$props, "focusTrap", 3, false), closeOnEscape = $2__namespace.prop($$props, "closeOnEscape", 3, true), closeOnBack = $2__namespace.prop($$props, "closeOnBack", 3, false), lockBodyScroll2 = $2__namespace.prop($$props, "lockBodyScroll", 3, true), rubberBand2 = $2__namespace.prop($$props, "rubberBand", 3, true), backdrop = $2__namespace.prop($$props, "backdrop", 3, true), closeOnBackdrop = $2__namespace.prop($$props, "closeOnBackdrop", 3, true), ariaLabel = $2__namespace.prop($$props, "ariaLabel", 3, "Bottom sheet");
@@ -3038,7 +3179,7 @@ function BottomSheet($$anchor, $$props) {
3038
3179
  let screenEl = $2__namespace.state(void 0);
3039
3180
  let viewState = $2__namespace.state($2__namespace.proxy({
3040
3181
  size: 0,
3041
- activeId: $$props.initial ?? $$props.snapPoints[0]?.id ?? "default",
3182
+ activeId: svelte.untrack(() => $$props.initial ?? $$props.snapPoints[0]?.id ?? "default"),
3042
3183
  isDragging: false,
3043
3184
  isAnimating: false,
3044
3185
  progress: 0
@@ -3051,7 +3192,7 @@ function BottomSheet($$anchor, $$props) {
3051
3192
  handle: $2__namespace.get(handleEl),
3052
3193
  scrollContainer: $2__namespace.get(contentEl),
3053
3194
  backdrop: $2__namespace.get(backdropEl),
3054
- screenComponent: $2__namespace.get(screenEl),
3195
+ scrim: $2__namespace.get(screenEl),
3055
3196
  mode: mode(),
3056
3197
  snapPoints: $$props.snapPoints,
3057
3198
  allowed: $$props.allowed,
@@ -3088,8 +3229,26 @@ function BottomSheet($$anchor, $$props) {
3088
3229
  const open = (id) => engine?.open(id) ?? Promise.resolve();
3089
3230
  const close = () => engine?.close() ?? Promise.resolve();
3090
3231
  const setAllowed = (ids, snap) => engine?.setAllowed(ids, snap);
3232
+ const setSnapPoints = (points, nextAllowed) => engine?.setSnapPoints(points, nextAllowed);
3233
+ const setScrim = (opts) => engine?.setScrim(opts);
3234
+ const setScrimOverlay = (opts) => engine?.setScrimOverlay(opts) ?? (() => {
3235
+ });
3091
3236
  const getState = () => engine?.state ?? $2__namespace.get(viewState);
3092
- var $$exports = { snapTo, open, close, setAllowed, getState };
3237
+ const getEngine = () => engine;
3238
+ let allowedIds = $2__namespace.derived(() => $$props.allowed && $$props.allowed.length > 0 ? $$props.allowed : $$props.snapPoints.map((s) => s.id));
3239
+ let activeIdx = $2__namespace.derived(() => $2__namespace.get(allowedIds).indexOf($2__namespace.get(viewState).activeId));
3240
+ let isVerticalAxis = $2__namespace.derived(() => mode() === "bottom" || mode() === "top");
3241
+ var $$exports = {
3242
+ snapTo,
3243
+ open,
3244
+ close,
3245
+ setAllowed,
3246
+ setSnapPoints,
3247
+ setScrim,
3248
+ setScrimOverlay,
3249
+ getState,
3250
+ getEngine
3251
+ };
3093
3252
  var div = root();
3094
3253
  var node = $2__namespace.child(div);
3095
3254
  {
@@ -3105,21 +3264,22 @@ function BottomSheet($$anchor, $$props) {
3105
3264
  if (backdrop()) $$render(consequent);
3106
3265
  });
3107
3266
  }
3108
- var node_1 = $2__namespace.sibling(node, 2);
3267
+ var div_2 = $2__namespace.sibling(node, 2);
3268
+ var node_1 = $2__namespace.child(div_2);
3109
3269
  {
3110
3270
  var consequent_1 = ($$anchor2) => {
3111
- var div_2 = root_2();
3112
- var node_2 = $2__namespace.child(div_2);
3271
+ var fragment = $2__namespace.comment();
3272
+ var node_2 = $2__namespace.first_child(fragment);
3113
3273
  $2__namespace.snippet(node_2, () => $$props.screen);
3114
- $2__namespace.reset(div_2);
3115
- $2__namespace.bind_this(div_2, ($$value) => $2__namespace.set(screenEl, $$value), () => $2__namespace.get(screenEl));
3116
- $2__namespace.append($$anchor2, div_2);
3274
+ $2__namespace.append($$anchor2, fragment);
3117
3275
  };
3118
3276
  $2__namespace.if(node_1, ($$render) => {
3119
3277
  if ($$props.screen) $$render(consequent_1);
3120
3278
  });
3121
3279
  }
3122
- var section = $2__namespace.sibling(node_1, 2);
3280
+ $2__namespace.reset(div_2);
3281
+ $2__namespace.bind_this(div_2, ($$value) => $2__namespace.set(screenEl, $$value), () => $2__namespace.get(screenEl));
3282
+ var section = $2__namespace.sibling(div_2, 2);
3123
3283
  var node_3 = $2__namespace.child(section);
3124
3284
  {
3125
3285
  var consequent_2 = ($$anchor2) => {
@@ -3147,13 +3307,14 @@ function BottomSheet($$anchor, $$props) {
3147
3307
  });
3148
3308
  }
3149
3309
  var div_5 = $2__namespace.sibling(node_5, 2);
3310
+ $2__namespace.set_attribute(div_5, "aria-valuemin", 0);
3150
3311
  var node_7 = $2__namespace.child(div_5);
3151
3312
  {
3152
3313
  var consequent_4 = ($$anchor2) => {
3153
- var fragment = $2__namespace.comment();
3154
- var node_8 = $2__namespace.first_child(fragment);
3314
+ var fragment_1 = $2__namespace.comment();
3315
+ var node_8 = $2__namespace.first_child(fragment_1);
3155
3316
  $2__namespace.snippet(node_8, () => $$props.header, () => $2__namespace.get(viewState));
3156
- $2__namespace.append($$anchor2, fragment);
3317
+ $2__namespace.append($$anchor2, fragment_1);
3157
3318
  };
3158
3319
  $2__namespace.if(node_7, ($$render) => {
3159
3320
  if ($$props.header) $$render(consequent_4);
@@ -3165,10 +3326,10 @@ function BottomSheet($$anchor, $$props) {
3165
3326
  var node_9 = $2__namespace.child(div_6);
3166
3327
  {
3167
3328
  var consequent_5 = ($$anchor2) => {
3168
- var fragment_1 = $2__namespace.comment();
3169
- var node_10 = $2__namespace.first_child(fragment_1);
3329
+ var fragment_2 = $2__namespace.comment();
3330
+ var node_10 = $2__namespace.first_child(fragment_2);
3170
3331
  $2__namespace.snippet(node_10, () => $$props.children);
3171
- $2__namespace.append($$anchor2, fragment_1);
3332
+ $2__namespace.append($$anchor2, fragment_2);
3172
3333
  };
3173
3334
  $2__namespace.if(node_9, ($$render) => {
3174
3335
  if ($$props.children) $$render(consequent_5);
@@ -3182,13 +3343,23 @@ function BottomSheet($$anchor, $$props) {
3182
3343
  $2__namespace.reset(section);
3183
3344
  $2__namespace.bind_this(section, ($$value) => $2__namespace.set(sheetEl, $$value), () => $2__namespace.get(sheetEl));
3184
3345
  $2__namespace.reset(div);
3185
- $2__namespace.template_effect(() => {
3186
- $2__namespace.set_attribute(section, "data-mode", mode());
3187
- $2__namespace.set_attribute(section, "data-active", $2__namespace.get(viewState).activeId);
3188
- $2__namespace.set_attribute(section, "aria-modal", focusTrap() ? "true" : void 0);
3189
- $2__namespace.set_attribute(section, "aria-label", ariaLabel());
3190
- $2__namespace.set_text(text, $2__namespace.get(viewState).activeId);
3191
- });
3346
+ $2__namespace.template_effect(
3347
+ ($0, $1) => {
3348
+ $2__namespace.set_attribute(section, "data-mode", mode());
3349
+ $2__namespace.set_attribute(section, "data-active", $2__namespace.get(viewState).activeId);
3350
+ $2__namespace.set_attribute(section, "aria-modal", focusTrap() ? "true" : void 0);
3351
+ $2__namespace.set_attribute(section, "aria-label", ariaLabel());
3352
+ $2__namespace.set_attribute(div_5, "aria-orientation", $2__namespace.get(isVerticalAxis) ? "vertical" : "horizontal");
3353
+ $2__namespace.set_attribute(div_5, "aria-valuemax", $0);
3354
+ $2__namespace.set_attribute(div_5, "aria-valuenow", $1);
3355
+ $2__namespace.set_attribute(div_5, "aria-valuetext", $2__namespace.get(viewState).activeId);
3356
+ $2__namespace.set_text(text, $2__namespace.get(viewState).activeId);
3357
+ },
3358
+ [
3359
+ () => Math.max(0, $2__namespace.get(allowedIds).length - 1),
3360
+ () => Math.max(0, $2__namespace.get(activeIdx))
3361
+ ]
3362
+ );
3192
3363
  $2__namespace.append($$anchor, div);
3193
3364
  return $2__namespace.pop($$exports);
3194
3365
  }