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