@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/solid.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createSignal, onMount, onCleanup, createMemo } from 'solid-js';
2
- import { jsxs, jsx } from 'solid-js/jsx-runtime';
1
+ import { insert, memo, use, effect, setAttribute, template } from 'solid-js/web';
2
+ import { createSignal, onMount, onCleanup, createEffect, on, createMemo } from 'solid-js';
3
3
 
4
4
  var __defProp = Object.defineProperty;
5
5
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -23,6 +23,18 @@ var SheetStack = class {
23
23
  this.entries.splice(idx, 1);
24
24
  this.recompute();
25
25
  }
26
+ promote(id) {
27
+ const idx = this.entries.findIndex((e) => e.id === id);
28
+ if (idx === -1) return;
29
+ if (idx !== this.entries.length - 1) {
30
+ const [entry] = this.entries.splice(idx, 1);
31
+ this.entries.push(entry);
32
+ }
33
+ this.recompute();
34
+ }
35
+ update() {
36
+ this.recompute();
37
+ }
26
38
  size() {
27
39
  return this.entries.length;
28
40
  }
@@ -30,9 +42,24 @@ var SheetStack = class {
30
42
  this.entries = [];
31
43
  }
32
44
  recompute() {
45
+ let top = null;
46
+ const open = [];
47
+ for (const entry of this.entries) {
48
+ if (!entry.isOpen || entry.isOpen()) {
49
+ top = entry;
50
+ open.push(entry);
51
+ }
52
+ }
53
+ if (!top && this.entries.length > 0) {
54
+ top = this.entries[this.entries.length - 1];
55
+ }
33
56
  this.entries.forEach((entry, i) => {
34
57
  entry.setZIndex(BASE_Z + i * STEP);
35
- entry.setIsTop(i === this.entries.length - 1);
58
+ entry.setIsTop(entry === top);
59
+ if (entry.setDepth) {
60
+ const openIdx = open.indexOf(entry);
61
+ entry.setDepth(openIdx === -1 ? 0 : open.length - 1 - openIdx);
62
+ }
36
63
  });
37
64
  }
38
65
  };
@@ -231,11 +258,6 @@ var SnapResolver = class {
231
258
  this.allowedIds = allowed ?? raw.map((p) => p.id);
232
259
  this.recompute();
233
260
  }
234
- /**
235
- * Re-resolve raw → resolved against the current viewport / fit-measure,
236
- * recompute maxAxisSize, invalidate range cache, notify engine. Called on
237
- * resize, orientationchange, visualViewport changes, and snap-list swaps.
238
- */
239
261
  recompute() {
240
262
  this.resolved = resolveSnapList(this.raw, this.mode, this.measureFit);
241
263
  this.maxAxisSize = this.resolved.reduce(
@@ -245,15 +267,10 @@ var SnapResolver = class {
245
267
  this.rangeCache = null;
246
268
  this.onMaxAxisSizeChange?.(this.maxAxisSize);
247
269
  }
248
- /** Replace the raw snap list and recompute. Engine calls on `setSnapPoints`. */
249
270
  setRaw(raw) {
250
271
  this.raw = raw;
251
272
  this.recompute();
252
273
  }
253
- /**
254
- * Replace the allow-list. Range cache depends on `(resolvedSnaps ×
255
- * allowedIds)` so flipping ids alone invalidates without a full recompute.
256
- */
257
274
  setAllowedIds(ids) {
258
275
  this.allowedIds = ids;
259
276
  this.rangeCache = null;
@@ -261,11 +278,6 @@ var SnapResolver = class {
261
278
  findById(id) {
262
279
  return findById(id, this.resolved);
263
280
  }
264
- /**
265
- * Pure drag-settle target resolution. Engine passes the gesture-specific
266
- * fields (delta, velocity, pointerKind, flickVelocity, dragThreshold) plus
267
- * `size`/`activeId` since those are still engine-owned.
268
- */
269
281
  findDragSettleTarget(args) {
270
282
  return findDragSettleTarget({
271
283
  ...args,
@@ -274,18 +286,9 @@ var SnapResolver = class {
274
286
  maxAxisSize: this.maxAxisSize
275
287
  });
276
288
  }
277
- /**
278
- * Memoised allowed-range query. Twice-per-drag-frame (gesture onMove +
279
- * applySize→computeProgress); cache collapses 2× iteration of resolvedSnaps
280
- * to one recompute per geometry/allow-list change.
281
- */
282
289
  getAllowedRange() {
283
290
  return this.rangeCache ?? (this.rangeCache = allowedRange(this.resolved, this.allowedIds));
284
291
  }
285
- /**
286
- * Read-only view of resolved snaps. Returned as `readonly` to discourage
287
- * accidental mutation by feature install sites that close over this list.
288
- */
289
292
  getResolvedSnaps() {
290
293
  return this.resolved;
291
294
  }
@@ -295,18 +298,6 @@ var SnapResolver = class {
295
298
  getMaxAxisSize() {
296
299
  return this.maxAxisSize;
297
300
  }
298
- /**
299
- * External clamp override. The resize-observer feature compares
300
- * `getMaxAxisSize()` against the live viewport and shrinks if the largest
301
- * resolved snap exceeds it (mobile address-bar collapse / soft-keyboard
302
- * window resize). The next `recompute()` overwrites this clamp from the
303
- * resolved-snap reduction.
304
- *
305
- * Does NOT invalidate the range cache — viewport clamping doesn't change
306
- * `(resolvedSnaps × allowedIds) → range`. DOES fire `onMaxAxisSizeChange`
307
- * so the resolver owns the layout-axis style write end-to-end (symmetric
308
- * with `recompute()` — feature call sites no longer duplicate the write).
309
- */
310
301
  setMaxAxisSize(n) {
311
302
  this.maxAxisSize = n;
312
303
  this.onMaxAxisSizeChange?.(n);
@@ -353,14 +344,17 @@ function installAutoCollapse(deps) {
353
344
  const fire = () => {
354
345
  if (deps.isDestroyed()) return;
355
346
  if (deps.isDragging()) return;
356
- const allowed = deps.getAllowedIds();
357
- const target = allowed.find((id) => {
347
+ const active = deps.resolveSnap(deps.getActiveId());
348
+ if (!active || active.size === 0) return;
349
+ let target = null;
350
+ for (const id of deps.getAllowedIds()) {
358
351
  const snap = deps.resolveSnap(id);
359
- return snap !== null && snap.size > 0;
360
- });
352
+ if (!snap || snap.size <= 0) continue;
353
+ if (!target || snap.size < target.size) target = snap;
354
+ }
361
355
  if (!target) return;
362
- if (target === deps.getActiveId()) return;
363
- deps.snapTo(target);
356
+ if (target.id === active.id || target.size >= active.size) return;
357
+ deps.snapTo(target.id);
364
358
  };
365
359
  const reset = () => {
366
360
  clearTimer();
@@ -371,6 +365,7 @@ function installAutoCollapse(deps) {
371
365
  }, ms);
372
366
  };
373
367
  const unsubscribe = deps.on("snap", () => reset());
368
+ if ((deps.resolveSnap(deps.getActiveId())?.size ?? 0) > 0) reset();
374
369
  return () => {
375
370
  clearTimer();
376
371
  unsubscribe();
@@ -383,6 +378,7 @@ function notifyLinkedSheets(sheets, self) {
383
378
  for (const linked of sheets) {
384
379
  if (linked === self) continue;
385
380
  try {
381
+ if (linked.state.size === 0) continue;
386
382
  const ids = linked.getAllowedIds();
387
383
  const target = ids.find((id) => id !== "closed");
388
384
  if (target && target !== linked.state.activeId) {
@@ -429,7 +425,7 @@ function installRoute(deps) {
429
425
  const onPop = (event) => {
430
426
  if (deps.isDestroyed()) return;
431
427
  const state = event.state;
432
- if (state && state.__bsRouted === target && deps.getSize() > 0) {
428
+ if ((!state || state.__bsRouted !== target) && deps.getSize() > 0) {
433
429
  routePushed = false;
434
430
  void deps.close();
435
431
  }
@@ -583,13 +579,17 @@ function installResizeObserver(deps) {
583
579
  if (viewportSize2 > 0 && viewportSize2 < max) {
584
580
  deps.setMaxAxisSize(viewportSize2);
585
581
  }
582
+ const wasAnimating = deps.isAnimating?.() ?? false;
586
583
  deps.cancelInFlight();
587
584
  deps.newCycle();
588
585
  const current = deps.resolveActiveSnap();
589
586
  const newMax = deps.getMaxAxisSize();
590
587
  const targetSize = current ? Math.min(current.size, newMax) : Math.min(deps.getSize(), newMax);
591
588
  deps.setSize(targetSize);
592
- if (current && !deps.isDragging()) deps.applySize(targetSize);
589
+ if (current && !deps.isDragging()) {
590
+ deps.applySize(targetSize);
591
+ if (wasAnimating) deps.resyncAfterCancel?.();
592
+ }
593
593
  };
594
594
  let resizeObserver = null;
595
595
  if (typeof ResizeObserver !== "undefined") {
@@ -681,73 +681,130 @@ var WriteSentinel = class {
681
681
  constructor() {
682
682
  __publicField(this, "last", -1);
683
683
  }
684
- /**
685
- * Returns `true` (and updates the cached value) if `next` differs from the
686
- * cached `last` by more than `epsilon`. Returns `false` (and leaves the
687
- * cache untouched) when the delta is within epsilon — caller skips the
688
- * CSSOM write.
689
- */
690
684
  shouldWrite(next, epsilon) {
691
685
  if (Math.abs(next - this.last) <= epsilon) return false;
692
686
  this.last = next;
693
687
  return true;
694
688
  }
695
- /**
696
- * Force the next `shouldWrite` to pass. Resets the cache to `-1` so any
697
- * non-negative `next` exceeds the epsilon comparison. Called from setters
698
- * that mutate state under the cache (range/mode/enabled flips that change
699
- * the progress→opacity mapping but don't move `progress` itself).
700
- */
701
689
  invalidate() {
702
690
  this.last = -1;
703
691
  }
704
- /**
705
- * Record `value` as the last-written without performing a write. Used
706
- * when a setter writes a literal value through a different path (e.g.
707
- * `setScrimMode("off")` writes `opacity: "0"` inline) and wants future
708
- * `shouldWrite` calls to short-circuit against that known cached state.
709
- */
710
692
  setLastWritten(value) {
711
693
  this.last = value;
712
694
  }
713
- /** Read-only accessor for diagnostic / dedup-aware peek. */
714
695
  get value() {
715
696
  return this.last;
716
697
  }
717
698
  };
718
699
 
700
+ // src/core/primitives/devWarn.ts
701
+ function devWarn(...args) {
702
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && typeof console !== "undefined") {
703
+ console.warn(...args);
704
+ }
705
+ }
706
+
707
+ // src/core/primitives/overlay-position.ts
708
+ function resolveSheetAnchoredStyle(mode, position, inset) {
709
+ const sizeOffset = `calc(var(--bs-size, 0px) + ${inset})`;
710
+ switch (mode) {
711
+ case "bottom":
712
+ if (position === "sheet-top-left") return { bottom: sizeOffset, left: inset };
713
+ if (position === "sheet-top-right") return { bottom: sizeOffset, right: inset };
714
+ return { bottom: sizeOffset, left: "50%", transform: "translateX(-50%)" };
715
+ case "top":
716
+ if (position === "sheet-top-left") return { top: sizeOffset, left: inset };
717
+ if (position === "sheet-top-right") return { top: sizeOffset, right: inset };
718
+ return { top: sizeOffset, left: "50%", transform: "translateX(-50%)" };
719
+ case "left":
720
+ if (position === "sheet-top-left") return { left: sizeOffset, top: inset };
721
+ if (position === "sheet-top-right") return { left: sizeOffset, bottom: inset };
722
+ return { left: sizeOffset, top: "50%", transform: "translateY(-50%)" };
723
+ case "right":
724
+ if (position === "sheet-top-left") return { right: sizeOffset, top: inset };
725
+ if (position === "sheet-top-right") return { right: sizeOffset, bottom: inset };
726
+ return { right: sizeOffset, top: "50%", transform: "translateY(-50%)" };
727
+ }
728
+ }
729
+ function applyOverlayPosition(ws, mode, position, inset) {
730
+ switch (position) {
731
+ case "top-left":
732
+ ws.top = inset;
733
+ ws.left = inset;
734
+ break;
735
+ case "top-center":
736
+ ws.top = inset;
737
+ ws.left = "50%";
738
+ ws.transform = "translateX(-50%)";
739
+ break;
740
+ case "top-right":
741
+ ws.top = inset;
742
+ ws.right = inset;
743
+ break;
744
+ case "center-left":
745
+ ws.top = "50%";
746
+ ws.left = inset;
747
+ ws.transform = "translateY(-50%)";
748
+ break;
749
+ case "center":
750
+ ws.top = "50%";
751
+ ws.left = "50%";
752
+ ws.transform = "translate(-50%, -50%)";
753
+ break;
754
+ case "center-right":
755
+ ws.top = "50%";
756
+ ws.right = inset;
757
+ ws.transform = "translateY(-50%)";
758
+ break;
759
+ case "bottom-left":
760
+ ws.bottom = inset;
761
+ ws.left = inset;
762
+ break;
763
+ case "bottom-center":
764
+ ws.bottom = inset;
765
+ ws.left = "50%";
766
+ ws.transform = "translateX(-50%)";
767
+ break;
768
+ case "bottom-right":
769
+ ws.bottom = inset;
770
+ ws.right = inset;
771
+ break;
772
+ case "sheet-top-left":
773
+ case "sheet-top-center":
774
+ case "sheet-top-right": {
775
+ const style = resolveSheetAnchoredStyle(mode, position, inset);
776
+ if (style.top !== void 0) ws.top = style.top;
777
+ if (style.bottom !== void 0) ws.bottom = style.bottom;
778
+ if (style.left !== void 0) ws.left = style.left;
779
+ if (style.right !== void 0) ws.right = style.right;
780
+ if (style.transform !== void 0) ws.transform = style.transform;
781
+ break;
782
+ }
783
+ }
784
+ }
785
+
719
786
  // src/core/controllers/scrim-controller.ts
787
+ var VALID_CSS_LENGTH = /^-?\d+(?:\.\d+)?(?:px|em|rem|%)$/;
720
788
  var ScrimController = class {
721
789
  constructor(deps, opts) {
722
- // ── DOM refs (mirror engine's; cached for hot path) ─────────────────
723
790
  __publicField(this, "screenComponent");
724
791
  __publicField(this, "backdrop");
725
792
  __publicField(this, "mode");
726
- // ── Engine callbacks ────────────────────────────────────────────────
727
793
  __publicField(this, "isDestroyed");
728
794
  __publicField(this, "isTopSheet");
729
795
  __publicField(this, "getAllowedIds");
730
796
  __publicField(this, "getResolvedSnaps");
731
797
  __publicField(this, "snapToFn");
732
- // ── Mode + state ────────────────────────────────────────────────────
798
+ __publicField(this, "closeFn");
799
+ __publicField(this, "scrimInteractive", false);
733
800
  __publicField(this, "scrimMode");
734
801
  __publicField(this, "backdropRange");
735
802
  __publicField(this, "screenRange");
736
- // setScrimEnabled toggle — gated through `scrimEnabled` rather than nuking
737
- // ranges so re-enable can restore prior values without a remount. Public
738
- // so tests / introspection can read it without an `as any` cast.
739
803
  __publicField(this, "scrimEnabled", true);
740
804
  __publicField(this, "savedScrimRanges", null);
741
- // ── Tap-to-close ────────────────────────────────────────────────────
742
805
  __publicField(this, "scrimTapToCloseEnabled", false);
743
806
  __publicField(this, "detachScrimTap", null);
744
- // ── Scrim overlay slot (positioned floating element) ────────────────
745
807
  __publicField(this, "scrimOverlayTeardown", null);
746
- // ── Hot-path opacity dedup caches (per-frame writes) ────────────────
747
- // Numeric sentinels migrated to WriteSentinel primitive — single
748
- // invalidation surface, shared with the engine's size/progress sentinels.
749
- // The two string-typed sentinels (`Pointer`, `Display`) keep their inline
750
- // pattern: equality on string union, no epsilon, different reset semantics.
751
808
  __publicField(this, "backdropOpacitySentinel", new WriteSentinel());
752
809
  __publicField(this, "screenOpacitySentinel", new WriteSentinel());
753
810
  __publicField(this, "lastBackdropPointer", "");
@@ -760,6 +817,7 @@ var ScrimController = class {
760
817
  this.getAllowedIds = deps.getAllowedIds;
761
818
  this.getResolvedSnaps = deps.getResolvedSnaps;
762
819
  this.snapToFn = deps.snapTo;
820
+ this.closeFn = deps.close;
763
821
  this.scrimMode = opts.scrimMode ?? "full";
764
822
  const preset = opts.scrimPreset ? SCRIM_PRESETS[opts.scrimPreset] : null;
765
823
  this.backdropRange = opts.backdropRange ?? preset?.range ?? [0, 1];
@@ -782,41 +840,15 @@ var ScrimController = class {
782
840
  }
783
841
  }
784
842
  }
785
- /**
786
- * Invalidate the opacity dedup caches. Called from any setter that mutates
787
- * range/mode/enabled — opacity is a pure function of (progress, range,
788
- * enabled), so a config change makes the cached values stale even if
789
- * `progress` itself didn't move. Next `applyOpacity(progress, false)` call
790
- * sees the -1 sentinel and re-runs the math instead of short-circuiting.
791
- *
792
- * Public for the engine: `setSnapPoints` mutates resolved geometry which
793
- * shifts `progress→opacity` mapping under the cache; engine calls this to
794
- * keep the next render frame correct on a static sheet.
795
- * @internal
796
- */
797
843
  invalidateOpacityCache() {
798
844
  this.backdropOpacitySentinel.invalidate();
799
845
  this.screenOpacitySentinel.invalidate();
800
846
  }
801
- /** Engine calls this when its `attach()` runs — installs scrim-tap-click if requested. */
802
847
  attach() {
803
848
  if (this.scrimTapToCloseEnabled) {
804
849
  this.setScrimTapToClose(true);
805
850
  }
806
851
  }
807
- // ── Hot-path: opacity computation called per-frame from engine.applySize ──
808
- /**
809
- * Per-frame opacity writer. Computes backdrop + screen opacity from the
810
- * sheet's progress (0..1 within allowed range), gated by sub-pixel dedup
811
- * caches so steady-state frames don't trigger style recalc.
812
- *
813
- * `progressChanged` is the engine's already-computed dedup gate — when
814
- * `false`, opacity is by construction a pure function of `progress` so
815
- * cached values are still valid; we short-circuit the Math.min/max +
816
- * division work entirely. Setters that mutate ranges/mode/enabled
817
- * invalidate the cache by writing -1 sentinels so the next call re-runs
818
- * the math.
819
- */
820
852
  applyOpacity(progress, progressChanged) {
821
853
  if (!progressChanged && this.backdropOpacitySentinel.value !== -1 && this.screenOpacitySentinel.value !== -1) {
822
854
  return;
@@ -862,80 +894,30 @@ var ScrimController = class {
862
894
  }
863
895
  }
864
896
  }
865
- // ── Public setters (engine forwards these 1:1) ─────────────────────
866
- /**
867
- * Replace the [start, end] progress window in which the backdrop fades 0→1.
868
- * @param range - Tuple of two progress values in 0..1 (start <= end).
869
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
870
- * @example
871
- * // Backdrop only starts darkening after the sheet crosses 30% open.
872
- * engine.setBackdropRange([0.3, 1]);
873
- */
874
897
  setBackdropRange(range, applySize) {
875
898
  if (this.isDestroyed()) return;
876
899
  this.backdropRange = range;
877
900
  this.invalidateOpacityCache();
878
901
  applySize();
879
902
  }
880
- /**
881
- * Replace the [start, end] progress window in which the screen scrim fades 0→1.
882
- * @param range - Tuple of two progress values in 0..1 (start <= end).
883
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
884
- * @example
885
- * // Scrim is fully opaque only when the sheet is past 50% — softer feel.
886
- * engine.setScreenRange([0, 0.5]);
887
- */
888
903
  setScreenRange(range, applySize) {
889
904
  if (this.isDestroyed()) return;
890
905
  this.screenRange = range;
891
906
  this.invalidateOpacityCache();
892
907
  applySize();
893
908
  }
894
- /**
895
- * Set the scrim background color. Pass `null`/`undefined` to clear the
896
- * inline write and let consumer CSS take over.
897
- * @param color - Any valid CSS color string, or `null`/`undefined` to clear.
898
- * @example
899
- * engine.setScrimColor("rgba(0, 0, 0, 0.6)");
900
- * engine.setScrimColor(null); // hand back to consumer CSS
901
- */
902
909
  setScrimColor(color) {
903
910
  if (this.isDestroyed()) return;
904
911
  this.applyScrimStyles({ color: color === void 0 ? null : color });
905
912
  }
906
- /**
907
- * Set the scrim backdrop-filter blur radius. Pass `null`/`undefined` to clear.
908
- * @param blur - CSS length (e.g. `"8px"`) consumed as `blur(<value>)`, or
909
- * `null`/`undefined` to remove the filter.
910
- * @example
911
- * engine.setScrimBlur("12px"); // frosted-glass effect
912
- * engine.setScrimBlur(null); // disable blur
913
- */
914
913
  setScrimBlur(blur) {
915
914
  if (this.isDestroyed()) return;
916
915
  this.applyScrimStyles({ blur: blur === void 0 ? null : blur });
917
916
  }
918
- /**
919
- * Toggle whether the scrim layer captures pointer events.
920
- * @param interactive - `true` → `pointer-events: auto`; `false` → `none`.
921
- * @example
922
- * engine.setScrimInteractive(true); // scrim swallows clicks
923
- * engine.setScrimInteractive(false); // clicks pass through to content below
924
- */
925
917
  setScrimInteractive(interactive) {
926
918
  if (this.isDestroyed()) return;
927
919
  this.applyScrimStyles({ interactive });
928
920
  }
929
- /**
930
- * Switch scrim positioning at runtime.
931
- * @param mode - `"full"` (consumer CSS), `"above-sheet"` (engine-pinned inset
932
- * so only the area NOT covered by the sheet is dimmed), or
933
- * `"off"` (display:none + opacity:0 — no remount needed).
934
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
935
- * @example
936
- * engine.setScrimMode("above-sheet"); // dim only the area above the sheet
937
- * engine.setScrimMode("off"); // disable scrim layer
938
- */
939
921
  setScrimMode(mode, applySize) {
940
922
  if (this.isDestroyed()) return;
941
923
  if (mode === this.scrimMode) return;
@@ -965,15 +947,6 @@ var ScrimController = class {
965
947
  }
966
948
  applySize();
967
949
  }
968
- /**
969
- * Enable or disable click-on-scrim → snap to first non-zero allowed snap.
970
- * Auto-promotes `pointer-events: auto` when scrim is non-interactive so the
971
- * click listener actually fires.
972
- * @param enabled - `true` installs the click listener; `false` removes it.
973
- * @example
974
- * engine.setScrimTapToClose(true); // tap outside sheet collapses it
975
- * engine.setScrimTapToClose(false); // require explicit close button
976
- */
977
950
  setScrimTapToClose(enabled) {
978
951
  if (this.isDestroyed()) return;
979
952
  if (!this.screenComponent) return;
@@ -986,11 +959,15 @@ var ScrimController = class {
986
959
  }
987
960
  const onClick = () => {
988
961
  if (this.isDestroyed()) return;
962
+ if (this.closeFn) {
963
+ this.closeFn();
964
+ return;
965
+ }
989
966
  const allowed = this.getAllowedIds();
990
967
  const snaps = this.getResolvedSnaps();
991
968
  const fallback = allowed.find((id) => {
992
969
  const snap = findById(id, snaps);
993
- return snap !== null && snap.size > 0;
970
+ return snap !== null && snap.size === 0;
994
971
  });
995
972
  if (fallback) this.snapToFn(fallback);
996
973
  };
@@ -1003,20 +980,11 @@ var ScrimController = class {
1003
980
  this.scrimTapToCloseEnabled = false;
1004
981
  }
1005
982
  }
1006
- /**
1007
- * Toggle the scrim layer without remounting. Disabling stashes current
1008
- * `screenRange`/`backdropRange` and degenerates them to `[1, 1]` (opacity
1009
- * formula → 0 across the whole range); re-enabling restores the saved values.
1010
- * @param enabled - `false` hides scrim, `true` restores prior ranges.
1011
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
1012
- * @example
1013
- * engine.setScrimEnabled(false); // hide scrim, ranges remembered
1014
- * engine.setScrimEnabled(true); // restore prior ranges (no remount)
1015
- */
1016
983
  setScrimEnabled(enabled, applySize) {
1017
984
  if (this.isDestroyed()) return;
1018
985
  if (enabled === this.scrimEnabled) return;
1019
986
  this.scrimEnabled = enabled;
987
+ this.invalidateOpacityCache();
1020
988
  if (!enabled) {
1021
989
  this.savedScrimRanges = {
1022
990
  screen: this.screenRange,
@@ -1031,20 +999,6 @@ var ScrimController = class {
1031
999
  }
1032
1000
  applySize();
1033
1001
  }
1034
- /**
1035
- * Batch-update multiple scrim fields with a single applySize() at the end.
1036
- * If `preset` is set, it seeds color/blur/interactive/range first; explicit
1037
- * fields override.
1038
- * @param opts - Partial scrim config: preset, mode, enabled, tapToClose,
1039
- * color, blur, interactive, range.
1040
- * @param applySize - Engine-supplied recompute hook; runs ONCE per call.
1041
- * @example
1042
- * engine.setScrim({
1043
- * preset: "frosted",
1044
- * mode: "above-sheet",
1045
- * tapToClose: true,
1046
- * });
1047
- */
1048
1002
  setScrim(opts, applySize) {
1049
1003
  if (this.isDestroyed()) return;
1050
1004
  if (opts.preset) {
@@ -1054,7 +1008,12 @@ var ScrimController = class {
1054
1008
  blur: cfg.blur ?? null,
1055
1009
  interactive: cfg.interactive
1056
1010
  });
1057
- this.screenRange = cfg.range;
1011
+ if (!this.scrimEnabled && this.savedScrimRanges) {
1012
+ this.savedScrimRanges = { screen: cfg.range, backdrop: cfg.range };
1013
+ } else {
1014
+ this.screenRange = cfg.range;
1015
+ this.backdropRange = cfg.range;
1016
+ }
1058
1017
  }
1059
1018
  const noop = () => {
1060
1019
  };
@@ -1067,27 +1026,18 @@ var ScrimController = class {
1067
1026
  interactive: opts.interactive
1068
1027
  });
1069
1028
  if (opts.range !== void 0) {
1070
- this.screenRange = opts.range;
1029
+ if (!this.scrimEnabled && this.savedScrimRanges) {
1030
+ this.savedScrimRanges = {
1031
+ screen: opts.range,
1032
+ backdrop: this.savedScrimRanges.backdrop
1033
+ };
1034
+ } else {
1035
+ this.screenRange = opts.range;
1036
+ }
1071
1037
  }
1072
1038
  this.invalidateOpacityCache();
1073
1039
  applySize();
1074
1040
  }
1075
- /**
1076
- * Inject a positioned floating element into the scrim layer (e.g. a close
1077
- * button anchored top-right). Replaces any prior overlay so repeated calls
1078
- * (reactive effects re-running) don't accumulate wrappers.
1079
- * @param opts - Children element, optional `inset` (default `"16px"`),
1080
- * `position` (9-way grid, default `"top-right"`), and
1081
- * `interactive` flag (default `true`).
1082
- * @returns Teardown function that removes the wrapper from the DOM.
1083
- * @example
1084
- * const teardown = engine.setScrimOverlay({
1085
- * children: closeBtn,
1086
- * position: "top-right",
1087
- * inset: "20px",
1088
- * });
1089
- * // later: teardown(); // remove the floating button
1090
- */
1091
1041
  setScrimOverlay(opts) {
1092
1042
  if (this.isDestroyed()) return () => {
1093
1043
  };
@@ -1095,61 +1045,38 @@ var ScrimController = class {
1095
1045
  };
1096
1046
  this.scrimOverlayTeardown?.();
1097
1047
  const screen = this.screenComponent;
1098
- const inset = opts.inset ?? "16px";
1048
+ const insetRaw = opts.inset ?? "16px";
1049
+ const inset = VALID_CSS_LENGTH.test(insetRaw) ? insetRaw : "16px";
1050
+ if (inset !== insetRaw) {
1051
+ devWarn(
1052
+ `[bottom-sheet] setScrimOverlay: invalid inset ${JSON.stringify(insetRaw)}; expected a CSS length like "16px" / "1rem" / "5%". Falling back to "16px".`
1053
+ );
1054
+ }
1055
+ if (opts.children.ownerDocument && opts.children.ownerDocument !== screen.ownerDocument) {
1056
+ devWarn(
1057
+ "[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."
1058
+ );
1059
+ }
1099
1060
  const position = opts.position ?? "top-right";
1100
1061
  const interactive = opts.interactive ?? true;
1101
1062
  const wrapper = screen.ownerDocument.createElement("div");
1102
1063
  wrapper.className = "bs-scrim-overlay";
1103
1064
  const ws = wrapper.style;
1104
1065
  ws.position = "absolute";
1105
- switch (position) {
1106
- case "top-left":
1107
- ws.top = inset;
1108
- ws.left = inset;
1109
- break;
1110
- case "top-center":
1111
- ws.top = inset;
1112
- ws.left = "50%";
1113
- ws.transform = "translateX(-50%)";
1114
- break;
1115
- case "top-right":
1116
- ws.top = inset;
1117
- ws.right = inset;
1118
- break;
1119
- case "center-left":
1120
- ws.top = "50%";
1121
- ws.left = inset;
1122
- ws.transform = "translateY(-50%)";
1123
- break;
1124
- case "center":
1125
- ws.top = "50%";
1126
- ws.left = "50%";
1127
- ws.transform = "translate(-50%, -50%)";
1128
- break;
1129
- case "center-right":
1130
- ws.top = "50%";
1131
- ws.right = inset;
1132
- ws.transform = "translateY(-50%)";
1133
- break;
1134
- case "bottom-left":
1135
- ws.bottom = inset;
1136
- ws.left = inset;
1137
- break;
1138
- case "bottom-center":
1139
- ws.bottom = inset;
1140
- ws.left = "50%";
1141
- ws.transform = "translateX(-50%)";
1142
- break;
1143
- case "bottom-right":
1144
- ws.bottom = inset;
1145
- ws.right = inset;
1146
- break;
1147
- }
1066
+ applyOverlayPosition(ws, this.mode, position, inset);
1148
1067
  if (interactive) {
1149
1068
  ws.pointerEvents = "auto";
1150
1069
  }
1151
1070
  wrapper.appendChild(opts.children);
1152
- screen.appendChild(wrapper);
1071
+ const host = screen.parentElement;
1072
+ if (!host) {
1073
+ devWarn(
1074
+ "[bottom-sheet] setScrimOverlay: scrim element has no parent, skipping overlay injection. Mount the sheet root before calling setScrimOverlay."
1075
+ );
1076
+ return () => {
1077
+ };
1078
+ }
1079
+ host.appendChild(wrapper);
1153
1080
  const teardown = () => {
1154
1081
  if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
1155
1082
  if (this.scrimOverlayTeardown === teardown) {
@@ -1159,7 +1086,6 @@ var ScrimController = class {
1159
1086
  this.scrimOverlayTeardown = teardown;
1160
1087
  return teardown;
1161
1088
  }
1162
- /** Tear down toggle-state listeners + remove any injected overlay wrapper. */
1163
1089
  destroy() {
1164
1090
  this.detachScrimTap?.();
1165
1091
  this.detachScrimTap = null;
@@ -1178,8 +1104,6 @@ var ScrimController = class {
1178
1104
  this.lastBackdropPointer = "";
1179
1105
  this.lastScreenDisplay = "";
1180
1106
  }
1181
- // ── Private helpers ────────────────────────────────────────────────
1182
- /** Centralized scrim style writer — atomic 3-property write across all setters. */
1183
1107
  applyScrimStyles(opts) {
1184
1108
  if (!this.screenComponent) return;
1185
1109
  const s = this.screenComponent.style;
@@ -1198,10 +1122,10 @@ var ScrimController = class {
1198
1122
  }
1199
1123
  }
1200
1124
  if (opts.interactive !== void 0) {
1125
+ this.scrimInteractive = opts.interactive;
1201
1126
  s.pointerEvents = opts.interactive ? "auto" : "none";
1202
1127
  }
1203
1128
  }
1204
- /** Pin the screen overlay to the area NOT occupied by the sheet. */
1205
1129
  applyAboveSheetInset() {
1206
1130
  if (!this.screenComponent) return;
1207
1131
  const insetByMode = {
@@ -1213,7 +1137,7 @@ var ScrimController = class {
1213
1137
  const s = this.screenComponent.style;
1214
1138
  s.position = "fixed";
1215
1139
  s.inset = insetByMode[this.mode];
1216
- s.pointerEvents = "none";
1140
+ s.pointerEvents = this.scrimInteractive || this.scrimTapToCloseEnabled ? "auto" : "none";
1217
1141
  }
1218
1142
  };
1219
1143
 
@@ -1267,6 +1191,10 @@ var tween = ({
1267
1191
  promise
1268
1192
  };
1269
1193
  };
1194
+ var prefersReducedMotion = () => {
1195
+ if (typeof window === "undefined" || !window.matchMedia) return false;
1196
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1197
+ };
1270
1198
 
1271
1199
  // src/core/animation/spring.ts
1272
1200
  var DEFAULT_SPRING = {
@@ -1297,14 +1225,20 @@ var runSpring = ({
1297
1225
  if (cancelled) return;
1298
1226
  const dt = Math.min((now - lastTime) / 1e3, MAX_DT);
1299
1227
  lastTime = now;
1300
- const subDt = Math.min(dt, 1 / 240);
1301
- let stepsLeft = Math.max(1, Math.ceil(dt / subDt));
1228
+ const steps = Math.max(1, Math.ceil(dt * 240));
1229
+ const subDt = dt / steps;
1230
+ let stepsLeft = steps;
1302
1231
  while (stepsLeft-- > 0) {
1303
1232
  const force = -cfg.stiffness * (x - to) - cfg.damping * v;
1304
1233
  const accel = force / cfg.mass;
1305
1234
  v += accel * subDt;
1306
1235
  x += v * subDt;
1307
1236
  }
1237
+ if (!Number.isFinite(x) || !Number.isFinite(v)) {
1238
+ onUpdate(to, 0);
1239
+ resolve();
1240
+ return;
1241
+ }
1308
1242
  onUpdate(x, v);
1309
1243
  const isAtRest = Math.abs(to - x) < (cfg.restDelta ?? 0.5) && Math.abs(v) < (cfg.restSpeed ?? 0.5);
1310
1244
  if (isAtRest) {
@@ -1368,13 +1302,9 @@ var AnimationRunner = class {
1368
1302
  __publicField(this, "duration");
1369
1303
  __publicField(this, "easing");
1370
1304
  __publicField(this, "respectReducedMotion");
1371
- /** Live-tracked via mq listener so animateTo skips matchMedia per snap. */
1372
1305
  __publicField(this, "reducedMotion", false);
1373
- /** Disposes the matchMedia change listener — drained from destroy(). */
1374
1306
  __publicField(this, "detachReducedMotion", null);
1375
- /** Computed once — both inputs (option + API presence) stable for life. */
1376
1307
  __publicField(this, "viewTransitionsAvailable");
1377
- /** In-flight handles. Public for tests / debugging only — engine drives via cancel(). */
1378
1308
  __publicField(this, "currentTween", null);
1379
1309
  __publicField(this, "currentSpring", null);
1380
1310
  this.element = deps.element;
@@ -1405,26 +1335,13 @@ var AnimationRunner = class {
1405
1335
  }
1406
1336
  }
1407
1337
  }
1408
- /**
1409
- * Live derived flag — `true` whenever a spring/tween handle is in flight.
1410
- * Reading the canonical handles (instead of mirroring into a separate
1411
- * boolean) eliminates the reduced-motion early-return latent bug where
1412
- * the flag stayed true forever, and removes the cross-controller
1413
- * setIsAnimating callback.
1414
- */
1415
1338
  get isAnimating() {
1416
1339
  return this.currentTween !== null || this.currentSpring !== null;
1417
1340
  }
1418
- /** Cancel any in-flight tween/spring without touching engine state. */
1419
1341
  cancel() {
1420
1342
  this.currentTween?.cancel();
1421
1343
  this.currentSpring?.cancel();
1422
1344
  }
1423
- /**
1424
- * Animate from current size to `target`. Resolves when the animation
1425
- * completes; resolves early if cancelled mid-flight (caller checks an
1426
- * abort signal post-await for cycle-invariant correctness).
1427
- */
1428
1345
  async animateTo(target, velocityPxPerMs) {
1429
1346
  if (target === this.getSize()) return;
1430
1347
  this.currentTween?.cancel();
@@ -1465,7 +1382,6 @@ var AnimationRunner = class {
1465
1382
  if (!this.isDragging()) this.element.style.willChange = "auto";
1466
1383
  }
1467
1384
  }
1468
- /** Tear down the matchMedia listener. Engine calls from destroy(). */
1469
1385
  destroy() {
1470
1386
  this.detachReducedMotion?.();
1471
1387
  this.detachReducedMotion = null;
@@ -1497,9 +1413,13 @@ var isVisible = (el) => {
1497
1413
  var focusables = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter(
1498
1414
  (el) => !el.hasAttribute("inert") && isVisible(el) && !el.matches("[aria-hidden='true']")
1499
1415
  );
1416
+ var trapStack = [];
1500
1417
  var installFocusTrap = (container, options = {}) => {
1501
1418
  if (typeof document === "undefined") return () => {
1502
1419
  };
1420
+ const token = {};
1421
+ trapStack.push(token);
1422
+ const isActive = () => trapStack[trapStack.length - 1] === token;
1503
1423
  const previouslyFocused = document.activeElement;
1504
1424
  const focusInitial = () => {
1505
1425
  const initial = typeof options.initialFocus === "string" ? container.querySelector(options.initialFocus) : options.initialFocus ?? focusables(container)[0] ?? container;
@@ -1507,6 +1427,7 @@ var installFocusTrap = (container, options = {}) => {
1507
1427
  };
1508
1428
  focusInitial();
1509
1429
  const handleKey = (e) => {
1430
+ if (!isActive()) return;
1510
1431
  if (e.key === "Escape" && options.onEscape) {
1511
1432
  options.onEscape();
1512
1433
  return;
@@ -1529,6 +1450,7 @@ var installFocusTrap = (container, options = {}) => {
1529
1450
  }
1530
1451
  };
1531
1452
  const handleFocusIn = (e) => {
1453
+ if (!isActive()) return;
1532
1454
  const target = e.target;
1533
1455
  if (target && !container.contains(target)) {
1534
1456
  const list = focusables(container);
@@ -1541,6 +1463,8 @@ var installFocusTrap = (container, options = {}) => {
1541
1463
  return () => {
1542
1464
  if (released) return;
1543
1465
  released = true;
1466
+ const idx = trapStack.indexOf(token);
1467
+ if (idx !== -1) trapStack.splice(idx, 1);
1544
1468
  document.removeEventListener("keydown", handleKey, true);
1545
1469
  document.removeEventListener("focusin", handleFocusIn, true);
1546
1470
  if (previouslyFocused && document.contains(previouslyFocused)) {
@@ -1596,11 +1520,17 @@ function createInertSiblings(rootProvider) {
1596
1520
  return {
1597
1521
  apply() {
1598
1522
  if (typeof document === "undefined") return;
1599
- const root = rootProvider();
1600
- const parent = root.parentElement ?? document.body;
1523
+ let top = rootProvider();
1524
+ while (top.parentElement && top.parentElement !== document.body) {
1525
+ top = top.parentElement;
1526
+ }
1527
+ const parent = top.parentElement ?? document.body;
1601
1528
  for (const child of Array.from(parent.children)) {
1602
- if (child === root) continue;
1529
+ if (child === top) continue;
1603
1530
  if (child.hasAttribute("inert")) continue;
1531
+ if (child.classList.contains("bs-backdrop") || child.classList.contains("bs-screen") || child.classList.contains("bs-root")) {
1532
+ continue;
1533
+ }
1604
1534
  child.setAttribute("inert", "");
1605
1535
  tracked.push(child);
1606
1536
  }
@@ -1616,7 +1546,6 @@ function createInertSiblings(rootProvider) {
1616
1546
  var LifecycleController = class {
1617
1547
  constructor(deps, opts) {
1618
1548
  __publicField(this, "element");
1619
- __publicField(this, "closeFn");
1620
1549
  __publicField(this, "focusTrapEnabled");
1621
1550
  __publicField(this, "initialFocus");
1622
1551
  __publicField(this, "closeOnEscape");
@@ -1625,16 +1554,11 @@ var LifecycleController = class {
1625
1554
  __publicField(this, "inertSiblings");
1626
1555
  __publicField(this, "shouldApplyInertSiblings");
1627
1556
  __publicField(this, "returnFocus");
1628
- /** Toggle-state teardowns — fire on close, NOT in destroy-only stack. */
1629
1557
  __publicField(this, "releaseFocusTrap", null);
1630
1558
  __publicField(this, "releaseScrollLock", null);
1631
- /** True between install() and release(). Guards release()/destroy() from
1632
- * re-running side effects (notably `returnFocus.focus()`) on already-
1633
- * released or never-installed instances. */
1634
1559
  __publicField(this, "installed", false);
1635
1560
  __publicField(this, "destroyed", false);
1636
1561
  this.element = deps.element;
1637
- this.closeFn = deps.close;
1638
1562
  this.focusTrapEnabled = opts.focusTrap ?? false;
1639
1563
  this.initialFocus = opts.initialFocus;
1640
1564
  this.closeOnEscape = opts.closeOnEscape ?? true;
@@ -1644,14 +1568,9 @@ var LifecycleController = class {
1644
1568
  this.returnFocus = opts.returnFocus;
1645
1569
  this.inertSiblings = createInertSiblings(() => this.element);
1646
1570
  }
1647
- /**
1648
- * Run the open-time install steps. Idempotent: each step checks its
1649
- * existing teardown ref so a re-open during a half-cancelled close
1650
- * doesn't double-install. Sets `installed=true` BEFORE the first side
1651
- * effect so a partial throw (detached / shadow-DOM target) leaves the
1652
- * caller's `release()` rollback path armed to undo whatever did succeed.
1653
- * No-op when destroyed.
1654
- */
1571
+ get isInstalled() {
1572
+ return this.installed;
1573
+ }
1655
1574
  install() {
1656
1575
  if (this.destroyed) return;
1657
1576
  this.installed = true;
@@ -1660,30 +1579,17 @@ var LifecycleController = class {
1660
1579
  }
1661
1580
  if (this.focusTrapEnabled && !this.releaseFocusTrap) {
1662
1581
  this.releaseFocusTrap = installFocusTrap(this.element, {
1663
- initialFocus: this.initialFocus,
1664
- onEscape: this.closeOnEscape ? () => void this.closeFn() : void 0
1582
+ initialFocus: this.initialFocus
1665
1583
  });
1666
1584
  }
1667
1585
  if (this.inertSiblingsEnabled && this.shouldApplyInertSiblings()) {
1668
1586
  this.inertSiblings.apply();
1669
1587
  }
1670
1588
  }
1671
- /**
1672
- * Update the post-release focus target. Overlay re-binds this when the
1673
- * consumer changes `returnFocus` after construction; the new value applies
1674
- * on the next `release()`. No-op when destroyed.
1675
- */
1676
1589
  setReturnFocus(target) {
1677
1590
  if (this.destroyed) return;
1678
1591
  this.returnFocus = target;
1679
1592
  }
1680
- /**
1681
- * Release all install-time teardowns. Short-circuits when not installed,
1682
- * so `close() → destroy()` (each routing through here) only restores
1683
- * focus once. The `installed` guard lets `inertSiblings.remove()` and
1684
- * the focus-trap/scroll-lock teardown nullings stay simple — they were
1685
- * already idempotent, but `returnFocus.focus()` was firing on every call.
1686
- */
1687
1593
  release() {
1688
1594
  if (!this.installed) return;
1689
1595
  this.installed = false;
@@ -1697,12 +1603,6 @@ var LifecycleController = class {
1697
1603
  target?.focus?.();
1698
1604
  }
1699
1605
  }
1700
- /**
1701
- * Final teardown — releases any still-installed listeners and seals the
1702
- * controller. After destroy(), install() is a no-op and setReturnFocus()
1703
- * is rejected, so post-destroy mutations can't queue up a stale focus
1704
- * restore.
1705
- */
1706
1606
  destroy() {
1707
1607
  this.release();
1708
1608
  this.destroyed = true;
@@ -1712,12 +1612,10 @@ var LifecycleController = class {
1712
1612
  // src/core/gestures.ts
1713
1613
  var isAxisVertical = (mode) => mode === "bottom" || mode === "top";
1714
1614
  var SampleRing = class {
1715
- // current count, ≤ capacity
1716
1615
  constructor(capacity) {
1717
1616
  __publicField(this, "slots");
1718
1617
  __publicField(this, "capacity");
1719
1618
  __publicField(this, "head", 0);
1720
- // next write index
1721
1619
  __publicField(this, "size", 0);
1722
1620
  this.capacity = capacity;
1723
1621
  this.slots = new Array(capacity);
@@ -1730,12 +1628,6 @@ var SampleRing = class {
1730
1628
  this.head = (this.head + 1) % this.capacity;
1731
1629
  if (this.size < this.capacity) this.size++;
1732
1630
  }
1733
- /**
1734
- * Drop oldest entries whose timestamp is below `cutoff`, but never shrink
1735
- * below `minKeep`. Mirrors the original
1736
- * `while (samples.length > minKeep && samples[0].t < cutoff) samples.shift()`
1737
- * but without copying — just advances the logical tail.
1738
- */
1739
1631
  pruneBefore(cutoff, minKeep) {
1740
1632
  while (this.size > minKeep) {
1741
1633
  const oldestIdx = (this.head - this.size + this.capacity) % this.capacity;
@@ -1841,10 +1733,11 @@ var installGestures = (handle, mode, callbacks) => {
1841
1733
  };
1842
1734
 
1843
1735
  // src/core/primitives/rubber-band.ts
1844
- var RUBBER_BAND_FACTOR = 0.55;
1845
1736
  function rubberBand(overshoot, maxAxisSize) {
1846
1737
  const cap = Math.min(maxAxisSize * 0.15, 80);
1847
- return Math.sign(overshoot) * Math.pow(Math.abs(overshoot), RUBBER_BAND_FACTOR) * Math.min(6, cap / Math.max(Math.abs(overshoot), 1));
1738
+ if (cap <= 0) return 0;
1739
+ const abs = Math.abs(overshoot);
1740
+ return Math.sign(overshoot) * (cap * abs / (abs + cap));
1848
1741
  }
1849
1742
 
1850
1743
  // src/core/features/soft-keyboard.ts
@@ -1871,28 +1764,14 @@ var GestureController = class {
1871
1764
  __publicField(this, "detach", null);
1872
1765
  __publicField(this, "isDragging_", false);
1873
1766
  __publicField(this, "dragStartSize", 0);
1874
- // Only "touch" gestures are eligible for soft-keyboard dismissal —
1875
- // mouse/trackpad users don't have a keyboard to dismiss.
1876
1767
  __publicField(this, "currentPointerKind", "touch");
1877
- // One blur per drag — flipping focus every move event races the keyboard's
1878
- // own animations on iOS and makes the page jitter.
1879
1768
  __publicField(this, "keyboardDismissed", false);
1880
- // Reused payload object for the "drag" event — mutated each onMove instead
1881
- // of allocating a fresh `{size, delta}` per frame. At 60-120Hz a long drag
1882
- // would otherwise churn tens of KB through the GC. Mirrors browser
1883
- // PointerEvent semantics: consumers must NOT retain the reference across
1884
- // frames (clone if needed). See SheetEventMap.drag JSDoc.
1885
1769
  __publicField(this, "dragPayload", { size: 0, delta: 0 });
1886
1770
  this.deps = deps;
1887
1771
  }
1888
- /** True between `dragstart` and `dragend`/`onCancel`. */
1889
1772
  get isDragging() {
1890
1773
  return this.isDragging_;
1891
1774
  }
1892
- /**
1893
- * Bind pointer listeners on the handle and return a teardown. Engine pushes
1894
- * the teardown into its TeardownStack so destroy() drains it once.
1895
- */
1896
1775
  install() {
1897
1776
  if (this.detach) return this.detach;
1898
1777
  this.detach = installGestures(this.deps.handle, this.deps.mode, {
@@ -1943,17 +1822,16 @@ var GestureController = class {
1943
1822
  this.isDragging_ = false;
1944
1823
  this.keyboardDismissed = false;
1945
1824
  this.deps.getRoot()?.removeAttribute("data-dragging");
1825
+ this.deps.emit("dragend", {
1826
+ size: this.deps.getDragContext().size,
1827
+ velocity: 0
1828
+ });
1946
1829
  const restoreTo = this.dragStartSize;
1947
1830
  void this.deps.animateTo(restoreTo, 0);
1948
1831
  }
1949
1832
  });
1950
1833
  return this.detach;
1951
1834
  }
1952
- /**
1953
- * Force-clear drag visual state. Engine calls from destroy() when destroyed
1954
- * mid-drag — gesture detach won't synthesize an onCancel, so we have to
1955
- * roll back the data-attribute + will-change ourselves.
1956
- */
1957
1835
  forceClearDragState() {
1958
1836
  if (!this.isDragging_) return;
1959
1837
  this.isDragging_ = false;
@@ -1972,11 +1850,6 @@ var AriaSliderWriter = class {
1972
1850
  mode === "bottom" || mode === "top" ? "vertical" : "horizontal"
1973
1851
  );
1974
1852
  }
1975
- /**
1976
- * Update slider position. `allowedIds` length determines `aria-valuemax`;
1977
- * `activeId` indexes into it for `aria-valuenow` + `aria-valuetext`.
1978
- * Empty list → strip every dynamic attr so AT skips the handle entirely.
1979
- */
1980
1853
  setValue(allowedIds, activeId) {
1981
1854
  if (allowedIds.length === 0) {
1982
1855
  this.handle.removeAttribute("aria-valuemin");
@@ -2026,6 +1899,287 @@ function createScrollCache(deps) {
2026
1899
  };
2027
1900
  }
2028
1901
 
1902
+ // src/core/primitives/anchor-animations.ts
1903
+ var DEFAULT_DURATION3 = 200;
1904
+ var DEFAULT_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
1905
+ var PRESET_FRAMES = {
1906
+ fade: {
1907
+ enter: [{ opacity: 0 }, { opacity: 1 }],
1908
+ exit: [{ opacity: 1 }, { opacity: 0 }]
1909
+ },
1910
+ scale: {
1911
+ enter: [
1912
+ { opacity: 0, transform: "scale(0.85)" },
1913
+ { opacity: 1, transform: "scale(1)" }
1914
+ ],
1915
+ exit: [
1916
+ { opacity: 1, transform: "scale(1)" },
1917
+ { opacity: 0, transform: "scale(0.85)" }
1918
+ ]
1919
+ },
1920
+ slide: {
1921
+ enter: [
1922
+ { opacity: 0, transform: "translateY(10px)" },
1923
+ { opacity: 1, transform: "translateY(0)" }
1924
+ ],
1925
+ exit: [
1926
+ { opacity: 1, transform: "translateY(0)" },
1927
+ { opacity: 0, transform: "translateY(10px)" }
1928
+ ]
1929
+ },
1930
+ pop: {
1931
+ enter: [
1932
+ { opacity: 0, transform: "scale(0.6)", offset: 0 },
1933
+ { opacity: 1, transform: "scale(1.06)", offset: 0.7 },
1934
+ { opacity: 1, transform: "scale(1)", offset: 1 }
1935
+ ],
1936
+ exit: [
1937
+ { opacity: 1, transform: "scale(1)" },
1938
+ { opacity: 0, transform: "scale(0.6)" }
1939
+ ]
1940
+ }
1941
+ };
1942
+ var INSTANT = {
1943
+ finished: Promise.resolve(),
1944
+ cancel: () => {
1945
+ }
1946
+ };
1947
+ function resolveTransitionFrames(spec, phase) {
1948
+ const fallback = { frames: null, duration: 0, easing: DEFAULT_EASING };
1949
+ if (spec === "none") return fallback;
1950
+ if (spec === void 0 || typeof spec === "string") {
1951
+ const preset2 = PRESET_FRAMES[spec ?? "fade"];
1952
+ return {
1953
+ frames: preset2[phase],
1954
+ duration: DEFAULT_DURATION3,
1955
+ easing: DEFAULT_EASING
1956
+ };
1957
+ }
1958
+ const custom = phase === "enter" ? spec.enter : spec.exit;
1959
+ if (custom) {
1960
+ return {
1961
+ frames: custom,
1962
+ duration: spec.duration ?? DEFAULT_DURATION3,
1963
+ easing: spec.easing ?? DEFAULT_EASING
1964
+ };
1965
+ }
1966
+ if (spec.preset === "none") return fallback;
1967
+ const preset = PRESET_FRAMES[spec.preset ?? "fade"];
1968
+ return {
1969
+ frames: preset[phase],
1970
+ duration: spec.duration ?? DEFAULT_DURATION3,
1971
+ easing: spec.easing ?? DEFAULT_EASING
1972
+ };
1973
+ }
1974
+ function runAnchorTransition(el, spec, phase) {
1975
+ const respectReduced = typeof spec === "object" ? spec.respectReducedMotion !== false : true;
1976
+ if (respectReduced && prefersReducedMotion()) return INSTANT;
1977
+ const { frames, duration, easing } = resolveTransitionFrames(spec, phase);
1978
+ if (!frames || duration <= 0) return INSTANT;
1979
+ if (typeof el.animate !== "function") return INSTANT;
1980
+ const animation = el.animate(frames, {
1981
+ duration,
1982
+ easing,
1983
+ fill: "both"
1984
+ });
1985
+ const finished = animation.finished.then(() => void 0).catch(() => void 0);
1986
+ return {
1987
+ finished,
1988
+ cancel: () => {
1989
+ try {
1990
+ animation.cancel();
1991
+ } catch {
1992
+ return;
1993
+ }
1994
+ }
1995
+ };
1996
+ }
1997
+
1998
+ // src/core/features/sheet-anchors.ts
1999
+ function attachAnchor(deps, opts) {
2000
+ const doc = deps.host.ownerDocument;
2001
+ const wrapper = doc.createElement("div");
2002
+ wrapper.className = "bs-anchor";
2003
+ const interactive = opts.interactive ?? true;
2004
+ const position = opts.position ?? "sheet-top-right";
2005
+ const isDock = position === "dock-bottom" || position === "dock-top";
2006
+ const ws = wrapper.style;
2007
+ ws.position = "fixed";
2008
+ if (isDock) {
2009
+ ws.left = "0";
2010
+ ws.right = "0";
2011
+ if (position === "dock-bottom") {
2012
+ ws.bottom = "0";
2013
+ } else {
2014
+ ws.top = "0";
2015
+ }
2016
+ } else {
2017
+ applyOverlayPosition(ws, deps.mode, position, opts.inset ?? "16px");
2018
+ }
2019
+ if (opts.fadeRange) {
2020
+ const [r0, r1] = opts.fadeRange;
2021
+ const span = Math.max(r1 - r0, 1e-4);
2022
+ ws.opacity = `clamp(0, calc((var(--bs-progress, 0) - ${r0}) / ${span}), 1)`;
2023
+ }
2024
+ wrapper.appendChild(opts.element);
2025
+ deps.host.appendChild(wrapper);
2026
+ let visible = null;
2027
+ let inFlight = null;
2028
+ const shouldShow = (state) => {
2029
+ if (typeof opts.showOn === "function") return opts.showOn(state);
2030
+ if (Array.isArray(opts.showOn)) return opts.showOn.includes(state.activeId);
2031
+ return isDock || state.size > 0;
2032
+ };
2033
+ const applyVisibility = (next, animate) => {
2034
+ if (next === visible) return;
2035
+ visible = next;
2036
+ inFlight?.cancel();
2037
+ inFlight = null;
2038
+ wrapper.style.pointerEvents = next && interactive ? "auto" : "none";
2039
+ if (next) {
2040
+ wrapper.style.visibility = "";
2041
+ if (animate) {
2042
+ inFlight = runAnchorTransition(opts.element, opts.animation, "enter");
2043
+ }
2044
+ return;
2045
+ }
2046
+ if (!animate) {
2047
+ wrapper.style.visibility = "hidden";
2048
+ return;
2049
+ }
2050
+ const handle = runAnchorTransition(opts.element, opts.animation, "exit");
2051
+ inFlight = handle;
2052
+ void handle.finished.then(() => {
2053
+ if (inFlight === handle && visible === false) {
2054
+ wrapper.style.visibility = "hidden";
2055
+ }
2056
+ });
2057
+ };
2058
+ const evaluate = (animate) => {
2059
+ if (deps.isDestroyed()) return;
2060
+ applyVisibility(shouldShow(deps.getState()), animate);
2061
+ };
2062
+ const offs = [
2063
+ deps.on("snap", () => evaluate(true)),
2064
+ deps.on("open", () => evaluate(true)),
2065
+ deps.on("close", () => evaluate(true))
2066
+ ];
2067
+ evaluate(false);
2068
+ return {
2069
+ wrapper,
2070
+ detach: () => {
2071
+ offs.forEach((off) => off());
2072
+ inFlight?.cancel();
2073
+ inFlight = null;
2074
+ if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
2075
+ },
2076
+ syncZ: (z) => {
2077
+ wrapper.style.zIndex = String(z);
2078
+ }
2079
+ };
2080
+ }
2081
+
2082
+ // src/core/features/scrim-stages.ts
2083
+ var matchesId = (def, activeId) => {
2084
+ if (def.for === void 0) return false;
2085
+ return Array.isArray(def.for) ? def.for.includes(activeId) : def.for === activeId;
2086
+ };
2087
+ var matchesRange = (def, progress) => {
2088
+ if (!def.forRange) return false;
2089
+ const [from, to] = def.forRange;
2090
+ return progress >= from && progress <= to;
2091
+ };
2092
+ function installScrimStages(deps, opts) {
2093
+ const doc = deps.host.ownerDocument;
2094
+ const entries = opts.stages.map((def) => {
2095
+ const wrapper = doc.createElement("div");
2096
+ wrapper.className = "bs-scrim-stage";
2097
+ const ws = wrapper.style;
2098
+ ws.position = "absolute";
2099
+ applyOverlayPosition(
2100
+ ws,
2101
+ deps.mode,
2102
+ def.position ?? opts.position ?? "center",
2103
+ def.inset ?? opts.inset ?? "16px"
2104
+ );
2105
+ ws.visibility = "hidden";
2106
+ ws.pointerEvents = "none";
2107
+ wrapper.appendChild(def.element);
2108
+ deps.host.appendChild(wrapper);
2109
+ return { def, wrapper, inFlight: null };
2110
+ });
2111
+ let active = null;
2112
+ const resolveActive = () => {
2113
+ const state = deps.getState();
2114
+ if (state.size === 0) {
2115
+ return entries.find((e) => matchesId(e.def, state.activeId)) ?? null;
2116
+ }
2117
+ return entries.find((e) => matchesId(e.def, state.activeId)) ?? entries.find((e) => matchesRange(e.def, state.progress)) ?? null;
2118
+ };
2119
+ const hide = (entry, animate) => {
2120
+ entry.inFlight?.cancel();
2121
+ entry.wrapper.style.pointerEvents = "none";
2122
+ if (!animate) {
2123
+ entry.inFlight = null;
2124
+ entry.wrapper.style.visibility = "hidden";
2125
+ return;
2126
+ }
2127
+ const handle = runAnchorTransition(
2128
+ entry.def.element,
2129
+ entry.def.animation ?? opts.animation,
2130
+ "exit"
2131
+ );
2132
+ entry.inFlight = handle;
2133
+ void handle.finished.then(() => {
2134
+ if (entry.inFlight === handle && active !== entry) {
2135
+ entry.wrapper.style.visibility = "hidden";
2136
+ }
2137
+ });
2138
+ };
2139
+ const show = (entry, animate) => {
2140
+ entry.inFlight?.cancel();
2141
+ entry.inFlight = null;
2142
+ entry.wrapper.style.visibility = "";
2143
+ entry.wrapper.style.pointerEvents = entry.def.interactive ?? opts.interactive ?? false ? "auto" : "none";
2144
+ if (animate) {
2145
+ entry.inFlight = runAnchorTransition(
2146
+ entry.def.element,
2147
+ entry.def.animation ?? opts.animation,
2148
+ "enter"
2149
+ );
2150
+ }
2151
+ };
2152
+ const update = (animate) => {
2153
+ if (deps.isDestroyed()) return;
2154
+ const next = resolveActive();
2155
+ if (next === active) return;
2156
+ const prev = active;
2157
+ active = next;
2158
+ if (prev) hide(prev, animate);
2159
+ if (next) show(next, animate);
2160
+ };
2161
+ const offs = [
2162
+ deps.on("snap", () => update(true)),
2163
+ deps.on("open", () => update(true)),
2164
+ deps.on("close", () => update(true))
2165
+ ];
2166
+ if (entries.some((e) => e.def.forRange)) {
2167
+ offs.push(deps.on("progress", () => update(true)));
2168
+ }
2169
+ update(false);
2170
+ return () => {
2171
+ offs.forEach((off) => off());
2172
+ for (const entry of entries) {
2173
+ entry.inFlight?.cancel();
2174
+ entry.inFlight = null;
2175
+ if (entry.wrapper.parentNode) {
2176
+ entry.wrapper.parentNode.removeChild(entry.wrapper);
2177
+ }
2178
+ }
2179
+ active = null;
2180
+ };
2181
+ }
2182
+
2029
2183
  // src/core/primitives/engine-options.ts
2030
2184
  var DEFAULT_FLICK_VELOCITY = 0.65;
2031
2185
  var DEFAULT_DRAG_THRESHOLD = 18;
@@ -2050,9 +2204,6 @@ function resolveEngineOptions(opts, extras) {
2050
2204
  scrimMode: opts.scrimMode,
2051
2205
  scrimColor: opts.scrimColor,
2052
2206
  scrimBlur: opts.scrimBlur,
2053
- // Auto-promote pointer-events when tap-to-close is on so the click
2054
- // listener actually receives events. Caller can still force `false`
2055
- // explicitly to opt out — `??` only kicks in for `undefined`.
2056
2207
  scrimInteractive: opts.scrimInteractive ?? (opts.scrimTapToClose ? true : void 0),
2057
2208
  scrimTapToClose: opts.scrimTapToClose,
2058
2209
  scrimPreset: opts.scrimPreset,
@@ -2073,10 +2224,6 @@ function resolveEngineOptions(opts, extras) {
2073
2224
  closeOnEscape: opts.closeOnEscape,
2074
2225
  lockBodyScroll: opts.lockBodyScroll,
2075
2226
  inertSiblings: opts.inertSiblings,
2076
- // Folded in from `extras` so the lifecycle bag is fully assembled at
2077
- // the call site — engine no longer needs a spread+override at
2078
- // controller construction. When `extras` is omitted, the field stays
2079
- // undefined and the controller uses its own default predicate.
2080
2227
  shouldApplyInertSiblings: extras?.shouldApplyInertSiblings
2081
2228
  }
2082
2229
  };
@@ -2111,6 +2258,7 @@ var BottomSheetEngine = class {
2111
2258
  __publicField(this, "scrollContainer");
2112
2259
  __publicField(this, "backdrop");
2113
2260
  __publicField(this, "screenComponent");
2261
+ __publicField(this, "scrimParent", null);
2114
2262
  __publicField(this, "mode");
2115
2263
  __publicField(this, "flickVelocity");
2116
2264
  __publicField(this, "dragThreshold");
@@ -2121,40 +2269,22 @@ var BottomSheetEngine = class {
2121
2269
  __publicField(this, "lifecycle");
2122
2270
  __publicField(this, "closeOnBack");
2123
2271
  __publicField(this, "routedTo");
2124
- // Engine retains `snapPointsRaw` for the "find a closed-id" query in
2125
- // close() (raw input, not resolved geometry). Resolver owns geometry.
2126
2272
  __publicField(this, "snapPointsRaw");
2127
2273
  __publicField(this, "snaps");
2128
2274
  __publicField(this, "activeId");
2129
2275
  __publicField(this, "size", 0);
2130
- // Constructed in attach(); a plugin's install() could read `state` before
2131
- // attach completes if the constructor throws partway through. State getter
2132
- // null-guards via `this.gesture?.isDragging ?? false`.
2133
2276
  __publicField(this, "gesture");
2134
2277
  __publicField(this, "rootEl", null);
2135
2278
  __publicField(this, "destroyed", false);
2136
- // In-flight ViewTransition handle. A second snapTo while a VT is in flight
2137
- // would otherwise let Chromium reject the second's promise and flicker —
2138
- // newCycle() calls skipTransition() to abort cleanly.
2139
2279
  __publicField(this, "currentViewTransition", null);
2140
- // Each cycle captures the signal at entry; when a newer cycle aborts the
2141
- // controller, the captured signal flips to `aborted=true` and post-await
2142
- // guards short-circuit (prevents stale snap/open/close emits).
2143
2280
  __publicField(this, "currentAbort", new AbortController());
2144
- // Sub-pixel `--bs-size` deltas and <1% `--bs-progress` deltas are not
2145
- // visually distinguishable; gating CSSOM writes drops 30-50% of writes
2146
- // during the spring's settle tail.
2147
2281
  __publicField(this, "sizeWriteSentinel", new WriteSentinel());
2148
2282
  __publicField(this, "progressWriteSentinel", new WriteSentinel());
2149
2283
  __publicField(this, "isTopSheet", true);
2150
- // Pooled payload — identity documented in SheetEventMap.progress JSDoc;
2151
- // consumers must not retain across frames.
2152
2284
  __publicField(this, "progressPayload", {
2153
2285
  value: 0,
2154
2286
  size: 0
2155
2287
  });
2156
- // Pooled buffer for `getDragContext()` — onMove calls this 60-120×/s.
2157
- // Inner `range` object identity is also stable (mutated in place).
2158
2288
  __publicField(this, "dragContextBuf", {
2159
2289
  size: 0,
2160
2290
  maxAxisSize: 0,
@@ -2165,13 +2295,13 @@ var BottomSheetEngine = class {
2165
2295
  __publicField(this, "persistKey");
2166
2296
  __publicField(this, "linkedSheets", []);
2167
2297
  __publicField(this, "scrollCache");
2168
- // Single LIFO stack for ALL destroy-only teardowns. Plugins (added last)
2169
- // tear down first before the infrastructure they depend on. Toggle-semantic
2170
- // listeners (focus-trap / scroll-lock / scrim tap-to-close) stay as named
2171
- // fields on their controllers.
2172
2298
  __publicField(this, "teardowns", new TeardownStack());
2173
- // Pre-computed transform template — built once per engine since `mode` is
2174
- // fixed for its lifetime. Avoids the 4-arm switch at 60-120 Hz.
2299
+ __publicField(this, "anchors", []);
2300
+ __publicField(this, "anchorHost", null);
2301
+ __publicField(this, "stackZ", 100);
2302
+ __publicField(this, "detachScrimStages", null);
2303
+ __publicField(this, "stackEffectEnabled", false);
2304
+ __publicField(this, "stackEffectPrimed", false);
2175
2305
  __publicField(this, "transformTemplate");
2176
2306
  const resolved = resolveEngineOptions(opts, {
2177
2307
  shouldApplyInertSiblings: () => {
@@ -2186,7 +2316,8 @@ var BottomSheetEngine = class {
2186
2316
  this.handle = opts.handle ?? opts.element;
2187
2317
  this.scrollContainer = opts.scrollContainer;
2188
2318
  this.backdrop = opts.backdrop;
2189
- this.screenComponent = opts.screenComponent;
2319
+ this.screenComponent = opts.scrim ?? opts.screenComponent;
2320
+ this.scrimParent = this.screenComponent?.parentElement ?? null;
2190
2321
  this.mode = resolved.mode;
2191
2322
  this.transformTemplate = buildTransformTemplate(this.mode);
2192
2323
  this.aria = new AriaSliderWriter(this.handle, this.mode);
@@ -2204,6 +2335,9 @@ var BottomSheetEngine = class {
2204
2335
  getResolvedSnaps: () => this.snaps.getResolvedSnaps().slice(),
2205
2336
  snapTo: (id) => {
2206
2337
  void this.snapTo(id);
2338
+ },
2339
+ close: () => {
2340
+ void this.close();
2207
2341
  }
2208
2342
  },
2209
2343
  resolved.scrim
@@ -2214,8 +2348,6 @@ var BottomSheetEngine = class {
2214
2348
  getRootEl: () => this.rootEl,
2215
2349
  applySize: (size) => this.applySize(size),
2216
2350
  getSize: () => this.size,
2217
- // Optional chain protects synthetic test paths that drive animateTo
2218
- // before attach() (gesture is constructed there).
2219
2351
  isDragging: () => this.gesture?.isDragging ?? false
2220
2352
  },
2221
2353
  resolved.animation
@@ -2229,6 +2361,7 @@ var BottomSheetEngine = class {
2229
2361
  );
2230
2362
  this.closeOnBack = resolved.closeOnBack;
2231
2363
  this.routedTo = opts.routedTo;
2364
+ this.stackEffectEnabled = opts.stackEffect ?? false;
2232
2365
  this.snapPointsRaw = opts.snapPoints;
2233
2366
  this.persistKey = opts.persistKey;
2234
2367
  if (opts.linkedSheets) {
@@ -2241,7 +2374,7 @@ var BottomSheetEngine = class {
2241
2374
  this.snapPointsRaw,
2242
2375
  resolved.initialAllowed,
2243
2376
  this.mode,
2244
- () => this.handle.offsetHeight,
2377
+ () => layoutAxis(this.mode) === "width" ? this.handle.offsetWidth : this.handle.offsetHeight,
2245
2378
  (maxAxisSize) => {
2246
2379
  this.element.style[layoutAxis(this.mode)] = `${maxAxisSize}px`;
2247
2380
  }
@@ -2320,21 +2453,6 @@ var BottomSheetEngine = class {
2320
2453
  on(event, fn) {
2321
2454
  return this.bus.on(event, fn);
2322
2455
  }
2323
- /**
2324
- * Register a plugin. `install` runs synchronously; if it returns a function,
2325
- * it's captured for teardown during `destroy()`. No-op if already destroyed
2326
- * — installing into a torn-down engine would leak listeners.
2327
- *
2328
- * Throws from `plugin.install` are caught and rethrown via `queueMicrotask`
2329
- * so a buggy plugin can't take the engine down with it: the engine stays
2330
- * usable, sibling plugins still install, and the error surfaces in dev tools
2331
- * on the next tick instead of being swallowed. Mirrors the per-fn isolation
2332
- * model used for teardowns.
2333
- *
2334
- * Plugins that accept the second `scope` argument get transactional cleanup:
2335
- * push partial-install cleanups via `scope.add(fn)` and the engine drains
2336
- * them on throw, OR merges them into destroy-time teardowns on success.
2337
- */
2338
2456
  use(plugin) {
2339
2457
  if (this.destroyed) {
2340
2458
  console.warn(
@@ -2344,9 +2462,6 @@ var BottomSheetEngine = class {
2344
2462
  }
2345
2463
  const scoped = [];
2346
2464
  const scope = {
2347
- // typeof check — not just truthiness — so a JS-only plugin pushing a
2348
- // bogus value is silently rejected instead of queueing a confusing
2349
- // TypeError on destroy() drain.
2350
2465
  add: (fn) => {
2351
2466
  if (typeof fn === "function") scoped.push(fn);
2352
2467
  }
@@ -2375,36 +2490,13 @@ var BottomSheetEngine = class {
2375
2490
  }
2376
2491
  return this;
2377
2492
  }
2378
- /**
2379
- * Replace the linked-sheets list at runtime. Use when peer engines aren't
2380
- * available at construction time (mounted later in the tree). The wiring
2381
- * to the `open` event is independent of this list — it reads `linkedSheets`
2382
- * lazily on emit, so this setter is a simple field swap.
2383
- */
2384
2493
  setLinkedSheets(sheets) {
2385
2494
  if (this.destroyed) return;
2386
2495
  this.linkedSheets = sheets;
2387
2496
  }
2388
- /** Read-only view of the current allow-list, for cross-sheet target negotiation. */
2389
2497
  getAllowedIds() {
2390
2498
  return this.snaps.getAllowedIds().slice();
2391
2499
  }
2392
- /**
2393
- * Animate to a snap point by id. Resolves when the animation completes.
2394
- *
2395
- * Two call shapes:
2396
- * ```ts
2397
- * engine.snapTo("full"); // default velocity
2398
- * engine.snapTo("full", 0.5); // px/ms initial velocity
2399
- * engine.snapTo("full", { velocity: 0.5, signal }); // abortable
2400
- * ```
2401
- *
2402
- * When `opts.signal` is provided, calling `controller.abort()` cancels the
2403
- * in-flight tween/spring and the returned promise resolves without firing
2404
- * `snap`. The captured `AbortSignal` is checked after each await so external
2405
- * aborts ride the same cycle invariant as internal ones (resize, destroy,
2406
- * concurrent snapTo).
2407
- */
2408
2500
  async snapTo(id, velocityOrOpts = 0, _skipBeforeSnap = false) {
2409
2501
  if (this.destroyed) return;
2410
2502
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2463,16 +2555,6 @@ var BottomSheetEngine = class {
2463
2555
  this.handleClose();
2464
2556
  }
2465
2557
  }
2466
- /**
2467
- * Drive the sheet to an arbitrary px size, bypassing snap matching.
2468
- * Useful for preview scrubs, animation chaining, or programmatic gestures.
2469
- * Animates with the configured spring/tween. Resolves when the animation
2470
- * completes. Does NOT update `activeId` — call `snapTo(id)` after to commit.
2471
- *
2472
- * Unlike `snapTo`, this does not fire `before-snap` (it isn't a snap commit)
2473
- * nor `snap` (the sheet isn't settling to a known snap). `progress` events
2474
- * still fire via applySize. Target is clamped to `[0, maxAxisSize]`.
2475
- */
2476
2558
  async dragTo(targetSize, velocityOrOpts = 0) {
2477
2559
  if (this.destroyed) return;
2478
2560
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2502,28 +2584,13 @@ var BottomSheetEngine = class {
2502
2584
  }
2503
2585
  setAllowed(ids, snap) {
2504
2586
  if (this.destroyed) return;
2505
- const willSnap = snap !== void 0 && ids.includes(snap) || !ids.includes(this.activeId) && ids.length > 0;
2506
- if (!willSnap) {
2507
- this.animation.cancel();
2508
- this.newCycle();
2509
- }
2510
2587
  this.snaps.setAllowedIds(ids);
2511
2588
  this.updateAriaSlider();
2512
2589
  if (snap && ids.includes(snap)) {
2513
- void this.snapTo(
2514
- snap,
2515
- 0,
2516
- /* internal */
2517
- true
2518
- );
2590
+ void this.snapTo(snap, 0, true);
2519
2591
  } else if (!ids.includes(this.activeId)) {
2520
2592
  const fallback = ids[0];
2521
- if (fallback) void this.snapTo(
2522
- fallback,
2523
- 0,
2524
- /* internal */
2525
- true
2526
- );
2593
+ if (fallback) void this.snapTo(fallback, 0, true);
2527
2594
  }
2528
2595
  }
2529
2596
  setBackdropRange(range) {
@@ -2541,21 +2608,6 @@ var BottomSheetEngine = class {
2541
2608
  setScrimInteractive(interactive) {
2542
2609
  this.scrim.setScrimInteractive(interactive);
2543
2610
  }
2544
- /**
2545
- * Batch-update multiple scrim fields in one call. Apply order is:
2546
- * 1. `preset` — fills baseline color/blur/range/interactive.
2547
- * 2. `mode` — re-positions the scrim (fixed inset / clear / hide).
2548
- * 3. `enabled` — flips the opacity contribution on/off.
2549
- * 4. `tapToClose` — installs/tears the click listener.
2550
- * 5. `color` / `blur` / `interactive` / `range` — explicit overrides.
2551
- * 6. ONE `applySize` so the per-frame opacity stays consistent.
2552
- *
2553
- * Performance: collecting changes here (vs calling individual setters)
2554
- * avoids repeated `applySize` calls during a multi-field swap. Setters
2555
- * called here suppress their own internal `applySize` either by passing
2556
- * through fast-paths (mode/enabled no-op when value unchanged) or by
2557
- * being visual-only (color/blur/interactive don't influence opacity).
2558
- */
2559
2611
  setScrim(opts) {
2560
2612
  this.scrim.setScrim(opts, () => this.applySize(this.size));
2561
2613
  }
@@ -2571,11 +2623,79 @@ var BottomSheetEngine = class {
2571
2623
  setScrimOverlay(opts) {
2572
2624
  return this.scrim.setScrimOverlay(opts);
2573
2625
  }
2574
- /**
2575
- * Read-only snapshot of scrim state. Use the `setScrim*` setters to mutate.
2576
- * Stable public API — internals (e.g. cached opacity sentinels) are NOT
2577
- * exposed so consumers can't accidentally couple to ScrimController shape.
2578
- */
2626
+ addAnchor(opts) {
2627
+ if (this.destroyed || typeof document === "undefined") return () => {
2628
+ };
2629
+ if (!this.anchorHost) {
2630
+ this.anchorHost = this.rootEl ?? this.element.parentElement ?? document.body;
2631
+ this.anchorHost.style.setProperty("--bs-size", `${this.size}px`);
2632
+ this.anchorHost.style.setProperty(
2633
+ "--bs-progress",
2634
+ String(this.computeProgress(this.size))
2635
+ );
2636
+ }
2637
+ const handle = attachAnchor(
2638
+ {
2639
+ mode: this.mode,
2640
+ host: this.anchorHost,
2641
+ getState: () => ({
2642
+ activeId: this.activeId,
2643
+ size: this.size,
2644
+ progress: this.computeProgress(this.size)
2645
+ }),
2646
+ on: (event, fn) => this.on(event, fn),
2647
+ isDestroyed: () => this.destroyed
2648
+ },
2649
+ opts
2650
+ );
2651
+ handle.syncZ(this.stackZ + 1);
2652
+ this.anchors.push(handle);
2653
+ const detach = () => {
2654
+ const idx = this.anchors.indexOf(handle);
2655
+ if (idx === -1) return;
2656
+ this.anchors.splice(idx, 1);
2657
+ handle.detach();
2658
+ };
2659
+ this.teardowns.add(detach);
2660
+ return detach;
2661
+ }
2662
+ setScrimStages(opts) {
2663
+ if (this.destroyed || typeof document === "undefined") return () => {
2664
+ };
2665
+ this.detachScrimStages?.();
2666
+ this.detachScrimStages = null;
2667
+ if (!opts) return () => {
2668
+ };
2669
+ const host = this.screenComponent?.parentElement;
2670
+ if (!host) {
2671
+ console.warn(
2672
+ "[BottomSheet] setScrimStages: no scrim element mounted \u2014 pass `scrim` to the engine first."
2673
+ );
2674
+ return () => {
2675
+ };
2676
+ }
2677
+ const detachInstalled = installScrimStages(
2678
+ {
2679
+ mode: this.mode,
2680
+ host,
2681
+ getState: () => ({
2682
+ activeId: this.activeId,
2683
+ size: this.size,
2684
+ progress: this.computeProgress(this.size)
2685
+ }),
2686
+ on: (event, fn) => this.on(event, fn),
2687
+ isDestroyed: () => this.destroyed
2688
+ },
2689
+ opts
2690
+ );
2691
+ const detach = () => {
2692
+ if (this.detachScrimStages === detach) this.detachScrimStages = null;
2693
+ detachInstalled();
2694
+ };
2695
+ this.detachScrimStages = detach;
2696
+ this.teardowns.add(() => this.detachScrimStages?.());
2697
+ return detach;
2698
+ }
2579
2699
  getScrimState() {
2580
2700
  return { mode: this.scrim.scrimMode, enabled: this.scrim.scrimEnabled };
2581
2701
  }
@@ -2621,24 +2741,11 @@ var BottomSheetEngine = class {
2621
2741
  emit(event, payload) {
2622
2742
  this.bus.emit(event, payload);
2623
2743
  }
2624
- /**
2625
- * Abort the current cycle's signal and replace the controller. Returns the
2626
- * fresh signal so the caller can capture it locally before any await — when
2627
- * a future newCycle() / destroy aborts our controller, the captured signal
2628
- * flips to aborted and post-await guards short-circuit.
2629
- */
2630
2744
  newCycle() {
2631
2745
  this.currentAbort.abort();
2632
2746
  this.currentAbort = new AbortController();
2633
2747
  return this.currentAbort.signal;
2634
2748
  }
2635
- /**
2636
- * Fire `before-snap` and report whether a listener cancelled it. The
2637
- * `cancel()` callback exposed to listeners is frozen after the synchronous
2638
- * emit phase so async callers can't silently miss — they get a console
2639
- * warning. Returns `true` when cancelled so callers can early-return /
2640
- * spring-back without re-implementing the cancel/frozen pattern.
2641
- */
2642
2749
  emitBeforeSnap(target, previousId) {
2643
2750
  let cancelled = false;
2644
2751
  let frozen = false;
@@ -2672,59 +2779,54 @@ var BottomSheetEngine = class {
2672
2779
  this.snaps.setRaw(this.snapPointsRaw);
2673
2780
  this.scrim.invalidateOpacityCache();
2674
2781
  }
2675
- /**
2676
- * Memoised `allowedRange()` — delegates to SnapResolver which owns the cache.
2677
- * Engine wrapper kept so existing call sites don't all need updating;
2678
- * resolver-level memoization survives across method invocations.
2679
- */
2680
2782
  getAllowedRange() {
2681
2783
  return this.snaps.getAllowedRange();
2682
2784
  }
2683
- /**
2684
- * Write ARIA slider value attributes onto the handle so the engine itself
2685
- * is the source of truth for screen-reader announcements. Called from the
2686
- * constructor and from every code path that mutates `activeId` /
2687
- * `allowedIds`. WAI-ARIA 1.2 requires `aria-valuenow` for `role="slider"` —
2688
- * without it screen readers can't announce drag/snap progress to keyboard
2689
- * + AT users (WCAG 4.1.2 Name, Role, Value).
2690
- *
2691
- * Engine does NOT set `role="slider"` itself — that's the adapter's or
2692
- * consumer markup's responsibility. We only decorate with dynamic values.
2693
- *
2694
- * Empty allowed-list is malformed input (settable via `setAllowed([])`);
2695
- * short-circuit to remove ARIA value attributes so screen readers see a
2696
- * no-value slider rather than a degenerate `min=max=now=0` (WAI-ARIA 1.2
2697
- * requires `valuemax > valuemin` for `role="slider"`).
2698
- */
2785
+ allowedIdsBySize() {
2786
+ return this.snaps.getAllowedIds().slice().sort(
2787
+ (a, b) => (this.snaps.findById(a)?.size ?? 0) - (this.snaps.findById(b)?.size ?? 0)
2788
+ );
2789
+ }
2699
2790
  updateAriaSlider() {
2700
- this.aria.setValue(this.snaps.getAllowedIds(), this.activeId);
2791
+ this.aria.setValue(this.allowedIdsBySize(), this.activeId);
2701
2792
  }
2702
2793
  registerInStack() {
2703
2794
  this.teardowns.add(sheetStack.push({
2704
2795
  id: this.id,
2705
2796
  setZIndex: (z) => {
2797
+ this.stackZ = z;
2706
2798
  this.element.style.zIndex = String(z);
2707
2799
  if (this.backdrop) this.backdrop.style.zIndex = String(z - 1);
2800
+ for (const anchor of this.anchors) anchor.syncZ(z + 1);
2708
2801
  },
2709
2802
  setIsTop: (isTop) => {
2710
2803
  this.isTopSheet = isTop;
2711
2804
  if (this.backdrop) {
2712
2805
  this.backdrop.style.display = isTop ? "" : "none";
2713
2806
  }
2714
- }
2807
+ },
2808
+ isOpen: () => this.size > 0,
2809
+ setDepth: (depth) => this.applyStackDepth(depth)
2715
2810
  }));
2716
2811
  }
2812
+ applyStackDepth(depth) {
2813
+ if (!this.stackEffectEnabled) return;
2814
+ this.element.setAttribute("data-stack-depth", String(depth));
2815
+ if (!this.stackEffectPrimed) {
2816
+ this.stackEffectPrimed = true;
2817
+ const isVertical = this.mode === "bottom" || this.mode === "top";
2818
+ this.element.style.transformOrigin = isVertical ? "50% 100%" : "0% 50%";
2819
+ this.element.style.transition = "scale 320ms cubic-bezier(0.29, 1.04, 0.84, 0.99)";
2820
+ }
2821
+ const scale = Math.max(1 - depth * 0.04, 0.86);
2822
+ this.element.style.scale = scale === 1 ? "" : String(scale);
2823
+ }
2717
2824
  attach() {
2718
2825
  this.gesture = new GestureController({
2719
2826
  handle: this.handle,
2720
2827
  element: this.element,
2721
2828
  mode: this.mode,
2722
2829
  getRoot: () => this.rootEl,
2723
- // Single coalesced reader — narrows the DI surface and removes 3 getter
2724
- // dispatches per onMove frame at 60-120Hz. Returns a SHARED buffer
2725
- // (this.dragContextBuf) — see field JSDoc for the retention contract.
2726
- // Inner `range` object is mutated in place too so callers caching its
2727
- // identity between frames stay valid.
2728
2830
  getDragContext: () => {
2729
2831
  const buf = this.dragContextBuf;
2730
2832
  const r = this.getAllowedRange();
@@ -2778,7 +2880,7 @@ var BottomSheetEngine = class {
2778
2880
  container: this.scrollContainer,
2779
2881
  isDragging: () => this.gesture?.isDragging ?? false,
2780
2882
  isAnimating: () => this.animation.isAnimating,
2781
- getAllowedIds: () => this.snaps.getAllowedIds(),
2883
+ getAllowedIds: () => this.allowedIdsBySize(),
2782
2884
  getActiveId: () => this.activeId,
2783
2885
  snapTo: (id) => {
2784
2886
  void this.snapTo(id);
@@ -2807,11 +2909,13 @@ var BottomSheetEngine = class {
2807
2909
  },
2808
2910
  newCycle: () => {
2809
2911
  this.newCycle();
2810
- }
2912
+ },
2913
+ isAnimating: () => this.animation.isAnimating,
2914
+ resyncAfterCancel: () => this.resyncAfterResize()
2811
2915
  }));
2812
2916
  if (this.lifecycle.closeOnEscape) {
2813
2917
  const onKey = (e) => {
2814
- if (this.destroyed) return;
2918
+ if (this.destroyed || e.defaultPrevented) return;
2815
2919
  if (e.key === "Escape" && this.isTopSheet && this.size > 0) {
2816
2920
  void this.close();
2817
2921
  }
@@ -2843,7 +2947,7 @@ var BottomSheetEngine = class {
2843
2947
  handle: this.handle,
2844
2948
  mode: this.mode,
2845
2949
  isDestroyed: () => this.destroyed,
2846
- getAllowedIds: () => this.snaps.getAllowedIds(),
2950
+ getAllowedIds: () => this.allowedIdsBySize(),
2847
2951
  getActiveId: () => this.activeId,
2848
2952
  snapTo: (id) => {
2849
2953
  void this.snapTo(id);
@@ -2864,6 +2968,7 @@ var BottomSheetEngine = class {
2864
2968
  });
2865
2969
  if (!target) return;
2866
2970
  const previousId = this.activeId;
2971
+ if (target.id === previousId && target.size === this.size) return;
2867
2972
  if (this.emitBeforeSnap(target, previousId)) {
2868
2973
  const restore = this.snaps.findById(previousId);
2869
2974
  if (restore) {
@@ -2872,6 +2977,7 @@ var BottomSheetEngine = class {
2872
2977
  return;
2873
2978
  }
2874
2979
  const previousSize = this.size;
2980
+ const previousSnapSize = this.snaps.findById(previousId)?.size;
2875
2981
  this.scrollCache.cache(previousId, previousSize, target.size);
2876
2982
  this.activeId = target.id;
2877
2983
  void this.animation.animateTo(target.size, velocity).then(() => {
@@ -2880,9 +2986,10 @@ var BottomSheetEngine = class {
2880
2986
  this.updateAriaSlider();
2881
2987
  this.emit("snap", { id: target.id, size: target.size });
2882
2988
  this.haptic();
2883
- if (previousId === "closed" && target.size > 0) {
2989
+ if (previousSnapSize === 0 && target.size > 0) {
2884
2990
  this.emit("open", { id: target.id });
2885
2991
  this.handleOpen();
2992
+ notifyLinkedSheets(this.linkedSheets, this);
2886
2993
  }
2887
2994
  if (target.size === 0) {
2888
2995
  this.emit("close", void 0);
@@ -2890,7 +2997,6 @@ var BottomSheetEngine = class {
2890
2997
  }
2891
2998
  });
2892
2999
  }
2893
- /** Trigger a brief haptic tick (iOS Safari + Android Chrome support). */
2894
3000
  haptic() {
2895
3001
  if (typeof navigator !== "undefined" && "vibrate" in navigator) {
2896
3002
  try {
@@ -2906,6 +3012,12 @@ var BottomSheetEngine = class {
2906
3012
  style.transform = this.transformTemplate(offset);
2907
3013
  if (this.sizeWriteSentinel.shouldWrite(size, SIZE_WRITE_EPSILON)) {
2908
3014
  style.setProperty("--bs-size", `${size}px`);
3015
+ if (this.scrimParent) {
3016
+ this.scrimParent.style.setProperty("--bs-size", `${size}px`);
3017
+ }
3018
+ if (this.anchorHost && this.anchorHost !== this.scrimParent) {
3019
+ this.anchorHost.style.setProperty("--bs-size", `${size}px`);
3020
+ }
2909
3021
  }
2910
3022
  const progress = this.computeProgress(size);
2911
3023
  const progressChanged = this.progressWriteSentinel.shouldWrite(
@@ -2914,6 +3026,9 @@ var BottomSheetEngine = class {
2914
3026
  );
2915
3027
  if (progressChanged) {
2916
3028
  style.setProperty("--bs-progress", String(progress));
3029
+ if (this.anchorHost) {
3030
+ this.anchorHost.style.setProperty("--bs-progress", String(progress));
3031
+ }
2917
3032
  }
2918
3033
  this.scrim.applyOpacity(progress, progressChanged);
2919
3034
  if (progressChanged && this.bus.listenerCount("progress") > 0) {
@@ -2928,6 +3043,7 @@ var BottomSheetEngine = class {
2928
3043
  return Math.min(Math.max((size - min) / (max - min), 0), 1);
2929
3044
  }
2930
3045
  handleOpen() {
3046
+ sheetStack.promote(this.id);
2931
3047
  try {
2932
3048
  this.lifecycle.install();
2933
3049
  } catch (err) {
@@ -2941,15 +3057,36 @@ var BottomSheetEngine = class {
2941
3057
  });
2942
3058
  }
2943
3059
  }
3060
+ resyncAfterResize() {
3061
+ if (this.destroyed) return;
3062
+ const target = this.snaps.findById(this.activeId);
3063
+ if (!target) return;
3064
+ this.updateAriaSlider();
3065
+ this.emit("snap", { id: target.id, size: target.size });
3066
+ if (target.size > 0 && !this.lifecycle.isInstalled) {
3067
+ this.emit("open", { id: target.id });
3068
+ this.handleOpen();
3069
+ notifyLinkedSheets(this.linkedSheets, this);
3070
+ } else if (target.size === 0 && this.lifecycle.isInstalled) {
3071
+ this.emit("close", void 0);
3072
+ this.handleClose();
3073
+ }
3074
+ }
2944
3075
  handleClose() {
2945
3076
  this.lifecycle.release();
3077
+ sheetStack.update();
2946
3078
  }
2947
3079
  };
3080
+
3081
+ // src/solid/index.tsx
3082
+ var _tmpl$ = /* @__PURE__ */ template(`<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>`);
3083
+ var _tmpl$2 = /* @__PURE__ */ template(`<div class=bs-backdrop>`);
2948
3084
  var BottomSheet = (props) => {
2949
3085
  let sheetEl;
2950
3086
  let handleEl;
2951
3087
  let contentEl;
2952
3088
  let backdropEl;
3089
+ let scrimEl;
2953
3090
  const initialActive = () => props.initial ?? props.snapPoints[0]?.id ?? "default";
2954
3091
  const [state, setState] = createSignal({
2955
3092
  size: 0,
@@ -2966,9 +3103,8 @@ var BottomSheet = (props) => {
2966
3103
  handle: handleEl,
2967
3104
  scrollContainer: contentEl,
2968
3105
  backdrop: props.backdrop !== false ? backdropEl : void 0,
3106
+ scrim: scrimEl,
2969
3107
  mode: props.mode ?? "bottom",
2970
- // Widen literal-id back to string at the engine boundary — the engine
2971
- // is string-typed at runtime, the TId narrowing is a TS-only veneer.
2972
3108
  snapPoints: props.snapPoints,
2973
3109
  allowed: props.allowed,
2974
3110
  initial: props.initial,
@@ -2986,7 +3122,9 @@ var BottomSheet = (props) => {
2986
3122
  const sync = () => {
2987
3123
  if (!engine) return;
2988
3124
  const s = engine.state;
2989
- setState({ ...s });
3125
+ setState({
3126
+ ...s
3127
+ });
2990
3128
  props.onChange?.(s);
2991
3129
  };
2992
3130
  sync();
@@ -3011,49 +3149,54 @@ var BottomSheet = (props) => {
3011
3149
  engine = null;
3012
3150
  props.engineRef?.(null);
3013
3151
  });
3152
+ createEffect(on(() => props.snapPoints, (points, prev) => {
3153
+ if (!engine || prev === void 0) return;
3154
+ engine.setSnapPoints(points, props.allowed);
3155
+ }, {
3156
+ defer: true
3157
+ }));
3158
+ createEffect(on(() => props.allowed, (allowed, prev) => {
3159
+ if (!engine || prev === void 0) return;
3160
+ if (allowed) engine.setAllowed(allowed);
3161
+ }, {
3162
+ defer: true
3163
+ }));
3014
3164
  const showBackdrop = createMemo(() => props.backdrop !== false);
3015
3165
  const mode = createMemo(() => props.mode ?? "bottom");
3016
3166
  const ariaLabel = createMemo(() => props.ariaLabel ?? "Bottom sheet");
3017
- return /* @__PURE__ */ jsxs("div", { class: "bs-root", children: [
3018
- showBackdrop() ? /* @__PURE__ */ jsx("div", { class: "bs-backdrop", ref: (el) => backdropEl = el }) : null,
3019
- /* @__PURE__ */ jsxs(
3020
- "section",
3021
- {
3022
- class: "bs-sheet",
3023
- ref: (el) => sheetEl = el,
3024
- "data-mode": mode(),
3025
- "data-active": state().activeId,
3026
- role: "dialog",
3027
- "aria-modal": props.focusTrap ? "true" : void 0,
3028
- "aria-label": ariaLabel(),
3029
- children: [
3030
- /* @__PURE__ */ jsx(
3031
- "div",
3032
- {
3033
- class: "bs-handle",
3034
- ref: (el) => handleEl = el,
3035
- role: "slider",
3036
- tabIndex: 0,
3037
- "aria-label": "Resize sheet",
3038
- children: props.header
3039
- }
3040
- ),
3041
- /* @__PURE__ */ jsx(
3042
- "div",
3043
- {
3044
- class: "bs-content",
3045
- ref: (el) => contentEl = el,
3046
- tabIndex: 0,
3047
- role: "region",
3048
- "aria-label": "Sheet content",
3049
- children: props.children
3050
- }
3051
- ),
3052
- /* @__PURE__ */ jsx("span", { class: "bs-sr-only", role: "status", "aria-live": "polite", children: state().activeId })
3053
- ]
3054
- }
3055
- )
3056
- ] });
3167
+ return (() => {
3168
+ var _el$ = _tmpl$(), _el$2 = _el$.firstChild, _el$3 = _el$2.nextSibling, _el$4 = _el$3.firstChild, _el$5 = _el$4.nextSibling, _el$6 = _el$5.nextSibling;
3169
+ insert(_el$, (() => {
3170
+ var _c$ = memo(() => !!showBackdrop());
3171
+ return () => _c$() ? (() => {
3172
+ var _el$7 = _tmpl$2();
3173
+ use((el) => backdropEl = el, _el$7);
3174
+ return _el$7;
3175
+ })() : null;
3176
+ })(), _el$2);
3177
+ use((el) => scrimEl = el, _el$2);
3178
+ insert(_el$2, () => props.screen);
3179
+ use((el) => sheetEl = el, _el$3);
3180
+ use((el) => handleEl = el, _el$4);
3181
+ insert(_el$4, () => props.header);
3182
+ use((el) => contentEl = el, _el$5);
3183
+ insert(_el$5, () => props.children);
3184
+ insert(_el$6, () => state().activeId);
3185
+ effect((_p$) => {
3186
+ var _v$ = mode(), _v$2 = state().activeId, _v$3 = props.focusTrap ? "true" : void 0, _v$4 = ariaLabel();
3187
+ _v$ !== _p$.e && setAttribute(_el$3, "data-mode", _p$.e = _v$);
3188
+ _v$2 !== _p$.t && setAttribute(_el$3, "data-active", _p$.t = _v$2);
3189
+ _v$3 !== _p$.a && setAttribute(_el$3, "aria-modal", _p$.a = _v$3);
3190
+ _v$4 !== _p$.o && setAttribute(_el$3, "aria-label", _p$.o = _v$4);
3191
+ return _p$;
3192
+ }, {
3193
+ e: void 0,
3194
+ t: void 0,
3195
+ a: void 0,
3196
+ o: void 0
3197
+ });
3198
+ return _el$;
3199
+ })();
3057
3200
  };
3058
3201
 
3059
3202
  export { BottomSheet };