@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/element.cjs CHANGED
@@ -33,6 +33,18 @@ var init_sheetStack = __esm({
33
33
  this.entries.splice(idx, 1);
34
34
  this.recompute();
35
35
  }
36
+ promote(id) {
37
+ const idx = this.entries.findIndex((e) => e.id === id);
38
+ if (idx === -1) return;
39
+ if (idx !== this.entries.length - 1) {
40
+ const [entry] = this.entries.splice(idx, 1);
41
+ this.entries.push(entry);
42
+ }
43
+ this.recompute();
44
+ }
45
+ update() {
46
+ this.recompute();
47
+ }
36
48
  size() {
37
49
  return this.entries.length;
38
50
  }
@@ -40,9 +52,24 @@ var init_sheetStack = __esm({
40
52
  this.entries = [];
41
53
  }
42
54
  recompute() {
55
+ let top = null;
56
+ const open = [];
57
+ for (const entry of this.entries) {
58
+ if (!entry.isOpen || entry.isOpen()) {
59
+ top = entry;
60
+ open.push(entry);
61
+ }
62
+ }
63
+ if (!top && this.entries.length > 0) {
64
+ top = this.entries[this.entries.length - 1];
65
+ }
43
66
  this.entries.forEach((entry, i) => {
44
67
  entry.setZIndex(BASE_Z + i * STEP);
45
- entry.setIsTop(i === this.entries.length - 1);
68
+ entry.setIsTop(entry === top);
69
+ if (entry.setDepth) {
70
+ const openIdx = open.indexOf(entry);
71
+ entry.setDepth(openIdx === -1 ? 0 : open.length - 1 - openIdx);
72
+ }
46
73
  });
47
74
  }
48
75
  };
@@ -272,11 +299,6 @@ var init_snap_resolver = __esm({
272
299
  this.allowedIds = allowed ?? raw.map((p) => p.id);
273
300
  this.recompute();
274
301
  }
275
- /**
276
- * Re-resolve raw → resolved against the current viewport / fit-measure,
277
- * recompute maxAxisSize, invalidate range cache, notify engine. Called on
278
- * resize, orientationchange, visualViewport changes, and snap-list swaps.
279
- */
280
302
  recompute() {
281
303
  this.resolved = resolveSnapList(this.raw, this.mode, this.measureFit);
282
304
  this.maxAxisSize = this.resolved.reduce(
@@ -286,15 +308,10 @@ var init_snap_resolver = __esm({
286
308
  this.rangeCache = null;
287
309
  this.onMaxAxisSizeChange?.(this.maxAxisSize);
288
310
  }
289
- /** Replace the raw snap list and recompute. Engine calls on `setSnapPoints`. */
290
311
  setRaw(raw) {
291
312
  this.raw = raw;
292
313
  this.recompute();
293
314
  }
294
- /**
295
- * Replace the allow-list. Range cache depends on `(resolvedSnaps ×
296
- * allowedIds)` so flipping ids alone invalidates without a full recompute.
297
- */
298
315
  setAllowedIds(ids) {
299
316
  this.allowedIds = ids;
300
317
  this.rangeCache = null;
@@ -302,11 +319,6 @@ var init_snap_resolver = __esm({
302
319
  findById(id) {
303
320
  return findById(id, this.resolved);
304
321
  }
305
- /**
306
- * Pure drag-settle target resolution. Engine passes the gesture-specific
307
- * fields (delta, velocity, pointerKind, flickVelocity, dragThreshold) plus
308
- * `size`/`activeId` since those are still engine-owned.
309
- */
310
322
  findDragSettleTarget(args) {
311
323
  return findDragSettleTarget({
312
324
  ...args,
@@ -315,18 +327,9 @@ var init_snap_resolver = __esm({
315
327
  maxAxisSize: this.maxAxisSize
316
328
  });
317
329
  }
318
- /**
319
- * Memoised allowed-range query. Twice-per-drag-frame (gesture onMove +
320
- * applySize→computeProgress); cache collapses 2× iteration of resolvedSnaps
321
- * to one recompute per geometry/allow-list change.
322
- */
323
330
  getAllowedRange() {
324
331
  return this.rangeCache ?? (this.rangeCache = allowedRange(this.resolved, this.allowedIds));
325
332
  }
326
- /**
327
- * Read-only view of resolved snaps. Returned as `readonly` to discourage
328
- * accidental mutation by feature install sites that close over this list.
329
- */
330
333
  getResolvedSnaps() {
331
334
  return this.resolved;
332
335
  }
@@ -336,18 +339,6 @@ var init_snap_resolver = __esm({
336
339
  getMaxAxisSize() {
337
340
  return this.maxAxisSize;
338
341
  }
339
- /**
340
- * External clamp override. The resize-observer feature compares
341
- * `getMaxAxisSize()` against the live viewport and shrinks if the largest
342
- * resolved snap exceeds it (mobile address-bar collapse / soft-keyboard
343
- * window resize). The next `recompute()` overwrites this clamp from the
344
- * resolved-snap reduction.
345
- *
346
- * Does NOT invalidate the range cache — viewport clamping doesn't change
347
- * `(resolvedSnaps × allowedIds) → range`. DOES fire `onMaxAxisSizeChange`
348
- * so the resolver owns the layout-axis style write end-to-end (symmetric
349
- * with `recompute()` — feature call sites no longer duplicate the write).
350
- */
351
342
  setMaxAxisSize(n) {
352
343
  this.maxAxisSize = n;
353
344
  this.onMaxAxisSizeChange?.(n);
@@ -400,14 +391,17 @@ function installAutoCollapse(deps) {
400
391
  const fire = () => {
401
392
  if (deps.isDestroyed()) return;
402
393
  if (deps.isDragging()) return;
403
- const allowed = deps.getAllowedIds();
404
- const target = allowed.find((id) => {
394
+ const active = deps.resolveSnap(deps.getActiveId());
395
+ if (!active || active.size === 0) return;
396
+ let target = null;
397
+ for (const id of deps.getAllowedIds()) {
405
398
  const snap = deps.resolveSnap(id);
406
- return snap !== null && snap.size > 0;
407
- });
399
+ if (!snap || snap.size <= 0) continue;
400
+ if (!target || snap.size < target.size) target = snap;
401
+ }
408
402
  if (!target) return;
409
- if (target === deps.getActiveId()) return;
410
- deps.snapTo(target);
403
+ if (target.id === active.id || target.size >= active.size) return;
404
+ deps.snapTo(target.id);
411
405
  };
412
406
  const reset = () => {
413
407
  clearTimer();
@@ -418,6 +412,7 @@ function installAutoCollapse(deps) {
418
412
  }, ms);
419
413
  };
420
414
  const unsubscribe = deps.on("snap", () => reset());
415
+ if ((deps.resolveSnap(deps.getActiveId())?.size ?? 0) > 0) reset();
421
416
  return () => {
422
417
  clearTimer();
423
418
  unsubscribe();
@@ -434,6 +429,7 @@ function notifyLinkedSheets(sheets, self) {
434
429
  for (const linked of sheets) {
435
430
  if (linked === self) continue;
436
431
  try {
432
+ if (linked.state.size === 0) continue;
437
433
  const ids = linked.getAllowedIds();
438
434
  const target = ids.find((id) => id !== "closed");
439
435
  if (target && target !== linked.state.activeId) {
@@ -484,7 +480,7 @@ function installRoute(deps) {
484
480
  const onPop = (event) => {
485
481
  if (deps.isDestroyed()) return;
486
482
  const state = event.state;
487
- if (state && state.__bsRouted === target && deps.getSize() > 0) {
483
+ if ((!state || state.__bsRouted !== target) && deps.getSize() > 0) {
488
484
  routePushed = false;
489
485
  void deps.close();
490
486
  }
@@ -652,13 +648,17 @@ function installResizeObserver(deps) {
652
648
  if (viewportSize2 > 0 && viewportSize2 < max) {
653
649
  deps.setMaxAxisSize(viewportSize2);
654
650
  }
651
+ const wasAnimating = deps.isAnimating?.() ?? false;
655
652
  deps.cancelInFlight();
656
653
  deps.newCycle();
657
654
  const current = deps.resolveActiveSnap();
658
655
  const newMax = deps.getMaxAxisSize();
659
656
  const targetSize = current ? Math.min(current.size, newMax) : Math.min(deps.getSize(), newMax);
660
657
  deps.setSize(targetSize);
661
- if (current && !deps.isDragging()) deps.applySize(targetSize);
658
+ if (current && !deps.isDragging()) {
659
+ deps.applySize(targetSize);
660
+ if (wasAnimating) deps.resyncAfterCancel?.();
661
+ }
662
662
  };
663
663
  let resizeObserver = null;
664
664
  if (typeof ResizeObserver !== "undefined") {
@@ -771,36 +771,17 @@ var init_opacity_dedup = __esm({
771
771
  constructor() {
772
772
  __publicField(this, "last", -1);
773
773
  }
774
- /**
775
- * Returns `true` (and updates the cached value) if `next` differs from the
776
- * cached `last` by more than `epsilon`. Returns `false` (and leaves the
777
- * cache untouched) when the delta is within epsilon — caller skips the
778
- * CSSOM write.
779
- */
780
774
  shouldWrite(next, epsilon) {
781
775
  if (Math.abs(next - this.last) <= epsilon) return false;
782
776
  this.last = next;
783
777
  return true;
784
778
  }
785
- /**
786
- * Force the next `shouldWrite` to pass. Resets the cache to `-1` so any
787
- * non-negative `next` exceeds the epsilon comparison. Called from setters
788
- * that mutate state under the cache (range/mode/enabled flips that change
789
- * the progress→opacity mapping but don't move `progress` itself).
790
- */
791
779
  invalidate() {
792
780
  this.last = -1;
793
781
  }
794
- /**
795
- * Record `value` as the last-written without performing a write. Used
796
- * when a setter writes a literal value through a different path (e.g.
797
- * `setScrimMode("off")` writes `opacity: "0"` inline) and wants future
798
- * `shouldWrite` calls to short-circuit against that known cached state.
799
- */
800
782
  setLastWritten(value) {
801
783
  this.last = value;
802
784
  }
803
- /** Read-only accessor for diagnostic / dedup-aware peek. */
804
785
  get value() {
805
786
  return this.last;
806
787
  }
@@ -808,45 +789,131 @@ var init_opacity_dedup = __esm({
808
789
  }
809
790
  });
810
791
 
792
+ // src/core/primitives/devWarn.ts
793
+ function devWarn(...args) {
794
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && typeof console !== "undefined") {
795
+ console.warn(...args);
796
+ }
797
+ }
798
+ var init_devWarn = __esm({
799
+ "src/core/primitives/devWarn.ts"() {
800
+ }
801
+ });
802
+
803
+ // src/core/primitives/overlay-position.ts
804
+ function resolveSheetAnchoredStyle(mode, position, inset) {
805
+ const sizeOffset = `calc(var(--bs-size, 0px) + ${inset})`;
806
+ switch (mode) {
807
+ case "bottom":
808
+ if (position === "sheet-top-left") return { bottom: sizeOffset, left: inset };
809
+ if (position === "sheet-top-right") return { bottom: sizeOffset, right: inset };
810
+ return { bottom: sizeOffset, left: "50%", transform: "translateX(-50%)" };
811
+ case "top":
812
+ if (position === "sheet-top-left") return { top: sizeOffset, left: inset };
813
+ if (position === "sheet-top-right") return { top: sizeOffset, right: inset };
814
+ return { top: sizeOffset, left: "50%", transform: "translateX(-50%)" };
815
+ case "left":
816
+ if (position === "sheet-top-left") return { left: sizeOffset, top: inset };
817
+ if (position === "sheet-top-right") return { left: sizeOffset, bottom: inset };
818
+ return { left: sizeOffset, top: "50%", transform: "translateY(-50%)" };
819
+ case "right":
820
+ if (position === "sheet-top-left") return { right: sizeOffset, top: inset };
821
+ if (position === "sheet-top-right") return { right: sizeOffset, bottom: inset };
822
+ return { right: sizeOffset, top: "50%", transform: "translateY(-50%)" };
823
+ }
824
+ }
825
+ function applyOverlayPosition(ws, mode, position, inset) {
826
+ switch (position) {
827
+ case "top-left":
828
+ ws.top = inset;
829
+ ws.left = inset;
830
+ break;
831
+ case "top-center":
832
+ ws.top = inset;
833
+ ws.left = "50%";
834
+ ws.transform = "translateX(-50%)";
835
+ break;
836
+ case "top-right":
837
+ ws.top = inset;
838
+ ws.right = inset;
839
+ break;
840
+ case "center-left":
841
+ ws.top = "50%";
842
+ ws.left = inset;
843
+ ws.transform = "translateY(-50%)";
844
+ break;
845
+ case "center":
846
+ ws.top = "50%";
847
+ ws.left = "50%";
848
+ ws.transform = "translate(-50%, -50%)";
849
+ break;
850
+ case "center-right":
851
+ ws.top = "50%";
852
+ ws.right = inset;
853
+ ws.transform = "translateY(-50%)";
854
+ break;
855
+ case "bottom-left":
856
+ ws.bottom = inset;
857
+ ws.left = inset;
858
+ break;
859
+ case "bottom-center":
860
+ ws.bottom = inset;
861
+ ws.left = "50%";
862
+ ws.transform = "translateX(-50%)";
863
+ break;
864
+ case "bottom-right":
865
+ ws.bottom = inset;
866
+ ws.right = inset;
867
+ break;
868
+ case "sheet-top-left":
869
+ case "sheet-top-center":
870
+ case "sheet-top-right": {
871
+ const style = resolveSheetAnchoredStyle(mode, position, inset);
872
+ if (style.top !== void 0) ws.top = style.top;
873
+ if (style.bottom !== void 0) ws.bottom = style.bottom;
874
+ if (style.left !== void 0) ws.left = style.left;
875
+ if (style.right !== void 0) ws.right = style.right;
876
+ if (style.transform !== void 0) ws.transform = style.transform;
877
+ break;
878
+ }
879
+ }
880
+ }
881
+ var init_overlay_position = __esm({
882
+ "src/core/primitives/overlay-position.ts"() {
883
+ }
884
+ });
885
+
811
886
  // src/core/controllers/scrim-controller.ts
812
- var ScrimController;
887
+ var VALID_CSS_LENGTH, ScrimController;
813
888
  var init_scrim_controller = __esm({
814
889
  "src/core/controllers/scrim-controller.ts"() {
815
890
  init_types();
816
891
  init_snap_points();
817
892
  init_hot_path_thresholds();
818
893
  init_opacity_dedup();
894
+ init_devWarn();
895
+ init_overlay_position();
896
+ VALID_CSS_LENGTH = /^-?\d+(?:\.\d+)?(?:px|em|rem|%)$/;
819
897
  ScrimController = class {
820
898
  constructor(deps, opts) {
821
- // ── DOM refs (mirror engine's; cached for hot path) ─────────────────
822
899
  __publicField(this, "screenComponent");
823
900
  __publicField(this, "backdrop");
824
901
  __publicField(this, "mode");
825
- // ── Engine callbacks ────────────────────────────────────────────────
826
902
  __publicField(this, "isDestroyed");
827
903
  __publicField(this, "isTopSheet");
828
904
  __publicField(this, "getAllowedIds");
829
905
  __publicField(this, "getResolvedSnaps");
830
906
  __publicField(this, "snapToFn");
831
- // ── Mode + state ────────────────────────────────────────────────────
907
+ __publicField(this, "closeFn");
908
+ __publicField(this, "scrimInteractive", false);
832
909
  __publicField(this, "scrimMode");
833
910
  __publicField(this, "backdropRange");
834
911
  __publicField(this, "screenRange");
835
- // setScrimEnabled toggle — gated through `scrimEnabled` rather than nuking
836
- // ranges so re-enable can restore prior values without a remount. Public
837
- // so tests / introspection can read it without an `as any` cast.
838
912
  __publicField(this, "scrimEnabled", true);
839
913
  __publicField(this, "savedScrimRanges", null);
840
- // ── Tap-to-close ────────────────────────────────────────────────────
841
914
  __publicField(this, "scrimTapToCloseEnabled", false);
842
915
  __publicField(this, "detachScrimTap", null);
843
- // ── Scrim overlay slot (positioned floating element) ────────────────
844
916
  __publicField(this, "scrimOverlayTeardown", null);
845
- // ── Hot-path opacity dedup caches (per-frame writes) ────────────────
846
- // Numeric sentinels migrated to WriteSentinel primitive — single
847
- // invalidation surface, shared with the engine's size/progress sentinels.
848
- // The two string-typed sentinels (`Pointer`, `Display`) keep their inline
849
- // pattern: equality on string union, no epsilon, different reset semantics.
850
917
  __publicField(this, "backdropOpacitySentinel", new WriteSentinel());
851
918
  __publicField(this, "screenOpacitySentinel", new WriteSentinel());
852
919
  __publicField(this, "lastBackdropPointer", "");
@@ -859,6 +926,7 @@ var init_scrim_controller = __esm({
859
926
  this.getAllowedIds = deps.getAllowedIds;
860
927
  this.getResolvedSnaps = deps.getResolvedSnaps;
861
928
  this.snapToFn = deps.snapTo;
929
+ this.closeFn = deps.close;
862
930
  this.scrimMode = opts.scrimMode ?? "full";
863
931
  const preset = opts.scrimPreset ? SCRIM_PRESETS[opts.scrimPreset] : null;
864
932
  this.backdropRange = opts.backdropRange ?? preset?.range ?? [0, 1];
@@ -881,41 +949,15 @@ var init_scrim_controller = __esm({
881
949
  }
882
950
  }
883
951
  }
884
- /**
885
- * Invalidate the opacity dedup caches. Called from any setter that mutates
886
- * range/mode/enabled — opacity is a pure function of (progress, range,
887
- * enabled), so a config change makes the cached values stale even if
888
- * `progress` itself didn't move. Next `applyOpacity(progress, false)` call
889
- * sees the -1 sentinel and re-runs the math instead of short-circuiting.
890
- *
891
- * Public for the engine: `setSnapPoints` mutates resolved geometry which
892
- * shifts `progress→opacity` mapping under the cache; engine calls this to
893
- * keep the next render frame correct on a static sheet.
894
- * @internal
895
- */
896
952
  invalidateOpacityCache() {
897
953
  this.backdropOpacitySentinel.invalidate();
898
954
  this.screenOpacitySentinel.invalidate();
899
955
  }
900
- /** Engine calls this when its `attach()` runs — installs scrim-tap-click if requested. */
901
956
  attach() {
902
957
  if (this.scrimTapToCloseEnabled) {
903
958
  this.setScrimTapToClose(true);
904
959
  }
905
960
  }
906
- // ── Hot-path: opacity computation called per-frame from engine.applySize ──
907
- /**
908
- * Per-frame opacity writer. Computes backdrop + screen opacity from the
909
- * sheet's progress (0..1 within allowed range), gated by sub-pixel dedup
910
- * caches so steady-state frames don't trigger style recalc.
911
- *
912
- * `progressChanged` is the engine's already-computed dedup gate — when
913
- * `false`, opacity is by construction a pure function of `progress` so
914
- * cached values are still valid; we short-circuit the Math.min/max +
915
- * division work entirely. Setters that mutate ranges/mode/enabled
916
- * invalidate the cache by writing -1 sentinels so the next call re-runs
917
- * the math.
918
- */
919
961
  applyOpacity(progress, progressChanged) {
920
962
  if (!progressChanged && this.backdropOpacitySentinel.value !== -1 && this.screenOpacitySentinel.value !== -1) {
921
963
  return;
@@ -961,80 +1003,30 @@ var init_scrim_controller = __esm({
961
1003
  }
962
1004
  }
963
1005
  }
964
- // ── Public setters (engine forwards these 1:1) ─────────────────────
965
- /**
966
- * Replace the [start, end] progress window in which the backdrop fades 0→1.
967
- * @param range - Tuple of two progress values in 0..1 (start <= end).
968
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
969
- * @example
970
- * // Backdrop only starts darkening after the sheet crosses 30% open.
971
- * engine.setBackdropRange([0.3, 1]);
972
- */
973
1006
  setBackdropRange(range, applySize) {
974
1007
  if (this.isDestroyed()) return;
975
1008
  this.backdropRange = range;
976
1009
  this.invalidateOpacityCache();
977
1010
  applySize();
978
1011
  }
979
- /**
980
- * Replace the [start, end] progress window in which the screen scrim fades 0→1.
981
- * @param range - Tuple of two progress values in 0..1 (start <= end).
982
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
983
- * @example
984
- * // Scrim is fully opaque only when the sheet is past 50% — softer feel.
985
- * engine.setScreenRange([0, 0.5]);
986
- */
987
1012
  setScreenRange(range, applySize) {
988
1013
  if (this.isDestroyed()) return;
989
1014
  this.screenRange = range;
990
1015
  this.invalidateOpacityCache();
991
1016
  applySize();
992
1017
  }
993
- /**
994
- * Set the scrim background color. Pass `null`/`undefined` to clear the
995
- * inline write and let consumer CSS take over.
996
- * @param color - Any valid CSS color string, or `null`/`undefined` to clear.
997
- * @example
998
- * engine.setScrimColor("rgba(0, 0, 0, 0.6)");
999
- * engine.setScrimColor(null); // hand back to consumer CSS
1000
- */
1001
1018
  setScrimColor(color) {
1002
1019
  if (this.isDestroyed()) return;
1003
1020
  this.applyScrimStyles({ color: color === void 0 ? null : color });
1004
1021
  }
1005
- /**
1006
- * Set the scrim backdrop-filter blur radius. Pass `null`/`undefined` to clear.
1007
- * @param blur - CSS length (e.g. `"8px"`) consumed as `blur(<value>)`, or
1008
- * `null`/`undefined` to remove the filter.
1009
- * @example
1010
- * engine.setScrimBlur("12px"); // frosted-glass effect
1011
- * engine.setScrimBlur(null); // disable blur
1012
- */
1013
1022
  setScrimBlur(blur) {
1014
1023
  if (this.isDestroyed()) return;
1015
1024
  this.applyScrimStyles({ blur: blur === void 0 ? null : blur });
1016
1025
  }
1017
- /**
1018
- * Toggle whether the scrim layer captures pointer events.
1019
- * @param interactive - `true` → `pointer-events: auto`; `false` → `none`.
1020
- * @example
1021
- * engine.setScrimInteractive(true); // scrim swallows clicks
1022
- * engine.setScrimInteractive(false); // clicks pass through to content below
1023
- */
1024
1026
  setScrimInteractive(interactive) {
1025
1027
  if (this.isDestroyed()) return;
1026
1028
  this.applyScrimStyles({ interactive });
1027
1029
  }
1028
- /**
1029
- * Switch scrim positioning at runtime.
1030
- * @param mode - `"full"` (consumer CSS), `"above-sheet"` (engine-pinned inset
1031
- * so only the area NOT covered by the sheet is dimmed), or
1032
- * `"off"` (display:none + opacity:0 — no remount needed).
1033
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
1034
- * @example
1035
- * engine.setScrimMode("above-sheet"); // dim only the area above the sheet
1036
- * engine.setScrimMode("off"); // disable scrim layer
1037
- */
1038
1030
  setScrimMode(mode, applySize) {
1039
1031
  if (this.isDestroyed()) return;
1040
1032
  if (mode === this.scrimMode) return;
@@ -1064,15 +1056,6 @@ var init_scrim_controller = __esm({
1064
1056
  }
1065
1057
  applySize();
1066
1058
  }
1067
- /**
1068
- * Enable or disable click-on-scrim → snap to first non-zero allowed snap.
1069
- * Auto-promotes `pointer-events: auto` when scrim is non-interactive so the
1070
- * click listener actually fires.
1071
- * @param enabled - `true` installs the click listener; `false` removes it.
1072
- * @example
1073
- * engine.setScrimTapToClose(true); // tap outside sheet collapses it
1074
- * engine.setScrimTapToClose(false); // require explicit close button
1075
- */
1076
1059
  setScrimTapToClose(enabled) {
1077
1060
  if (this.isDestroyed()) return;
1078
1061
  if (!this.screenComponent) return;
@@ -1085,11 +1068,15 @@ var init_scrim_controller = __esm({
1085
1068
  }
1086
1069
  const onClick = () => {
1087
1070
  if (this.isDestroyed()) return;
1071
+ if (this.closeFn) {
1072
+ this.closeFn();
1073
+ return;
1074
+ }
1088
1075
  const allowed = this.getAllowedIds();
1089
1076
  const snaps = this.getResolvedSnaps();
1090
1077
  const fallback = allowed.find((id) => {
1091
1078
  const snap = findById(id, snaps);
1092
- return snap !== null && snap.size > 0;
1079
+ return snap !== null && snap.size === 0;
1093
1080
  });
1094
1081
  if (fallback) this.snapToFn(fallback);
1095
1082
  };
@@ -1102,20 +1089,11 @@ var init_scrim_controller = __esm({
1102
1089
  this.scrimTapToCloseEnabled = false;
1103
1090
  }
1104
1091
  }
1105
- /**
1106
- * Toggle the scrim layer without remounting. Disabling stashes current
1107
- * `screenRange`/`backdropRange` and degenerates them to `[1, 1]` (opacity
1108
- * formula → 0 across the whole range); re-enabling restores the saved values.
1109
- * @param enabled - `false` hides scrim, `true` restores prior ranges.
1110
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
1111
- * @example
1112
- * engine.setScrimEnabled(false); // hide scrim, ranges remembered
1113
- * engine.setScrimEnabled(true); // restore prior ranges (no remount)
1114
- */
1115
1092
  setScrimEnabled(enabled, applySize) {
1116
1093
  if (this.isDestroyed()) return;
1117
1094
  if (enabled === this.scrimEnabled) return;
1118
1095
  this.scrimEnabled = enabled;
1096
+ this.invalidateOpacityCache();
1119
1097
  if (!enabled) {
1120
1098
  this.savedScrimRanges = {
1121
1099
  screen: this.screenRange,
@@ -1130,20 +1108,6 @@ var init_scrim_controller = __esm({
1130
1108
  }
1131
1109
  applySize();
1132
1110
  }
1133
- /**
1134
- * Batch-update multiple scrim fields with a single applySize() at the end.
1135
- * If `preset` is set, it seeds color/blur/interactive/range first; explicit
1136
- * fields override.
1137
- * @param opts - Partial scrim config: preset, mode, enabled, tapToClose,
1138
- * color, blur, interactive, range.
1139
- * @param applySize - Engine-supplied recompute hook; runs ONCE per call.
1140
- * @example
1141
- * engine.setScrim({
1142
- * preset: "frosted",
1143
- * mode: "above-sheet",
1144
- * tapToClose: true,
1145
- * });
1146
- */
1147
1111
  setScrim(opts, applySize) {
1148
1112
  if (this.isDestroyed()) return;
1149
1113
  if (opts.preset) {
@@ -1153,7 +1117,12 @@ var init_scrim_controller = __esm({
1153
1117
  blur: cfg.blur ?? null,
1154
1118
  interactive: cfg.interactive
1155
1119
  });
1156
- this.screenRange = cfg.range;
1120
+ if (!this.scrimEnabled && this.savedScrimRanges) {
1121
+ this.savedScrimRanges = { screen: cfg.range, backdrop: cfg.range };
1122
+ } else {
1123
+ this.screenRange = cfg.range;
1124
+ this.backdropRange = cfg.range;
1125
+ }
1157
1126
  }
1158
1127
  const noop = () => {
1159
1128
  };
@@ -1166,27 +1135,18 @@ var init_scrim_controller = __esm({
1166
1135
  interactive: opts.interactive
1167
1136
  });
1168
1137
  if (opts.range !== void 0) {
1169
- this.screenRange = opts.range;
1138
+ if (!this.scrimEnabled && this.savedScrimRanges) {
1139
+ this.savedScrimRanges = {
1140
+ screen: opts.range,
1141
+ backdrop: this.savedScrimRanges.backdrop
1142
+ };
1143
+ } else {
1144
+ this.screenRange = opts.range;
1145
+ }
1170
1146
  }
1171
1147
  this.invalidateOpacityCache();
1172
1148
  applySize();
1173
1149
  }
1174
- /**
1175
- * Inject a positioned floating element into the scrim layer (e.g. a close
1176
- * button anchored top-right). Replaces any prior overlay so repeated calls
1177
- * (reactive effects re-running) don't accumulate wrappers.
1178
- * @param opts - Children element, optional `inset` (default `"16px"`),
1179
- * `position` (9-way grid, default `"top-right"`), and
1180
- * `interactive` flag (default `true`).
1181
- * @returns Teardown function that removes the wrapper from the DOM.
1182
- * @example
1183
- * const teardown = engine.setScrimOverlay({
1184
- * children: closeBtn,
1185
- * position: "top-right",
1186
- * inset: "20px",
1187
- * });
1188
- * // later: teardown(); // remove the floating button
1189
- */
1190
1150
  setScrimOverlay(opts) {
1191
1151
  if (this.isDestroyed()) return () => {
1192
1152
  };
@@ -1194,61 +1154,38 @@ var init_scrim_controller = __esm({
1194
1154
  };
1195
1155
  this.scrimOverlayTeardown?.();
1196
1156
  const screen = this.screenComponent;
1197
- const inset = opts.inset ?? "16px";
1157
+ const insetRaw = opts.inset ?? "16px";
1158
+ const inset = VALID_CSS_LENGTH.test(insetRaw) ? insetRaw : "16px";
1159
+ if (inset !== insetRaw) {
1160
+ devWarn(
1161
+ `[bottom-sheet] setScrimOverlay: invalid inset ${JSON.stringify(insetRaw)}; expected a CSS length like "16px" / "1rem" / "5%". Falling back to "16px".`
1162
+ );
1163
+ }
1164
+ if (opts.children.ownerDocument && opts.children.ownerDocument !== screen.ownerDocument) {
1165
+ devWarn(
1166
+ "[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."
1167
+ );
1168
+ }
1198
1169
  const position = opts.position ?? "top-right";
1199
1170
  const interactive = opts.interactive ?? true;
1200
1171
  const wrapper = screen.ownerDocument.createElement("div");
1201
1172
  wrapper.className = "bs-scrim-overlay";
1202
1173
  const ws = wrapper.style;
1203
1174
  ws.position = "absolute";
1204
- switch (position) {
1205
- case "top-left":
1206
- ws.top = inset;
1207
- ws.left = inset;
1208
- break;
1209
- case "top-center":
1210
- ws.top = inset;
1211
- ws.left = "50%";
1212
- ws.transform = "translateX(-50%)";
1213
- break;
1214
- case "top-right":
1215
- ws.top = inset;
1216
- ws.right = inset;
1217
- break;
1218
- case "center-left":
1219
- ws.top = "50%";
1220
- ws.left = inset;
1221
- ws.transform = "translateY(-50%)";
1222
- break;
1223
- case "center":
1224
- ws.top = "50%";
1225
- ws.left = "50%";
1226
- ws.transform = "translate(-50%, -50%)";
1227
- break;
1228
- case "center-right":
1229
- ws.top = "50%";
1230
- ws.right = inset;
1231
- ws.transform = "translateY(-50%)";
1232
- break;
1233
- case "bottom-left":
1234
- ws.bottom = inset;
1235
- ws.left = inset;
1236
- break;
1237
- case "bottom-center":
1238
- ws.bottom = inset;
1239
- ws.left = "50%";
1240
- ws.transform = "translateX(-50%)";
1241
- break;
1242
- case "bottom-right":
1243
- ws.bottom = inset;
1244
- ws.right = inset;
1245
- break;
1246
- }
1175
+ applyOverlayPosition(ws, this.mode, position, inset);
1247
1176
  if (interactive) {
1248
1177
  ws.pointerEvents = "auto";
1249
1178
  }
1250
1179
  wrapper.appendChild(opts.children);
1251
- screen.appendChild(wrapper);
1180
+ const host = screen.parentElement;
1181
+ if (!host) {
1182
+ devWarn(
1183
+ "[bottom-sheet] setScrimOverlay: scrim element has no parent, skipping overlay injection. Mount the sheet root before calling setScrimOverlay."
1184
+ );
1185
+ return () => {
1186
+ };
1187
+ }
1188
+ host.appendChild(wrapper);
1252
1189
  const teardown = () => {
1253
1190
  if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
1254
1191
  if (this.scrimOverlayTeardown === teardown) {
@@ -1258,7 +1195,6 @@ var init_scrim_controller = __esm({
1258
1195
  this.scrimOverlayTeardown = teardown;
1259
1196
  return teardown;
1260
1197
  }
1261
- /** Tear down toggle-state listeners + remove any injected overlay wrapper. */
1262
1198
  destroy() {
1263
1199
  this.detachScrimTap?.();
1264
1200
  this.detachScrimTap = null;
@@ -1277,8 +1213,6 @@ var init_scrim_controller = __esm({
1277
1213
  this.lastBackdropPointer = "";
1278
1214
  this.lastScreenDisplay = "";
1279
1215
  }
1280
- // ── Private helpers ────────────────────────────────────────────────
1281
- /** Centralized scrim style writer — atomic 3-property write across all setters. */
1282
1216
  applyScrimStyles(opts) {
1283
1217
  if (!this.screenComponent) return;
1284
1218
  const s = this.screenComponent.style;
@@ -1297,10 +1231,10 @@ var init_scrim_controller = __esm({
1297
1231
  }
1298
1232
  }
1299
1233
  if (opts.interactive !== void 0) {
1234
+ this.scrimInteractive = opts.interactive;
1300
1235
  s.pointerEvents = opts.interactive ? "auto" : "none";
1301
1236
  }
1302
1237
  }
1303
- /** Pin the screen overlay to the area NOT occupied by the sheet. */
1304
1238
  applyAboveSheetInset() {
1305
1239
  if (!this.screenComponent) return;
1306
1240
  const insetByMode = {
@@ -1312,14 +1246,14 @@ var init_scrim_controller = __esm({
1312
1246
  const s = this.screenComponent.style;
1313
1247
  s.position = "fixed";
1314
1248
  s.inset = insetByMode[this.mode];
1315
- s.pointerEvents = "none";
1249
+ s.pointerEvents = this.scrimInteractive || this.scrimTapToCloseEnabled ? "auto" : "none";
1316
1250
  }
1317
1251
  };
1318
1252
  }
1319
1253
  });
1320
1254
 
1321
1255
  // src/core/animation/animation.ts
1322
- var easeOutBack, easeOutCubic, easeLinear, easeOutQuint, tween;
1256
+ var easeOutBack, easeOutCubic, easeLinear, easeOutQuint, tween, prefersReducedMotion;
1323
1257
  var init_animation = __esm({
1324
1258
  "src/core/animation/animation.ts"() {
1325
1259
  easeOutBack = (t) => {
@@ -1371,6 +1305,10 @@ var init_animation = __esm({
1371
1305
  promise
1372
1306
  };
1373
1307
  };
1308
+ prefersReducedMotion = () => {
1309
+ if (typeof window === "undefined" || !window.matchMedia) return false;
1310
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1311
+ };
1374
1312
  }
1375
1313
  });
1376
1314
 
@@ -1406,14 +1344,20 @@ var init_spring = __esm({
1406
1344
  if (cancelled) return;
1407
1345
  const dt = Math.min((now - lastTime) / 1e3, MAX_DT);
1408
1346
  lastTime = now;
1409
- const subDt = Math.min(dt, 1 / 240);
1410
- let stepsLeft = Math.max(1, Math.ceil(dt / subDt));
1347
+ const steps = Math.max(1, Math.ceil(dt * 240));
1348
+ const subDt = dt / steps;
1349
+ let stepsLeft = steps;
1411
1350
  while (stepsLeft-- > 0) {
1412
1351
  const force = -cfg.stiffness * (x - to) - cfg.damping * v;
1413
1352
  const accel = force / cfg.mass;
1414
1353
  v += accel * subDt;
1415
1354
  x += v * subDt;
1416
1355
  }
1356
+ if (!Number.isFinite(x) || !Number.isFinite(v)) {
1357
+ onUpdate(to, 0);
1358
+ resolve();
1359
+ return;
1360
+ }
1417
1361
  onUpdate(x, v);
1418
1362
  const isAtRest = Math.abs(to - x) < (cfg.restDelta ?? 0.5) && Math.abs(v) < (cfg.restSpeed ?? 0.5);
1419
1363
  if (isAtRest) {
@@ -1491,13 +1435,9 @@ var init_animation_runner = __esm({
1491
1435
  __publicField(this, "duration");
1492
1436
  __publicField(this, "easing");
1493
1437
  __publicField(this, "respectReducedMotion");
1494
- /** Live-tracked via mq listener so animateTo skips matchMedia per snap. */
1495
1438
  __publicField(this, "reducedMotion", false);
1496
- /** Disposes the matchMedia change listener — drained from destroy(). */
1497
1439
  __publicField(this, "detachReducedMotion", null);
1498
- /** Computed once — both inputs (option + API presence) stable for life. */
1499
1440
  __publicField(this, "viewTransitionsAvailable");
1500
- /** In-flight handles. Public for tests / debugging only — engine drives via cancel(). */
1501
1441
  __publicField(this, "currentTween", null);
1502
1442
  __publicField(this, "currentSpring", null);
1503
1443
  this.element = deps.element;
@@ -1528,26 +1468,13 @@ var init_animation_runner = __esm({
1528
1468
  }
1529
1469
  }
1530
1470
  }
1531
- /**
1532
- * Live derived flag — `true` whenever a spring/tween handle is in flight.
1533
- * Reading the canonical handles (instead of mirroring into a separate
1534
- * boolean) eliminates the reduced-motion early-return latent bug where
1535
- * the flag stayed true forever, and removes the cross-controller
1536
- * setIsAnimating callback.
1537
- */
1538
1471
  get isAnimating() {
1539
1472
  return this.currentTween !== null || this.currentSpring !== null;
1540
1473
  }
1541
- /** Cancel any in-flight tween/spring without touching engine state. */
1542
1474
  cancel() {
1543
1475
  this.currentTween?.cancel();
1544
1476
  this.currentSpring?.cancel();
1545
1477
  }
1546
- /**
1547
- * Animate from current size to `target`. Resolves when the animation
1548
- * completes; resolves early if cancelled mid-flight (caller checks an
1549
- * abort signal post-await for cycle-invariant correctness).
1550
- */
1551
1478
  async animateTo(target, velocityPxPerMs) {
1552
1479
  if (target === this.getSize()) return;
1553
1480
  this.currentTween?.cancel();
@@ -1588,7 +1515,6 @@ var init_animation_runner = __esm({
1588
1515
  if (!this.isDragging()) this.element.style.willChange = "auto";
1589
1516
  }
1590
1517
  }
1591
- /** Tear down the matchMedia listener. Engine calls from destroy(). */
1592
1518
  destroy() {
1593
1519
  this.detachReducedMotion?.();
1594
1520
  this.detachReducedMotion = null;
@@ -1600,7 +1526,7 @@ var init_animation_runner = __esm({
1600
1526
  });
1601
1527
 
1602
1528
  // src/core/lifecycle/focusTrap.ts
1603
- var FOCUSABLE, isVisible, focusables, installFocusTrap;
1529
+ var FOCUSABLE, isVisible, focusables, trapStack, installFocusTrap;
1604
1530
  var init_focusTrap = __esm({
1605
1531
  "src/core/lifecycle/focusTrap.ts"() {
1606
1532
  FOCUSABLE = [
@@ -1625,9 +1551,13 @@ var init_focusTrap = __esm({
1625
1551
  focusables = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter(
1626
1552
  (el) => !el.hasAttribute("inert") && isVisible(el) && !el.matches("[aria-hidden='true']")
1627
1553
  );
1554
+ trapStack = [];
1628
1555
  installFocusTrap = (container, options = {}) => {
1629
1556
  if (typeof document === "undefined") return () => {
1630
1557
  };
1558
+ const token = {};
1559
+ trapStack.push(token);
1560
+ const isActive = () => trapStack[trapStack.length - 1] === token;
1631
1561
  const previouslyFocused = document.activeElement;
1632
1562
  const focusInitial = () => {
1633
1563
  const initial = typeof options.initialFocus === "string" ? container.querySelector(options.initialFocus) : options.initialFocus ?? focusables(container)[0] ?? container;
@@ -1635,6 +1565,7 @@ var init_focusTrap = __esm({
1635
1565
  };
1636
1566
  focusInitial();
1637
1567
  const handleKey = (e) => {
1568
+ if (!isActive()) return;
1638
1569
  if (e.key === "Escape" && options.onEscape) {
1639
1570
  options.onEscape();
1640
1571
  return;
@@ -1657,6 +1588,7 @@ var init_focusTrap = __esm({
1657
1588
  }
1658
1589
  };
1659
1590
  const handleFocusIn = (e) => {
1591
+ if (!isActive()) return;
1660
1592
  const target = e.target;
1661
1593
  if (target && !container.contains(target)) {
1662
1594
  const list = focusables(container);
@@ -1669,6 +1601,8 @@ var init_focusTrap = __esm({
1669
1601
  return () => {
1670
1602
  if (released) return;
1671
1603
  released = true;
1604
+ const idx = trapStack.indexOf(token);
1605
+ if (idx !== -1) trapStack.splice(idx, 1);
1672
1606
  document.removeEventListener("keydown", handleKey, true);
1673
1607
  document.removeEventListener("focusin", handleFocusIn, true);
1674
1608
  if (previouslyFocused && document.contains(previouslyFocused)) {
@@ -1731,11 +1665,17 @@ function createInertSiblings(rootProvider) {
1731
1665
  return {
1732
1666
  apply() {
1733
1667
  if (typeof document === "undefined") return;
1734
- const root = rootProvider();
1735
- const parent = root.parentElement ?? document.body;
1668
+ let top = rootProvider();
1669
+ while (top.parentElement && top.parentElement !== document.body) {
1670
+ top = top.parentElement;
1671
+ }
1672
+ const parent = top.parentElement ?? document.body;
1736
1673
  for (const child of Array.from(parent.children)) {
1737
- if (child === root) continue;
1674
+ if (child === top) continue;
1738
1675
  if (child.hasAttribute("inert")) continue;
1676
+ if (child.classList.contains("bs-backdrop") || child.classList.contains("bs-screen") || child.classList.contains("bs-root")) {
1677
+ continue;
1678
+ }
1739
1679
  child.setAttribute("inert", "");
1740
1680
  tracked.push(child);
1741
1681
  }
@@ -1761,7 +1701,6 @@ var init_lifecycle_controller = __esm({
1761
1701
  LifecycleController = class {
1762
1702
  constructor(deps, opts) {
1763
1703
  __publicField(this, "element");
1764
- __publicField(this, "closeFn");
1765
1704
  __publicField(this, "focusTrapEnabled");
1766
1705
  __publicField(this, "initialFocus");
1767
1706
  __publicField(this, "closeOnEscape");
@@ -1770,16 +1709,11 @@ var init_lifecycle_controller = __esm({
1770
1709
  __publicField(this, "inertSiblings");
1771
1710
  __publicField(this, "shouldApplyInertSiblings");
1772
1711
  __publicField(this, "returnFocus");
1773
- /** Toggle-state teardowns — fire on close, NOT in destroy-only stack. */
1774
1712
  __publicField(this, "releaseFocusTrap", null);
1775
1713
  __publicField(this, "releaseScrollLock", null);
1776
- /** True between install() and release(). Guards release()/destroy() from
1777
- * re-running side effects (notably `returnFocus.focus()`) on already-
1778
- * released or never-installed instances. */
1779
1714
  __publicField(this, "installed", false);
1780
1715
  __publicField(this, "destroyed", false);
1781
1716
  this.element = deps.element;
1782
- this.closeFn = deps.close;
1783
1717
  this.focusTrapEnabled = opts.focusTrap ?? false;
1784
1718
  this.initialFocus = opts.initialFocus;
1785
1719
  this.closeOnEscape = opts.closeOnEscape ?? true;
@@ -1789,14 +1723,9 @@ var init_lifecycle_controller = __esm({
1789
1723
  this.returnFocus = opts.returnFocus;
1790
1724
  this.inertSiblings = createInertSiblings(() => this.element);
1791
1725
  }
1792
- /**
1793
- * Run the open-time install steps. Idempotent: each step checks its
1794
- * existing teardown ref so a re-open during a half-cancelled close
1795
- * doesn't double-install. Sets `installed=true` BEFORE the first side
1796
- * effect so a partial throw (detached / shadow-DOM target) leaves the
1797
- * caller's `release()` rollback path armed to undo whatever did succeed.
1798
- * No-op when destroyed.
1799
- */
1726
+ get isInstalled() {
1727
+ return this.installed;
1728
+ }
1800
1729
  install() {
1801
1730
  if (this.destroyed) return;
1802
1731
  this.installed = true;
@@ -1805,30 +1734,17 @@ var init_lifecycle_controller = __esm({
1805
1734
  }
1806
1735
  if (this.focusTrapEnabled && !this.releaseFocusTrap) {
1807
1736
  this.releaseFocusTrap = installFocusTrap(this.element, {
1808
- initialFocus: this.initialFocus,
1809
- onEscape: this.closeOnEscape ? () => void this.closeFn() : void 0
1737
+ initialFocus: this.initialFocus
1810
1738
  });
1811
1739
  }
1812
1740
  if (this.inertSiblingsEnabled && this.shouldApplyInertSiblings()) {
1813
1741
  this.inertSiblings.apply();
1814
1742
  }
1815
1743
  }
1816
- /**
1817
- * Update the post-release focus target. Overlay re-binds this when the
1818
- * consumer changes `returnFocus` after construction; the new value applies
1819
- * on the next `release()`. No-op when destroyed.
1820
- */
1821
1744
  setReturnFocus(target) {
1822
1745
  if (this.destroyed) return;
1823
1746
  this.returnFocus = target;
1824
1747
  }
1825
- /**
1826
- * Release all install-time teardowns. Short-circuits when not installed,
1827
- * so `close() → destroy()` (each routing through here) only restores
1828
- * focus once. The `installed` guard lets `inertSiblings.remove()` and
1829
- * the focus-trap/scroll-lock teardown nullings stay simple — they were
1830
- * already idempotent, but `returnFocus.focus()` was firing on every call.
1831
- */
1832
1748
  release() {
1833
1749
  if (!this.installed) return;
1834
1750
  this.installed = false;
@@ -1842,12 +1758,6 @@ var init_lifecycle_controller = __esm({
1842
1758
  target?.focus?.();
1843
1759
  }
1844
1760
  }
1845
- /**
1846
- * Final teardown — releases any still-installed listeners and seals the
1847
- * controller. After destroy(), install() is a no-op and setReturnFocus()
1848
- * is rejected, so post-destroy mutations can't queue up a stale focus
1849
- * restore.
1850
- */
1851
1761
  destroy() {
1852
1762
  this.release();
1853
1763
  this.destroyed = true;
@@ -1862,12 +1772,10 @@ var init_gestures = __esm({
1862
1772
  "src/core/gestures.ts"() {
1863
1773
  isAxisVertical = (mode) => mode === "bottom" || mode === "top";
1864
1774
  SampleRing = class {
1865
- // current count, ≤ capacity
1866
1775
  constructor(capacity) {
1867
1776
  __publicField(this, "slots");
1868
1777
  __publicField(this, "capacity");
1869
1778
  __publicField(this, "head", 0);
1870
- // next write index
1871
1779
  __publicField(this, "size", 0);
1872
1780
  this.capacity = capacity;
1873
1781
  this.slots = new Array(capacity);
@@ -1880,12 +1788,6 @@ var init_gestures = __esm({
1880
1788
  this.head = (this.head + 1) % this.capacity;
1881
1789
  if (this.size < this.capacity) this.size++;
1882
1790
  }
1883
- /**
1884
- * Drop oldest entries whose timestamp is below `cutoff`, but never shrink
1885
- * below `minKeep`. Mirrors the original
1886
- * `while (samples.length > minKeep && samples[0].t < cutoff) samples.shift()`
1887
- * but without copying — just advances the logical tail.
1888
- */
1889
1791
  pruneBefore(cutoff, minKeep) {
1890
1792
  while (this.size > minKeep) {
1891
1793
  const oldestIdx = (this.head - this.size + this.capacity) % this.capacity;
@@ -1995,12 +1897,12 @@ var init_gestures = __esm({
1995
1897
  // src/core/primitives/rubber-band.ts
1996
1898
  function rubberBand(overshoot, maxAxisSize) {
1997
1899
  const cap = Math.min(maxAxisSize * 0.15, 80);
1998
- return Math.sign(overshoot) * Math.pow(Math.abs(overshoot), RUBBER_BAND_FACTOR) * Math.min(6, cap / Math.max(Math.abs(overshoot), 1));
1900
+ if (cap <= 0) return 0;
1901
+ const abs = Math.abs(overshoot);
1902
+ return Math.sign(overshoot) * (cap * abs / (abs + cap));
1999
1903
  }
2000
- var RUBBER_BAND_FACTOR;
2001
1904
  var init_rubber_band = __esm({
2002
1905
  "src/core/primitives/rubber-band.ts"() {
2003
- RUBBER_BAND_FACTOR = 0.55;
2004
1906
  }
2005
1907
  });
2006
1908
 
@@ -2038,28 +1940,14 @@ var init_gesture_controller = __esm({
2038
1940
  __publicField(this, "detach", null);
2039
1941
  __publicField(this, "isDragging_", false);
2040
1942
  __publicField(this, "dragStartSize", 0);
2041
- // Only "touch" gestures are eligible for soft-keyboard dismissal —
2042
- // mouse/trackpad users don't have a keyboard to dismiss.
2043
1943
  __publicField(this, "currentPointerKind", "touch");
2044
- // One blur per drag — flipping focus every move event races the keyboard's
2045
- // own animations on iOS and makes the page jitter.
2046
1944
  __publicField(this, "keyboardDismissed", false);
2047
- // Reused payload object for the "drag" event — mutated each onMove instead
2048
- // of allocating a fresh `{size, delta}` per frame. At 60-120Hz a long drag
2049
- // would otherwise churn tens of KB through the GC. Mirrors browser
2050
- // PointerEvent semantics: consumers must NOT retain the reference across
2051
- // frames (clone if needed). See SheetEventMap.drag JSDoc.
2052
1945
  __publicField(this, "dragPayload", { size: 0, delta: 0 });
2053
1946
  this.deps = deps;
2054
1947
  }
2055
- /** True between `dragstart` and `dragend`/`onCancel`. */
2056
1948
  get isDragging() {
2057
1949
  return this.isDragging_;
2058
1950
  }
2059
- /**
2060
- * Bind pointer listeners on the handle and return a teardown. Engine pushes
2061
- * the teardown into its TeardownStack so destroy() drains it once.
2062
- */
2063
1951
  install() {
2064
1952
  if (this.detach) return this.detach;
2065
1953
  this.detach = installGestures(this.deps.handle, this.deps.mode, {
@@ -2110,17 +1998,16 @@ var init_gesture_controller = __esm({
2110
1998
  this.isDragging_ = false;
2111
1999
  this.keyboardDismissed = false;
2112
2000
  this.deps.getRoot()?.removeAttribute("data-dragging");
2001
+ this.deps.emit("dragend", {
2002
+ size: this.deps.getDragContext().size,
2003
+ velocity: 0
2004
+ });
2113
2005
  const restoreTo = this.dragStartSize;
2114
2006
  void this.deps.animateTo(restoreTo, 0);
2115
2007
  }
2116
2008
  });
2117
2009
  return this.detach;
2118
2010
  }
2119
- /**
2120
- * Force-clear drag visual state. Engine calls from destroy() when destroyed
2121
- * mid-drag — gesture detach won't synthesize an onCancel, so we have to
2122
- * roll back the data-attribute + will-change ourselves.
2123
- */
2124
2011
  forceClearDragState() {
2125
2012
  if (!this.isDragging_) return;
2126
2013
  this.isDragging_ = false;
@@ -2144,11 +2031,6 @@ var init_aria_slider_writer = __esm({
2144
2031
  mode === "bottom" || mode === "top" ? "vertical" : "horizontal"
2145
2032
  );
2146
2033
  }
2147
- /**
2148
- * Update slider position. `allowedIds` length determines `aria-valuemax`;
2149
- * `activeId` indexes into it for `aria-valuenow` + `aria-valuetext`.
2150
- * Empty list → strip every dynamic attr so AT skips the handle entirely.
2151
- */
2152
2034
  setValue(allowedIds, activeId) {
2153
2035
  if (allowedIds.length === 0) {
2154
2036
  this.handle.removeAttribute("aria-valuemin");
@@ -2204,6 +2086,306 @@ var init_scroll_cache = __esm({
2204
2086
  }
2205
2087
  });
2206
2088
 
2089
+ // src/core/primitives/anchor-animations.ts
2090
+ function resolveTransitionFrames(spec, phase) {
2091
+ const fallback = { frames: null, duration: 0, easing: DEFAULT_EASING };
2092
+ if (spec === "none") return fallback;
2093
+ if (spec === void 0 || typeof spec === "string") {
2094
+ const preset2 = PRESET_FRAMES[spec ?? "fade"];
2095
+ return {
2096
+ frames: preset2[phase],
2097
+ duration: DEFAULT_DURATION3,
2098
+ easing: DEFAULT_EASING
2099
+ };
2100
+ }
2101
+ const custom = phase === "enter" ? spec.enter : spec.exit;
2102
+ if (custom) {
2103
+ return {
2104
+ frames: custom,
2105
+ duration: spec.duration ?? DEFAULT_DURATION3,
2106
+ easing: spec.easing ?? DEFAULT_EASING
2107
+ };
2108
+ }
2109
+ if (spec.preset === "none") return fallback;
2110
+ const preset = PRESET_FRAMES[spec.preset ?? "fade"];
2111
+ return {
2112
+ frames: preset[phase],
2113
+ duration: spec.duration ?? DEFAULT_DURATION3,
2114
+ easing: spec.easing ?? DEFAULT_EASING
2115
+ };
2116
+ }
2117
+ function runAnchorTransition(el, spec, phase) {
2118
+ const respectReduced = typeof spec === "object" ? spec.respectReducedMotion !== false : true;
2119
+ if (respectReduced && prefersReducedMotion()) return INSTANT;
2120
+ const { frames, duration, easing } = resolveTransitionFrames(spec, phase);
2121
+ if (!frames || duration <= 0) return INSTANT;
2122
+ if (typeof el.animate !== "function") return INSTANT;
2123
+ const animation = el.animate(frames, {
2124
+ duration,
2125
+ easing,
2126
+ fill: "both"
2127
+ });
2128
+ const finished = animation.finished.then(() => void 0).catch(() => void 0);
2129
+ return {
2130
+ finished,
2131
+ cancel: () => {
2132
+ try {
2133
+ animation.cancel();
2134
+ } catch {
2135
+ return;
2136
+ }
2137
+ }
2138
+ };
2139
+ }
2140
+ var DEFAULT_DURATION3, DEFAULT_EASING, PRESET_FRAMES, INSTANT;
2141
+ var init_anchor_animations = __esm({
2142
+ "src/core/primitives/anchor-animations.ts"() {
2143
+ init_animation();
2144
+ DEFAULT_DURATION3 = 200;
2145
+ DEFAULT_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
2146
+ PRESET_FRAMES = {
2147
+ fade: {
2148
+ enter: [{ opacity: 0 }, { opacity: 1 }],
2149
+ exit: [{ opacity: 1 }, { opacity: 0 }]
2150
+ },
2151
+ scale: {
2152
+ enter: [
2153
+ { opacity: 0, transform: "scale(0.85)" },
2154
+ { opacity: 1, transform: "scale(1)" }
2155
+ ],
2156
+ exit: [
2157
+ { opacity: 1, transform: "scale(1)" },
2158
+ { opacity: 0, transform: "scale(0.85)" }
2159
+ ]
2160
+ },
2161
+ slide: {
2162
+ enter: [
2163
+ { opacity: 0, transform: "translateY(10px)" },
2164
+ { opacity: 1, transform: "translateY(0)" }
2165
+ ],
2166
+ exit: [
2167
+ { opacity: 1, transform: "translateY(0)" },
2168
+ { opacity: 0, transform: "translateY(10px)" }
2169
+ ]
2170
+ },
2171
+ pop: {
2172
+ enter: [
2173
+ { opacity: 0, transform: "scale(0.6)", offset: 0 },
2174
+ { opacity: 1, transform: "scale(1.06)", offset: 0.7 },
2175
+ { opacity: 1, transform: "scale(1)", offset: 1 }
2176
+ ],
2177
+ exit: [
2178
+ { opacity: 1, transform: "scale(1)" },
2179
+ { opacity: 0, transform: "scale(0.6)" }
2180
+ ]
2181
+ }
2182
+ };
2183
+ INSTANT = {
2184
+ finished: Promise.resolve(),
2185
+ cancel: () => {
2186
+ }
2187
+ };
2188
+ }
2189
+ });
2190
+
2191
+ // src/core/features/sheet-anchors.ts
2192
+ function attachAnchor(deps, opts) {
2193
+ const doc = deps.host.ownerDocument;
2194
+ const wrapper = doc.createElement("div");
2195
+ wrapper.className = "bs-anchor";
2196
+ const interactive = opts.interactive ?? true;
2197
+ const position = opts.position ?? "sheet-top-right";
2198
+ const isDock = position === "dock-bottom" || position === "dock-top";
2199
+ const ws = wrapper.style;
2200
+ ws.position = "fixed";
2201
+ if (isDock) {
2202
+ ws.left = "0";
2203
+ ws.right = "0";
2204
+ if (position === "dock-bottom") {
2205
+ ws.bottom = "0";
2206
+ } else {
2207
+ ws.top = "0";
2208
+ }
2209
+ } else {
2210
+ applyOverlayPosition(ws, deps.mode, position, opts.inset ?? "16px");
2211
+ }
2212
+ if (opts.fadeRange) {
2213
+ const [r0, r1] = opts.fadeRange;
2214
+ const span = Math.max(r1 - r0, 1e-4);
2215
+ ws.opacity = `clamp(0, calc((var(--bs-progress, 0) - ${r0}) / ${span}), 1)`;
2216
+ }
2217
+ wrapper.appendChild(opts.element);
2218
+ deps.host.appendChild(wrapper);
2219
+ let visible = null;
2220
+ let inFlight = null;
2221
+ const shouldShow = (state) => {
2222
+ if (typeof opts.showOn === "function") return opts.showOn(state);
2223
+ if (Array.isArray(opts.showOn)) return opts.showOn.includes(state.activeId);
2224
+ return isDock || state.size > 0;
2225
+ };
2226
+ const applyVisibility = (next, animate) => {
2227
+ if (next === visible) return;
2228
+ visible = next;
2229
+ inFlight?.cancel();
2230
+ inFlight = null;
2231
+ wrapper.style.pointerEvents = next && interactive ? "auto" : "none";
2232
+ if (next) {
2233
+ wrapper.style.visibility = "";
2234
+ if (animate) {
2235
+ inFlight = runAnchorTransition(opts.element, opts.animation, "enter");
2236
+ }
2237
+ return;
2238
+ }
2239
+ if (!animate) {
2240
+ wrapper.style.visibility = "hidden";
2241
+ return;
2242
+ }
2243
+ const handle = runAnchorTransition(opts.element, opts.animation, "exit");
2244
+ inFlight = handle;
2245
+ void handle.finished.then(() => {
2246
+ if (inFlight === handle && visible === false) {
2247
+ wrapper.style.visibility = "hidden";
2248
+ }
2249
+ });
2250
+ };
2251
+ const evaluate = (animate) => {
2252
+ if (deps.isDestroyed()) return;
2253
+ applyVisibility(shouldShow(deps.getState()), animate);
2254
+ };
2255
+ const offs = [
2256
+ deps.on("snap", () => evaluate(true)),
2257
+ deps.on("open", () => evaluate(true)),
2258
+ deps.on("close", () => evaluate(true))
2259
+ ];
2260
+ evaluate(false);
2261
+ return {
2262
+ wrapper,
2263
+ detach: () => {
2264
+ offs.forEach((off) => off());
2265
+ inFlight?.cancel();
2266
+ inFlight = null;
2267
+ if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
2268
+ },
2269
+ syncZ: (z) => {
2270
+ wrapper.style.zIndex = String(z);
2271
+ }
2272
+ };
2273
+ }
2274
+ var init_sheet_anchors = __esm({
2275
+ "src/core/features/sheet-anchors.ts"() {
2276
+ init_overlay_position();
2277
+ init_anchor_animations();
2278
+ }
2279
+ });
2280
+
2281
+ // src/core/features/scrim-stages.ts
2282
+ function installScrimStages(deps, opts) {
2283
+ const doc = deps.host.ownerDocument;
2284
+ const entries = opts.stages.map((def) => {
2285
+ const wrapper = doc.createElement("div");
2286
+ wrapper.className = "bs-scrim-stage";
2287
+ const ws = wrapper.style;
2288
+ ws.position = "absolute";
2289
+ applyOverlayPosition(
2290
+ ws,
2291
+ deps.mode,
2292
+ def.position ?? opts.position ?? "center",
2293
+ def.inset ?? opts.inset ?? "16px"
2294
+ );
2295
+ ws.visibility = "hidden";
2296
+ ws.pointerEvents = "none";
2297
+ wrapper.appendChild(def.element);
2298
+ deps.host.appendChild(wrapper);
2299
+ return { def, wrapper, inFlight: null };
2300
+ });
2301
+ let active = null;
2302
+ const resolveActive = () => {
2303
+ const state = deps.getState();
2304
+ if (state.size === 0) {
2305
+ return entries.find((e) => matchesId(e.def, state.activeId)) ?? null;
2306
+ }
2307
+ return entries.find((e) => matchesId(e.def, state.activeId)) ?? entries.find((e) => matchesRange(e.def, state.progress)) ?? null;
2308
+ };
2309
+ const hide = (entry, animate) => {
2310
+ entry.inFlight?.cancel();
2311
+ entry.wrapper.style.pointerEvents = "none";
2312
+ if (!animate) {
2313
+ entry.inFlight = null;
2314
+ entry.wrapper.style.visibility = "hidden";
2315
+ return;
2316
+ }
2317
+ const handle = runAnchorTransition(
2318
+ entry.def.element,
2319
+ entry.def.animation ?? opts.animation,
2320
+ "exit"
2321
+ );
2322
+ entry.inFlight = handle;
2323
+ void handle.finished.then(() => {
2324
+ if (entry.inFlight === handle && active !== entry) {
2325
+ entry.wrapper.style.visibility = "hidden";
2326
+ }
2327
+ });
2328
+ };
2329
+ const show = (entry, animate) => {
2330
+ entry.inFlight?.cancel();
2331
+ entry.inFlight = null;
2332
+ entry.wrapper.style.visibility = "";
2333
+ entry.wrapper.style.pointerEvents = entry.def.interactive ?? opts.interactive ?? false ? "auto" : "none";
2334
+ if (animate) {
2335
+ entry.inFlight = runAnchorTransition(
2336
+ entry.def.element,
2337
+ entry.def.animation ?? opts.animation,
2338
+ "enter"
2339
+ );
2340
+ }
2341
+ };
2342
+ const update = (animate) => {
2343
+ if (deps.isDestroyed()) return;
2344
+ const next = resolveActive();
2345
+ if (next === active) return;
2346
+ const prev = active;
2347
+ active = next;
2348
+ if (prev) hide(prev, animate);
2349
+ if (next) show(next, animate);
2350
+ };
2351
+ const offs = [
2352
+ deps.on("snap", () => update(true)),
2353
+ deps.on("open", () => update(true)),
2354
+ deps.on("close", () => update(true))
2355
+ ];
2356
+ if (entries.some((e) => e.def.forRange)) {
2357
+ offs.push(deps.on("progress", () => update(true)));
2358
+ }
2359
+ update(false);
2360
+ return () => {
2361
+ offs.forEach((off) => off());
2362
+ for (const entry of entries) {
2363
+ entry.inFlight?.cancel();
2364
+ entry.inFlight = null;
2365
+ if (entry.wrapper.parentNode) {
2366
+ entry.wrapper.parentNode.removeChild(entry.wrapper);
2367
+ }
2368
+ }
2369
+ active = null;
2370
+ };
2371
+ }
2372
+ var matchesId, matchesRange;
2373
+ var init_scrim_stages = __esm({
2374
+ "src/core/features/scrim-stages.ts"() {
2375
+ init_overlay_position();
2376
+ init_anchor_animations();
2377
+ matchesId = (def, activeId) => {
2378
+ if (def.for === void 0) return false;
2379
+ return Array.isArray(def.for) ? def.for.includes(activeId) : def.for === activeId;
2380
+ };
2381
+ matchesRange = (def, progress) => {
2382
+ if (!def.forRange) return false;
2383
+ const [from, to] = def.forRange;
2384
+ return progress >= from && progress <= to;
2385
+ };
2386
+ }
2387
+ });
2388
+
2207
2389
  // src/core/primitives/engine-options.ts
2208
2390
  function resolveEngineOptions(opts, extras) {
2209
2391
  const initialAllowed = opts.allowed ?? opts.snapPoints.map((p) => p.id);
@@ -2226,9 +2408,6 @@ function resolveEngineOptions(opts, extras) {
2226
2408
  scrimMode: opts.scrimMode,
2227
2409
  scrimColor: opts.scrimColor,
2228
2410
  scrimBlur: opts.scrimBlur,
2229
- // Auto-promote pointer-events when tap-to-close is on so the click
2230
- // listener actually receives events. Caller can still force `false`
2231
- // explicitly to opt out — `??` only kicks in for `undefined`.
2232
2411
  scrimInteractive: opts.scrimInteractive ?? (opts.scrimTapToClose ? true : void 0),
2233
2412
  scrimTapToClose: opts.scrimTapToClose,
2234
2413
  scrimPreset: opts.scrimPreset,
@@ -2249,10 +2428,6 @@ function resolveEngineOptions(opts, extras) {
2249
2428
  closeOnEscape: opts.closeOnEscape,
2250
2429
  lockBodyScroll: opts.lockBodyScroll,
2251
2430
  inertSiblings: opts.inertSiblings,
2252
- // Folded in from `extras` so the lifecycle bag is fully assembled at
2253
- // the call site — engine no longer needs a spread+override at
2254
- // controller construction. When `extras` is omitted, the field stays
2255
- // undefined and the controller uses its own default predicate.
2256
2431
  shouldApplyInertSiblings: extras?.shouldApplyInertSiblings
2257
2432
  }
2258
2433
  };
@@ -2291,6 +2466,8 @@ var init_BottomSheetEngine = __esm({
2291
2466
  init_aria_slider_writer();
2292
2467
  init_hot_path_thresholds();
2293
2468
  init_scroll_cache();
2469
+ init_sheet_anchors();
2470
+ init_scrim_stages();
2294
2471
  init_engine_options();
2295
2472
  init_opacity_dedup();
2296
2473
  HAPTIC_DURATION_MS = 8;
@@ -2321,6 +2498,7 @@ var init_BottomSheetEngine = __esm({
2321
2498
  __publicField(this, "scrollContainer");
2322
2499
  __publicField(this, "backdrop");
2323
2500
  __publicField(this, "screenComponent");
2501
+ __publicField(this, "scrimParent", null);
2324
2502
  __publicField(this, "mode");
2325
2503
  __publicField(this, "flickVelocity");
2326
2504
  __publicField(this, "dragThreshold");
@@ -2331,40 +2509,22 @@ var init_BottomSheetEngine = __esm({
2331
2509
  __publicField(this, "lifecycle");
2332
2510
  __publicField(this, "closeOnBack");
2333
2511
  __publicField(this, "routedTo");
2334
- // Engine retains `snapPointsRaw` for the "find a closed-id" query in
2335
- // close() (raw input, not resolved geometry). Resolver owns geometry.
2336
2512
  __publicField(this, "snapPointsRaw");
2337
2513
  __publicField(this, "snaps");
2338
2514
  __publicField(this, "activeId");
2339
2515
  __publicField(this, "size", 0);
2340
- // Constructed in attach(); a plugin's install() could read `state` before
2341
- // attach completes if the constructor throws partway through. State getter
2342
- // null-guards via `this.gesture?.isDragging ?? false`.
2343
2516
  __publicField(this, "gesture");
2344
2517
  __publicField(this, "rootEl", null);
2345
2518
  __publicField(this, "destroyed", false);
2346
- // In-flight ViewTransition handle. A second snapTo while a VT is in flight
2347
- // would otherwise let Chromium reject the second's promise and flicker —
2348
- // newCycle() calls skipTransition() to abort cleanly.
2349
2519
  __publicField(this, "currentViewTransition", null);
2350
- // Each cycle captures the signal at entry; when a newer cycle aborts the
2351
- // controller, the captured signal flips to `aborted=true` and post-await
2352
- // guards short-circuit (prevents stale snap/open/close emits).
2353
2520
  __publicField(this, "currentAbort", new AbortController());
2354
- // Sub-pixel `--bs-size` deltas and <1% `--bs-progress` deltas are not
2355
- // visually distinguishable; gating CSSOM writes drops 30-50% of writes
2356
- // during the spring's settle tail.
2357
2521
  __publicField(this, "sizeWriteSentinel", new WriteSentinel());
2358
2522
  __publicField(this, "progressWriteSentinel", new WriteSentinel());
2359
2523
  __publicField(this, "isTopSheet", true);
2360
- // Pooled payload — identity documented in SheetEventMap.progress JSDoc;
2361
- // consumers must not retain across frames.
2362
2524
  __publicField(this, "progressPayload", {
2363
2525
  value: 0,
2364
2526
  size: 0
2365
2527
  });
2366
- // Pooled buffer for `getDragContext()` — onMove calls this 60-120×/s.
2367
- // Inner `range` object identity is also stable (mutated in place).
2368
2528
  __publicField(this, "dragContextBuf", {
2369
2529
  size: 0,
2370
2530
  maxAxisSize: 0,
@@ -2375,13 +2535,13 @@ var init_BottomSheetEngine = __esm({
2375
2535
  __publicField(this, "persistKey");
2376
2536
  __publicField(this, "linkedSheets", []);
2377
2537
  __publicField(this, "scrollCache");
2378
- // Single LIFO stack for ALL destroy-only teardowns. Plugins (added last)
2379
- // tear down first before the infrastructure they depend on. Toggle-semantic
2380
- // listeners (focus-trap / scroll-lock / scrim tap-to-close) stay as named
2381
- // fields on their controllers.
2382
2538
  __publicField(this, "teardowns", new TeardownStack());
2383
- // Pre-computed transform template — built once per engine since `mode` is
2384
- // fixed for its lifetime. Avoids the 4-arm switch at 60-120 Hz.
2539
+ __publicField(this, "anchors", []);
2540
+ __publicField(this, "anchorHost", null);
2541
+ __publicField(this, "stackZ", 100);
2542
+ __publicField(this, "detachScrimStages", null);
2543
+ __publicField(this, "stackEffectEnabled", false);
2544
+ __publicField(this, "stackEffectPrimed", false);
2385
2545
  __publicField(this, "transformTemplate");
2386
2546
  const resolved = resolveEngineOptions(opts, {
2387
2547
  shouldApplyInertSiblings: () => {
@@ -2396,7 +2556,8 @@ var init_BottomSheetEngine = __esm({
2396
2556
  this.handle = opts.handle ?? opts.element;
2397
2557
  this.scrollContainer = opts.scrollContainer;
2398
2558
  this.backdrop = opts.backdrop;
2399
- this.screenComponent = opts.screenComponent;
2559
+ this.screenComponent = opts.scrim ?? opts.screenComponent;
2560
+ this.scrimParent = this.screenComponent?.parentElement ?? null;
2400
2561
  this.mode = resolved.mode;
2401
2562
  this.transformTemplate = buildTransformTemplate(this.mode);
2402
2563
  this.aria = new AriaSliderWriter(this.handle, this.mode);
@@ -2414,6 +2575,9 @@ var init_BottomSheetEngine = __esm({
2414
2575
  getResolvedSnaps: () => this.snaps.getResolvedSnaps().slice(),
2415
2576
  snapTo: (id) => {
2416
2577
  void this.snapTo(id);
2578
+ },
2579
+ close: () => {
2580
+ void this.close();
2417
2581
  }
2418
2582
  },
2419
2583
  resolved.scrim
@@ -2424,8 +2588,6 @@ var init_BottomSheetEngine = __esm({
2424
2588
  getRootEl: () => this.rootEl,
2425
2589
  applySize: (size) => this.applySize(size),
2426
2590
  getSize: () => this.size,
2427
- // Optional chain protects synthetic test paths that drive animateTo
2428
- // before attach() (gesture is constructed there).
2429
2591
  isDragging: () => this.gesture?.isDragging ?? false
2430
2592
  },
2431
2593
  resolved.animation
@@ -2439,6 +2601,7 @@ var init_BottomSheetEngine = __esm({
2439
2601
  );
2440
2602
  this.closeOnBack = resolved.closeOnBack;
2441
2603
  this.routedTo = opts.routedTo;
2604
+ this.stackEffectEnabled = opts.stackEffect ?? false;
2442
2605
  this.snapPointsRaw = opts.snapPoints;
2443
2606
  this.persistKey = opts.persistKey;
2444
2607
  if (opts.linkedSheets) {
@@ -2451,7 +2614,7 @@ var init_BottomSheetEngine = __esm({
2451
2614
  this.snapPointsRaw,
2452
2615
  resolved.initialAllowed,
2453
2616
  this.mode,
2454
- () => this.handle.offsetHeight,
2617
+ () => layoutAxis(this.mode) === "width" ? this.handle.offsetWidth : this.handle.offsetHeight,
2455
2618
  (maxAxisSize) => {
2456
2619
  this.element.style[layoutAxis(this.mode)] = `${maxAxisSize}px`;
2457
2620
  }
@@ -2530,21 +2693,6 @@ var init_BottomSheetEngine = __esm({
2530
2693
  on(event, fn) {
2531
2694
  return this.bus.on(event, fn);
2532
2695
  }
2533
- /**
2534
- * Register a plugin. `install` runs synchronously; if it returns a function,
2535
- * it's captured for teardown during `destroy()`. No-op if already destroyed
2536
- * — installing into a torn-down engine would leak listeners.
2537
- *
2538
- * Throws from `plugin.install` are caught and rethrown via `queueMicrotask`
2539
- * so a buggy plugin can't take the engine down with it: the engine stays
2540
- * usable, sibling plugins still install, and the error surfaces in dev tools
2541
- * on the next tick instead of being swallowed. Mirrors the per-fn isolation
2542
- * model used for teardowns.
2543
- *
2544
- * Plugins that accept the second `scope` argument get transactional cleanup:
2545
- * push partial-install cleanups via `scope.add(fn)` and the engine drains
2546
- * them on throw, OR merges them into destroy-time teardowns on success.
2547
- */
2548
2696
  use(plugin) {
2549
2697
  if (this.destroyed) {
2550
2698
  console.warn(
@@ -2554,9 +2702,6 @@ var init_BottomSheetEngine = __esm({
2554
2702
  }
2555
2703
  const scoped = [];
2556
2704
  const scope = {
2557
- // typeof check — not just truthiness — so a JS-only plugin pushing a
2558
- // bogus value is silently rejected instead of queueing a confusing
2559
- // TypeError on destroy() drain.
2560
2705
  add: (fn) => {
2561
2706
  if (typeof fn === "function") scoped.push(fn);
2562
2707
  }
@@ -2585,36 +2730,13 @@ var init_BottomSheetEngine = __esm({
2585
2730
  }
2586
2731
  return this;
2587
2732
  }
2588
- /**
2589
- * Replace the linked-sheets list at runtime. Use when peer engines aren't
2590
- * available at construction time (mounted later in the tree). The wiring
2591
- * to the `open` event is independent of this list — it reads `linkedSheets`
2592
- * lazily on emit, so this setter is a simple field swap.
2593
- */
2594
2733
  setLinkedSheets(sheets) {
2595
2734
  if (this.destroyed) return;
2596
2735
  this.linkedSheets = sheets;
2597
2736
  }
2598
- /** Read-only view of the current allow-list, for cross-sheet target negotiation. */
2599
2737
  getAllowedIds() {
2600
2738
  return this.snaps.getAllowedIds().slice();
2601
2739
  }
2602
- /**
2603
- * Animate to a snap point by id. Resolves when the animation completes.
2604
- *
2605
- * Two call shapes:
2606
- * ```ts
2607
- * engine.snapTo("full"); // default velocity
2608
- * engine.snapTo("full", 0.5); // px/ms initial velocity
2609
- * engine.snapTo("full", { velocity: 0.5, signal }); // abortable
2610
- * ```
2611
- *
2612
- * When `opts.signal` is provided, calling `controller.abort()` cancels the
2613
- * in-flight tween/spring and the returned promise resolves without firing
2614
- * `snap`. The captured `AbortSignal` is checked after each await so external
2615
- * aborts ride the same cycle invariant as internal ones (resize, destroy,
2616
- * concurrent snapTo).
2617
- */
2618
2740
  async snapTo(id, velocityOrOpts = 0, _skipBeforeSnap = false) {
2619
2741
  if (this.destroyed) return;
2620
2742
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2673,16 +2795,6 @@ var init_BottomSheetEngine = __esm({
2673
2795
  this.handleClose();
2674
2796
  }
2675
2797
  }
2676
- /**
2677
- * Drive the sheet to an arbitrary px size, bypassing snap matching.
2678
- * Useful for preview scrubs, animation chaining, or programmatic gestures.
2679
- * Animates with the configured spring/tween. Resolves when the animation
2680
- * completes. Does NOT update `activeId` — call `snapTo(id)` after to commit.
2681
- *
2682
- * Unlike `snapTo`, this does not fire `before-snap` (it isn't a snap commit)
2683
- * nor `snap` (the sheet isn't settling to a known snap). `progress` events
2684
- * still fire via applySize. Target is clamped to `[0, maxAxisSize]`.
2685
- */
2686
2798
  async dragTo(targetSize, velocityOrOpts = 0) {
2687
2799
  if (this.destroyed) return;
2688
2800
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2712,28 +2824,13 @@ var init_BottomSheetEngine = __esm({
2712
2824
  }
2713
2825
  setAllowed(ids, snap) {
2714
2826
  if (this.destroyed) return;
2715
- const willSnap = snap !== void 0 && ids.includes(snap) || !ids.includes(this.activeId) && ids.length > 0;
2716
- if (!willSnap) {
2717
- this.animation.cancel();
2718
- this.newCycle();
2719
- }
2720
2827
  this.snaps.setAllowedIds(ids);
2721
2828
  this.updateAriaSlider();
2722
2829
  if (snap && ids.includes(snap)) {
2723
- void this.snapTo(
2724
- snap,
2725
- 0,
2726
- /* internal */
2727
- true
2728
- );
2830
+ void this.snapTo(snap, 0, true);
2729
2831
  } else if (!ids.includes(this.activeId)) {
2730
2832
  const fallback = ids[0];
2731
- if (fallback) void this.snapTo(
2732
- fallback,
2733
- 0,
2734
- /* internal */
2735
- true
2736
- );
2833
+ if (fallback) void this.snapTo(fallback, 0, true);
2737
2834
  }
2738
2835
  }
2739
2836
  setBackdropRange(range) {
@@ -2751,21 +2848,6 @@ var init_BottomSheetEngine = __esm({
2751
2848
  setScrimInteractive(interactive) {
2752
2849
  this.scrim.setScrimInteractive(interactive);
2753
2850
  }
2754
- /**
2755
- * Batch-update multiple scrim fields in one call. Apply order is:
2756
- * 1. `preset` — fills baseline color/blur/range/interactive.
2757
- * 2. `mode` — re-positions the scrim (fixed inset / clear / hide).
2758
- * 3. `enabled` — flips the opacity contribution on/off.
2759
- * 4. `tapToClose` — installs/tears the click listener.
2760
- * 5. `color` / `blur` / `interactive` / `range` — explicit overrides.
2761
- * 6. ONE `applySize` so the per-frame opacity stays consistent.
2762
- *
2763
- * Performance: collecting changes here (vs calling individual setters)
2764
- * avoids repeated `applySize` calls during a multi-field swap. Setters
2765
- * called here suppress their own internal `applySize` either by passing
2766
- * through fast-paths (mode/enabled no-op when value unchanged) or by
2767
- * being visual-only (color/blur/interactive don't influence opacity).
2768
- */
2769
2851
  setScrim(opts) {
2770
2852
  this.scrim.setScrim(opts, () => this.applySize(this.size));
2771
2853
  }
@@ -2781,11 +2863,79 @@ var init_BottomSheetEngine = __esm({
2781
2863
  setScrimOverlay(opts) {
2782
2864
  return this.scrim.setScrimOverlay(opts);
2783
2865
  }
2784
- /**
2785
- * Read-only snapshot of scrim state. Use the `setScrim*` setters to mutate.
2786
- * Stable public API — internals (e.g. cached opacity sentinels) are NOT
2787
- * exposed so consumers can't accidentally couple to ScrimController shape.
2788
- */
2866
+ addAnchor(opts) {
2867
+ if (this.destroyed || typeof document === "undefined") return () => {
2868
+ };
2869
+ if (!this.anchorHost) {
2870
+ this.anchorHost = this.rootEl ?? this.element.parentElement ?? document.body;
2871
+ this.anchorHost.style.setProperty("--bs-size", `${this.size}px`);
2872
+ this.anchorHost.style.setProperty(
2873
+ "--bs-progress",
2874
+ String(this.computeProgress(this.size))
2875
+ );
2876
+ }
2877
+ const handle = attachAnchor(
2878
+ {
2879
+ mode: this.mode,
2880
+ host: this.anchorHost,
2881
+ getState: () => ({
2882
+ activeId: this.activeId,
2883
+ size: this.size,
2884
+ progress: this.computeProgress(this.size)
2885
+ }),
2886
+ on: (event, fn) => this.on(event, fn),
2887
+ isDestroyed: () => this.destroyed
2888
+ },
2889
+ opts
2890
+ );
2891
+ handle.syncZ(this.stackZ + 1);
2892
+ this.anchors.push(handle);
2893
+ const detach = () => {
2894
+ const idx = this.anchors.indexOf(handle);
2895
+ if (idx === -1) return;
2896
+ this.anchors.splice(idx, 1);
2897
+ handle.detach();
2898
+ };
2899
+ this.teardowns.add(detach);
2900
+ return detach;
2901
+ }
2902
+ setScrimStages(opts) {
2903
+ if (this.destroyed || typeof document === "undefined") return () => {
2904
+ };
2905
+ this.detachScrimStages?.();
2906
+ this.detachScrimStages = null;
2907
+ if (!opts) return () => {
2908
+ };
2909
+ const host = this.screenComponent?.parentElement;
2910
+ if (!host) {
2911
+ console.warn(
2912
+ "[BottomSheet] setScrimStages: no scrim element mounted \u2014 pass `scrim` to the engine first."
2913
+ );
2914
+ return () => {
2915
+ };
2916
+ }
2917
+ const detachInstalled = installScrimStages(
2918
+ {
2919
+ mode: this.mode,
2920
+ host,
2921
+ getState: () => ({
2922
+ activeId: this.activeId,
2923
+ size: this.size,
2924
+ progress: this.computeProgress(this.size)
2925
+ }),
2926
+ on: (event, fn) => this.on(event, fn),
2927
+ isDestroyed: () => this.destroyed
2928
+ },
2929
+ opts
2930
+ );
2931
+ const detach = () => {
2932
+ if (this.detachScrimStages === detach) this.detachScrimStages = null;
2933
+ detachInstalled();
2934
+ };
2935
+ this.detachScrimStages = detach;
2936
+ this.teardowns.add(() => this.detachScrimStages?.());
2937
+ return detach;
2938
+ }
2789
2939
  getScrimState() {
2790
2940
  return { mode: this.scrim.scrimMode, enabled: this.scrim.scrimEnabled };
2791
2941
  }
@@ -2831,24 +2981,11 @@ var init_BottomSheetEngine = __esm({
2831
2981
  emit(event, payload) {
2832
2982
  this.bus.emit(event, payload);
2833
2983
  }
2834
- /**
2835
- * Abort the current cycle's signal and replace the controller. Returns the
2836
- * fresh signal so the caller can capture it locally before any await — when
2837
- * a future newCycle() / destroy aborts our controller, the captured signal
2838
- * flips to aborted and post-await guards short-circuit.
2839
- */
2840
2984
  newCycle() {
2841
2985
  this.currentAbort.abort();
2842
2986
  this.currentAbort = new AbortController();
2843
2987
  return this.currentAbort.signal;
2844
2988
  }
2845
- /**
2846
- * Fire `before-snap` and report whether a listener cancelled it. The
2847
- * `cancel()` callback exposed to listeners is frozen after the synchronous
2848
- * emit phase so async callers can't silently miss — they get a console
2849
- * warning. Returns `true` when cancelled so callers can early-return /
2850
- * spring-back without re-implementing the cancel/frozen pattern.
2851
- */
2852
2989
  emitBeforeSnap(target, previousId) {
2853
2990
  let cancelled = false;
2854
2991
  let frozen = false;
@@ -2882,59 +3019,54 @@ var init_BottomSheetEngine = __esm({
2882
3019
  this.snaps.setRaw(this.snapPointsRaw);
2883
3020
  this.scrim.invalidateOpacityCache();
2884
3021
  }
2885
- /**
2886
- * Memoised `allowedRange()` — delegates to SnapResolver which owns the cache.
2887
- * Engine wrapper kept so existing call sites don't all need updating;
2888
- * resolver-level memoization survives across method invocations.
2889
- */
2890
3022
  getAllowedRange() {
2891
3023
  return this.snaps.getAllowedRange();
2892
3024
  }
2893
- /**
2894
- * Write ARIA slider value attributes onto the handle so the engine itself
2895
- * is the source of truth for screen-reader announcements. Called from the
2896
- * constructor and from every code path that mutates `activeId` /
2897
- * `allowedIds`. WAI-ARIA 1.2 requires `aria-valuenow` for `role="slider"` —
2898
- * without it screen readers can't announce drag/snap progress to keyboard
2899
- * + AT users (WCAG 4.1.2 Name, Role, Value).
2900
- *
2901
- * Engine does NOT set `role="slider"` itself — that's the adapter's or
2902
- * consumer markup's responsibility. We only decorate with dynamic values.
2903
- *
2904
- * Empty allowed-list is malformed input (settable via `setAllowed([])`);
2905
- * short-circuit to remove ARIA value attributes so screen readers see a
2906
- * no-value slider rather than a degenerate `min=max=now=0` (WAI-ARIA 1.2
2907
- * requires `valuemax > valuemin` for `role="slider"`).
2908
- */
3025
+ allowedIdsBySize() {
3026
+ return this.snaps.getAllowedIds().slice().sort(
3027
+ (a, b) => (this.snaps.findById(a)?.size ?? 0) - (this.snaps.findById(b)?.size ?? 0)
3028
+ );
3029
+ }
2909
3030
  updateAriaSlider() {
2910
- this.aria.setValue(this.snaps.getAllowedIds(), this.activeId);
3031
+ this.aria.setValue(this.allowedIdsBySize(), this.activeId);
2911
3032
  }
2912
3033
  registerInStack() {
2913
3034
  this.teardowns.add(sheetStack.push({
2914
3035
  id: this.id,
2915
3036
  setZIndex: (z) => {
3037
+ this.stackZ = z;
2916
3038
  this.element.style.zIndex = String(z);
2917
3039
  if (this.backdrop) this.backdrop.style.zIndex = String(z - 1);
3040
+ for (const anchor of this.anchors) anchor.syncZ(z + 1);
2918
3041
  },
2919
3042
  setIsTop: (isTop) => {
2920
3043
  this.isTopSheet = isTop;
2921
3044
  if (this.backdrop) {
2922
3045
  this.backdrop.style.display = isTop ? "" : "none";
2923
3046
  }
2924
- }
3047
+ },
3048
+ isOpen: () => this.size > 0,
3049
+ setDepth: (depth) => this.applyStackDepth(depth)
2925
3050
  }));
2926
3051
  }
3052
+ applyStackDepth(depth) {
3053
+ if (!this.stackEffectEnabled) return;
3054
+ this.element.setAttribute("data-stack-depth", String(depth));
3055
+ if (!this.stackEffectPrimed) {
3056
+ this.stackEffectPrimed = true;
3057
+ const isVertical = this.mode === "bottom" || this.mode === "top";
3058
+ this.element.style.transformOrigin = isVertical ? "50% 100%" : "0% 50%";
3059
+ this.element.style.transition = "scale 320ms cubic-bezier(0.29, 1.04, 0.84, 0.99)";
3060
+ }
3061
+ const scale = Math.max(1 - depth * 0.04, 0.86);
3062
+ this.element.style.scale = scale === 1 ? "" : String(scale);
3063
+ }
2927
3064
  attach() {
2928
3065
  this.gesture = new GestureController({
2929
3066
  handle: this.handle,
2930
3067
  element: this.element,
2931
3068
  mode: this.mode,
2932
3069
  getRoot: () => this.rootEl,
2933
- // Single coalesced reader — narrows the DI surface and removes 3 getter
2934
- // dispatches per onMove frame at 60-120Hz. Returns a SHARED buffer
2935
- // (this.dragContextBuf) — see field JSDoc for the retention contract.
2936
- // Inner `range` object is mutated in place too so callers caching its
2937
- // identity between frames stay valid.
2938
3070
  getDragContext: () => {
2939
3071
  const buf = this.dragContextBuf;
2940
3072
  const r = this.getAllowedRange();
@@ -2988,7 +3120,7 @@ var init_BottomSheetEngine = __esm({
2988
3120
  container: this.scrollContainer,
2989
3121
  isDragging: () => this.gesture?.isDragging ?? false,
2990
3122
  isAnimating: () => this.animation.isAnimating,
2991
- getAllowedIds: () => this.snaps.getAllowedIds(),
3123
+ getAllowedIds: () => this.allowedIdsBySize(),
2992
3124
  getActiveId: () => this.activeId,
2993
3125
  snapTo: (id) => {
2994
3126
  void this.snapTo(id);
@@ -3017,11 +3149,13 @@ var init_BottomSheetEngine = __esm({
3017
3149
  },
3018
3150
  newCycle: () => {
3019
3151
  this.newCycle();
3020
- }
3152
+ },
3153
+ isAnimating: () => this.animation.isAnimating,
3154
+ resyncAfterCancel: () => this.resyncAfterResize()
3021
3155
  }));
3022
3156
  if (this.lifecycle.closeOnEscape) {
3023
3157
  const onKey = (e) => {
3024
- if (this.destroyed) return;
3158
+ if (this.destroyed || e.defaultPrevented) return;
3025
3159
  if (e.key === "Escape" && this.isTopSheet && this.size > 0) {
3026
3160
  void this.close();
3027
3161
  }
@@ -3053,7 +3187,7 @@ var init_BottomSheetEngine = __esm({
3053
3187
  handle: this.handle,
3054
3188
  mode: this.mode,
3055
3189
  isDestroyed: () => this.destroyed,
3056
- getAllowedIds: () => this.snaps.getAllowedIds(),
3190
+ getAllowedIds: () => this.allowedIdsBySize(),
3057
3191
  getActiveId: () => this.activeId,
3058
3192
  snapTo: (id) => {
3059
3193
  void this.snapTo(id);
@@ -3074,6 +3208,7 @@ var init_BottomSheetEngine = __esm({
3074
3208
  });
3075
3209
  if (!target) return;
3076
3210
  const previousId = this.activeId;
3211
+ if (target.id === previousId && target.size === this.size) return;
3077
3212
  if (this.emitBeforeSnap(target, previousId)) {
3078
3213
  const restore = this.snaps.findById(previousId);
3079
3214
  if (restore) {
@@ -3082,6 +3217,7 @@ var init_BottomSheetEngine = __esm({
3082
3217
  return;
3083
3218
  }
3084
3219
  const previousSize = this.size;
3220
+ const previousSnapSize = this.snaps.findById(previousId)?.size;
3085
3221
  this.scrollCache.cache(previousId, previousSize, target.size);
3086
3222
  this.activeId = target.id;
3087
3223
  void this.animation.animateTo(target.size, velocity).then(() => {
@@ -3090,9 +3226,10 @@ var init_BottomSheetEngine = __esm({
3090
3226
  this.updateAriaSlider();
3091
3227
  this.emit("snap", { id: target.id, size: target.size });
3092
3228
  this.haptic();
3093
- if (previousId === "closed" && target.size > 0) {
3229
+ if (previousSnapSize === 0 && target.size > 0) {
3094
3230
  this.emit("open", { id: target.id });
3095
3231
  this.handleOpen();
3232
+ notifyLinkedSheets(this.linkedSheets, this);
3096
3233
  }
3097
3234
  if (target.size === 0) {
3098
3235
  this.emit("close", void 0);
@@ -3100,7 +3237,6 @@ var init_BottomSheetEngine = __esm({
3100
3237
  }
3101
3238
  });
3102
3239
  }
3103
- /** Trigger a brief haptic tick (iOS Safari + Android Chrome support). */
3104
3240
  haptic() {
3105
3241
  if (typeof navigator !== "undefined" && "vibrate" in navigator) {
3106
3242
  try {
@@ -3116,6 +3252,12 @@ var init_BottomSheetEngine = __esm({
3116
3252
  style.transform = this.transformTemplate(offset);
3117
3253
  if (this.sizeWriteSentinel.shouldWrite(size, SIZE_WRITE_EPSILON)) {
3118
3254
  style.setProperty("--bs-size", `${size}px`);
3255
+ if (this.scrimParent) {
3256
+ this.scrimParent.style.setProperty("--bs-size", `${size}px`);
3257
+ }
3258
+ if (this.anchorHost && this.anchorHost !== this.scrimParent) {
3259
+ this.anchorHost.style.setProperty("--bs-size", `${size}px`);
3260
+ }
3119
3261
  }
3120
3262
  const progress = this.computeProgress(size);
3121
3263
  const progressChanged = this.progressWriteSentinel.shouldWrite(
@@ -3124,6 +3266,9 @@ var init_BottomSheetEngine = __esm({
3124
3266
  );
3125
3267
  if (progressChanged) {
3126
3268
  style.setProperty("--bs-progress", String(progress));
3269
+ if (this.anchorHost) {
3270
+ this.anchorHost.style.setProperty("--bs-progress", String(progress));
3271
+ }
3127
3272
  }
3128
3273
  this.scrim.applyOpacity(progress, progressChanged);
3129
3274
  if (progressChanged && this.bus.listenerCount("progress") > 0) {
@@ -3138,6 +3283,7 @@ var init_BottomSheetEngine = __esm({
3138
3283
  return Math.min(Math.max((size - min) / (max - min), 0), 1);
3139
3284
  }
3140
3285
  handleOpen() {
3286
+ sheetStack.promote(this.id);
3141
3287
  try {
3142
3288
  this.lifecycle.install();
3143
3289
  } catch (err) {
@@ -3151,8 +3297,24 @@ var init_BottomSheetEngine = __esm({
3151
3297
  });
3152
3298
  }
3153
3299
  }
3300
+ resyncAfterResize() {
3301
+ if (this.destroyed) return;
3302
+ const target = this.snaps.findById(this.activeId);
3303
+ if (!target) return;
3304
+ this.updateAriaSlider();
3305
+ this.emit("snap", { id: target.id, size: target.size });
3306
+ if (target.size > 0 && !this.lifecycle.isInstalled) {
3307
+ this.emit("open", { id: target.id });
3308
+ this.handleOpen();
3309
+ notifyLinkedSheets(this.linkedSheets, this);
3310
+ } else if (target.size === 0 && this.lifecycle.isInstalled) {
3311
+ this.emit("close", void 0);
3312
+ this.handleClose();
3313
+ }
3314
+ }
3154
3315
  handleClose() {
3155
3316
  this.lifecycle.release();
3317
+ sheetStack.update();
3156
3318
  }
3157
3319
  };
3158
3320
  }
@@ -3162,7 +3324,7 @@ var init_BottomSheetEngine = __esm({
3162
3324
  var baseStyles;
3163
3325
  var init_baseStyles = __esm({
3164
3326
  "src/web-component/baseStyles.ts"() {
3165
- baseStyles = `:where(.bs-root){--bs-surface:#ffffff;--bs-surface-border:rgba(225,227,237,1);--bs-shadow:0 -3px 12px rgba(0,0,0,0.12);--bs-radius:20px;--bs-handle-color:#b5b3a8;--bs-handle-width:40px;--bs-handle-height:5px;--bs-z-backdrop:90;--bs-z-sheet:100;--bs-header-min-height:80px;--bs-backdrop-color:rgba(0,0,0,0.5);--bs-scrim-color:rgba(0,0,0,0.4);--bs-scrim-blur:0px;--bs-z-scrim:calc(var(--bs-z-sheet) - 1);--bs-size:0px;--bs-progress:0}.bs-backdrop{position:fixed;inset:0;background:var(--bs-backdrop-color);opacity:0;pointer-events:none;z-index:var(--bs-z-backdrop);transition:opacity 0.15s linear;will-change:opacity}.bs-root[data-dragging="true"] .bs-backdrop,.bs-root[data-animating="true"] .bs-backdrop{transition:none}.bs-sheet{position:fixed;background:var(--bs-surface);box-shadow:var(--bs-shadow);z-index:var(--bs-z-sheet);overflow:hidden;display:flex;flex-direction:column;-webkit-tap-highlight-color:transparent;user-select:none;overscroll-behavior:contain}.bs-sheet[data-mode="bottom"]{left:0;right:0;bottom:0;width:100%;border-top-left-radius:var(--bs-radius);border-top-right-radius:var(--bs-radius);border-top:1px solid var(--bs-surface-border);padding-bottom:env(safe-area-inset-bottom);box-shadow:0 calc(var(--bs-progress,0) * -2px - 3px) calc(var(--bs-progress,0) * 18px + 12px) rgba(0,0,0,calc(var(--bs-progress,0) * 0.15 + 0.12))}.bs-sheet[data-mode="top"]{left:0;right:0;top:0;width:100%;border-bottom-left-radius:var(--bs-radius);border-bottom-right-radius:var(--bs-radius);border-bottom:1px solid var(--bs-surface-border);flex-direction:column-reverse;box-shadow:0 3px 12px rgba(0,0,0,0.12);padding-top:env(safe-area-inset-top)}.bs-sheet[data-mode="left"]{top:0;bottom:0;left:0;height:100%;border-top-right-radius:var(--bs-radius);border-bottom-right-radius:var(--bs-radius);border-right:1px solid var(--bs-surface-border);box-shadow:3px 0 12px rgba(0,0,0,0.12)}.bs-sheet[data-mode="right"]{top:0;bottom:0;right:0;height:100%;border-top-left-radius:var(--bs-radius);border-bottom-left-radius:var(--bs-radius);border-left:1px solid var(--bs-surface-border);box-shadow:-3px 0 12px rgba(0,0,0,0.12)}.bs-sheet[data-mode="left"] .bs-content{padding-right:36px}.bs-sheet[data-mode="right"] .bs-content{padding-left:36px}.bs-handle{flex-shrink:0;min-height:44px;display:flex;flex-direction:column;justify-content:center;padding:8px 16px 4px;cursor:grab;touch-action:none;user-select:none;background:var(--bs-surface);-webkit-tap-highlight-color:transparent;outline:none;position:relative}.bs-handle:active{cursor:grabbing}.bs-handle:focus-visible{outline:2px solid currentColor;outline-offset:-2px;border-radius:var(--bs-radius)}.bs-handle::before{content:"";display:block;width:var(--bs-handle-width);height:var(--bs-handle-height);background:var(--bs-handle-color);border-radius:calc(var(--bs-handle-height) / 2);margin:0 auto 8px;transition:width 0.15s ease,background 0.15s ease,transform 0.15s ease;pointer-events:none}.bs-sheet[data-mode="left"] .bs-handle,.bs-sheet[data-mode="right"] .bs-handle{position:absolute;top:0;bottom:0;width:28px;min-width:0;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0;background:var(--bs-surface);z-index:1;overflow:hidden}.bs-sheet[data-mode="left"] .bs-handle>*,.bs-sheet[data-mode="right"] .bs-handle>*{display:none}.bs-sheet[data-mode="left"] .bs-handle{right:0;border-left:1px solid var(--bs-surface-border)}.bs-sheet[data-mode="right"] .bs-handle{left:0;border-right:1px solid var(--bs-surface-border)}.bs-sheet[data-mode="left"] .bs-handle::before,.bs-sheet[data-mode="right"] .bs-handle::before{width:var(--bs-handle-height);height:var(--bs-handle-width);margin:0}.bs-sheet:hover .bs-handle::before{background:color-mix(in oklab,var(--bs-handle-color),#000 15%)}.bs-handle:active::before{background:color-mix(in oklab,var(--bs-handle-color),#000 35%);transform:scaleX(1.15)}.bs-handle.bs-handle--ticks::after{content:"";display:block;position:absolute;left:50%;bottom:6px;transform:translateX(-50%);width:calc(var(--bs-handle-width) * 2);height:3px;background-image:radial-gradient( circle,var(--bs-handle-color) 0 1.5px,transparent 1.6px );background-size:8px 3px;background-repeat:repeat-x;background-position:center;opacity:0;transition:opacity 0.18s ease-out;pointer-events:none}.bs-root[data-dragging="true"] .bs-handle.bs-handle--ticks::after{opacity:0.4}.bs-sheet[data-mode="top"] .bs-handle.bs-handle--ticks::after{bottom:auto;top:6px}.bs-sheet[data-mode="left"] .bs-handle.bs-handle--ticks::after,.bs-sheet[data-mode="right"] .bs-handle.bs-handle--ticks::after{left:50%;top:50%;bottom:auto;transform:translate(-50%,-50%);width:3px;height:calc(var(--bs-handle-width) * 2);background-size:3px 8px;background-repeat:repeat-y;margin-left:calc(var(--bs-handle-height) + 6px)}.bs-sheet[data-mode="right"] .bs-handle.bs-handle--ticks::after{margin-left:calc(-1 * (var(--bs-handle-height) + 6px))}.bs-content{flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;padding:0 16px 16px}.bs-screen{position:absolute;inset:0;pointer-events:none;display:none;z-index:var(--bs-z-scrim)}.bs-scrim{background:var(--bs-scrim-color);transition:opacity 0.15s linear;will-change:opacity}.bs-root[data-dragging="true"] .bs-scrim,.bs-root[data-animating="true"] .bs-scrim{transition:none}.bs-scrim{backdrop-filter:blur(var(--bs-scrim-blur));-webkit-backdrop-filter:blur(var(--bs-scrim-blur))}.bs-button-slot{position:absolute;z-index:calc(var(--bs-z-sheet) + 1);pointer-events:auto}.bs-button-slot[data-side="left"]{left:16px;bottom:calc(var(--bs-size,0px) + 12px);transition:bottom 0.15s ease}.bs-button-slot[data-side="right"]{right:16px;bottom:calc(var(--bs-size,0px) + 12px);transition:bottom 0.15s ease}@media (prefers-reduced-motion:reduce){.bs-backdrop,.bs-handle::before,.bs-button-slot[data-side="left"],.bs-button-slot[data-side="right"]{transition:none !important}}@media (prefers-color-scheme:dark){:where(.bs-root){--bs-surface:#1a1a1d;--bs-surface-border:rgba(255,255,255,0.08);--bs-handle-color:rgba(255,255,255,0.45);--bs-shadow:0 -3px 16px rgba(0,0,0,0.5)}}.bs-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}:where(.ovl-root){--ovl-surface:var(--bs-surface,#ffffff);--ovl-surface-border:var(--bs-surface-border,rgba(0,0,0,0.08));--ovl-shadow:0 -8px 40px rgba(0,0,0,0.35);--ovl-radius:var(--bs-radius,20px);--ovl-backdrop-color:rgba(0,0,0,0.5);--ovl-z-backdrop:200;--ovl-z-panel:201}.ovl-backdrop{position:fixed;inset:0;background:var(--ovl-backdrop-color);opacity:0;pointer-events:none;z-index:var(--ovl-z-backdrop)}.ovl-panel{position:fixed;background:var(--ovl-surface);box-shadow:var(--ovl-shadow);z-index:var(--ovl-z-panel);-webkit-tap-highlight-color:transparent;outline:none;transition:transform 0.32s cubic-bezier(0.29,1.24,0.84,0.99),opacity 0.32s ease-out}.ovl-panel[data-state="open"]{transform:translate3d(0,0,0);opacity:1}@starting-style{.ovl-panel[data-edge="bottom"][data-state="open"]{transform:translate3d(0,100%,0);opacity:0}.ovl-panel[data-edge="top"][data-state="open"]{transform:translate3d(0,-100%,0);opacity:0}.ovl-panel[data-edge="left"][data-state="open"]{transform:translate3d(-100%,0,0);opacity:0}.ovl-panel[data-edge="right"][data-state="open"]{transform:translate3d(100%,0,0);opacity:0}}.ovl-panel[data-edge="bottom"]{left:0;right:0;bottom:0;width:100%;border-top-left-radius:var(--ovl-radius);border-top-right-radius:var(--ovl-radius);border-top:1px solid var(--ovl-surface-border);padding-bottom:env(safe-area-inset-bottom)}.ovl-panel[data-edge="top"]{left:0;right:0;top:0;width:100%;border-bottom-left-radius:var(--ovl-radius);border-bottom-right-radius:var(--ovl-radius);border-bottom:1px solid var(--ovl-surface-border);padding-top:env(safe-area-inset-top)}.ovl-panel[data-edge="left"]{top:0;bottom:0;left:0;height:100%;border-top-right-radius:var(--ovl-radius);border-bottom-right-radius:var(--ovl-radius);border-right:1px solid var(--ovl-surface-border)}.ovl-panel[data-edge="right"]{top:0;bottom:0;right:0;height:100%;border-top-left-radius:var(--ovl-radius);border-bottom-left-radius:var(--ovl-radius);border-left:1px solid var(--ovl-surface-border)}@media (prefers-reduced-motion:reduce){.ovl-panel,.ovl-backdrop{transition:none !important}}`;
3327
+ baseStyles = `:where(.bs-root){--bs-surface:#ffffff;--bs-surface-border:rgba(225,227,237,1);--bs-radius:20px;--bs-handle-color:#b5b3a8;--bs-handle-width:40px;--bs-handle-height:5px;--bs-z-backdrop:90;--bs-z-sheet:100;--bs-header-min-height:80px;--bs-backdrop-color:rgba(0,0,0,0.5);--bs-scrim-color:rgba(0,0,0,0.4);--bs-scrim-blur:0px;--bs-z-scrim:calc(var(--bs-z-sheet) - 1);--bs-size:0px;--bs-progress:0}.bs-backdrop{position:fixed;inset:0;background:var(--bs-backdrop-color);opacity:0;pointer-events:none;z-index:var(--bs-z-backdrop);transition:opacity 0.15s linear;will-change:opacity}.bs-root[data-dragging="true"] .bs-backdrop,.bs-root[data-animating="true"] .bs-backdrop{transition:none}.bs-sheet{position:fixed;background:var(--bs-surface);box-shadow:var(--bs-shadow,var(--bs-shadow-auto,none));z-index:var(--bs-z-sheet);overflow:hidden;display:flex;flex-direction:column;-webkit-tap-highlight-color:transparent;user-select:none;overscroll-behavior:contain}.bs-sheet[data-mode="bottom"]{left:0;right:0;bottom:0;width:100%;border-top-left-radius:var(--bs-radius);border-top-right-radius:var(--bs-radius);border-top:1px solid var(--bs-surface-border);padding-bottom:env(safe-area-inset-bottom);--bs-shadow-auto:0 calc(var(--bs-progress,0) * -2px - 3px) calc(var(--bs-progress,0) * 18px + 12px) rgba(0,0,0,calc(var(--bs-progress,0) * 0.15 + 0.12))}.bs-sheet[data-mode="top"]{left:0;right:0;top:0;width:100%;border-bottom-left-radius:var(--bs-radius);border-bottom-right-radius:var(--bs-radius);border-bottom:1px solid var(--bs-surface-border);flex-direction:column-reverse;--bs-shadow-auto:0 3px 12px rgba(0,0,0,0.12);padding-top:env(safe-area-inset-top)}.bs-sheet[data-mode="left"]{top:0;bottom:0;left:0;height:100%;border-top-right-radius:var(--bs-radius);border-bottom-right-radius:var(--bs-radius);border-right:1px solid var(--bs-surface-border);--bs-shadow-auto:3px 0 12px rgba(0,0,0,0.12)}.bs-sheet[data-mode="right"]{top:0;bottom:0;right:0;height:100%;border-top-left-radius:var(--bs-radius);border-bottom-left-radius:var(--bs-radius);border-left:1px solid var(--bs-surface-border);--bs-shadow-auto:-3px 0 12px rgba(0,0,0,0.12)}.bs-sheet[data-mode="left"] .bs-content{padding-right:36px}.bs-sheet[data-mode="right"] .bs-content{padding-left:36px}.bs-handle{flex-shrink:0;min-height:44px;display:flex;flex-direction:column;justify-content:center;padding:8px 16px 4px;cursor:grab;touch-action:none;user-select:none;background:var(--bs-surface);-webkit-tap-highlight-color:transparent;outline:none;position:relative}.bs-handle:active{cursor:grabbing}.bs-handle:focus-visible{outline:2px solid currentColor;outline-offset:-2px;border-radius:var(--bs-radius)}.bs-handle::before{content:"";display:block;width:var(--bs-handle-width);height:var(--bs-handle-height);background:var(--bs-handle-color);border-radius:calc(var(--bs-handle-height) / 2);margin:0 auto 8px;transition:width 0.15s ease,background 0.15s ease,transform 0.15s ease;pointer-events:none}.bs-sheet[data-mode="left"] .bs-handle,.bs-sheet[data-mode="right"] .bs-handle{position:absolute;top:0;bottom:0;width:28px;min-width:0;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0;background:var(--bs-surface);z-index:1;overflow:hidden}.bs-sheet[data-mode="left"] .bs-handle>*,.bs-sheet[data-mode="right"] .bs-handle>*{display:none}.bs-sheet[data-mode="left"] .bs-handle{right:0;border-left:1px solid var(--bs-surface-border)}.bs-sheet[data-mode="right"] .bs-handle{left:0;border-right:1px solid var(--bs-surface-border)}.bs-sheet[data-mode="left"] .bs-handle::before,.bs-sheet[data-mode="right"] .bs-handle::before{width:var(--bs-handle-height);height:var(--bs-handle-width);margin:0}.bs-sheet:hover .bs-handle::before{background:color-mix(in oklab,var(--bs-handle-color),#000 15%)}.bs-handle:active::before{background:color-mix(in oklab,var(--bs-handle-color),#000 35%);transform:scaleX(1.15)}.bs-handle.bs-handle--ticks::after{content:"";display:block;position:absolute;left:50%;bottom:6px;transform:translateX(-50%);width:calc(var(--bs-handle-width) * 2);height:3px;background-image:radial-gradient( circle,var(--bs-handle-color) 0 1.5px,transparent 1.6px );background-size:8px 3px;background-repeat:repeat-x;background-position:center;opacity:0;transition:opacity 0.18s ease-out;pointer-events:none}.bs-root[data-dragging="true"] .bs-handle.bs-handle--ticks::after{opacity:0.4}.bs-sheet[data-mode="top"] .bs-handle.bs-handle--ticks::after{bottom:auto;top:6px}.bs-sheet[data-mode="left"] .bs-handle.bs-handle--ticks::after,.bs-sheet[data-mode="right"] .bs-handle.bs-handle--ticks::after{left:50%;top:50%;bottom:auto;transform:translate(-50%,-50%);width:3px;height:calc(var(--bs-handle-width) * 2);background-size:3px 8px;background-repeat:repeat-y;margin-left:calc(var(--bs-handle-height) + 6px)}.bs-sheet[data-mode="right"] .bs-handle.bs-handle--ticks::after{margin-left:calc(-1 * (var(--bs-handle-height) + 6px))}.bs-content{flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;user-select:text;padding:0 16px 16px}.bs-screen{position:absolute;inset:0;pointer-events:none;z-index:var(--bs-z-scrim)}.bs-scrim-overlay{z-index:calc(var(--bs-z-sheet) + 1);pointer-events:none}.bs-scrim{background:var(--bs-scrim-color);transition:opacity 0.15s linear;will-change:opacity}.bs-root[data-dragging="true"] .bs-scrim,.bs-root[data-animating="true"] .bs-scrim{transition:none}.bs-scrim{backdrop-filter:blur(var(--bs-scrim-blur));-webkit-backdrop-filter:blur(var(--bs-scrim-blur))}.bs-button-slot{position:absolute;z-index:calc(var(--bs-z-sheet) + 1);pointer-events:auto}.bs-button-slot[data-side="left"]{left:16px;bottom:calc(var(--bs-size,0px) + 12px);transition:bottom 0.15s ease}.bs-button-slot[data-side="right"]{right:16px;bottom:calc(var(--bs-size,0px) + 12px);transition:bottom 0.15s ease}@media (prefers-reduced-motion:reduce){.bs-backdrop,.bs-handle::before,.bs-button-slot[data-side="left"],.bs-button-slot[data-side="right"]{transition:none !important}}@media (prefers-color-scheme:dark){:where(.bs-root){--bs-surface:#1a1a1d;--bs-surface-border:rgba(255,255,255,0.08);--bs-handle-color:rgba(255,255,255,0.45);--bs-shadow-auto:0 -3px 16px rgba(0,0,0,0.5)}}.bs-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}:where(.ovl-root){--ovl-surface:var(--bs-surface,#ffffff);--ovl-surface-border:var(--bs-surface-border,rgba(0,0,0,0.08));--ovl-shadow:0 -8px 40px rgba(0,0,0,0.35);--ovl-radius:var(--bs-radius,20px);--ovl-backdrop-color:rgba(0,0,0,0.5);--ovl-z-backdrop:200;--ovl-z-panel:201}.ovl-backdrop{position:fixed;inset:0;background:var(--ovl-backdrop-color);opacity:0;pointer-events:none;z-index:var(--ovl-z-backdrop)}.ovl-panel{position:fixed;background:var(--ovl-surface);box-shadow:var(--ovl-shadow);z-index:var(--ovl-z-panel);-webkit-tap-highlight-color:transparent;outline:none;transition:transform 0.32s cubic-bezier(0.29,1.24,0.84,0.99),opacity 0.32s ease-out}.ovl-panel[data-state="open"]{transform:translate3d(0,0,0);opacity:1}@starting-style{.ovl-panel[data-edge="bottom"][data-state="open"]{transform:translate3d(0,100%,0);opacity:0}.ovl-panel[data-edge="top"][data-state="open"]{transform:translate3d(0,-100%,0);opacity:0}.ovl-panel[data-edge="left"][data-state="open"]{transform:translate3d(-100%,0,0);opacity:0}.ovl-panel[data-edge="right"][data-state="open"]{transform:translate3d(100%,0,0);opacity:0}}.ovl-panel[data-edge="bottom"]{left:0;right:0;bottom:0;width:100%;border-top-left-radius:var(--ovl-radius);border-top-right-radius:var(--ovl-radius);border-top:1px solid var(--ovl-surface-border);padding-bottom:env(safe-area-inset-bottom)}.ovl-panel[data-edge="top"]{left:0;right:0;top:0;width:100%;border-bottom-left-radius:var(--ovl-radius);border-bottom-right-radius:var(--ovl-radius);border-bottom:1px solid var(--ovl-surface-border);padding-top:env(safe-area-inset-top)}.ovl-panel[data-edge="left"]{top:0;bottom:0;left:0;height:100%;border-top-right-radius:var(--ovl-radius);border-bottom-right-radius:var(--ovl-radius);border-right:1px solid var(--ovl-surface-border)}.ovl-panel[data-edge="right"]{top:0;bottom:0;right:0;height:100%;border-top-left-radius:var(--ovl-radius);border-bottom-left-radius:var(--ovl-radius);border-left:1px solid var(--ovl-surface-border)}@media (prefers-reduced-motion:reduce){.ovl-panel,.ovl-backdrop{transition:none !important}}`;
3166
3328
  }
3167
3329
  });
3168
3330
 
@@ -3172,7 +3334,7 @@ __export(BottomSheetElement_exports, {
3172
3334
  BottomSheetElement: () => exports.BottomSheetElement,
3173
3335
  defineBottomSheet: () => exports.defineBottomSheet
3174
3336
  });
3175
- var ATTR_SNAP_POINTS, ATTR_ALLOWED, ATTR_INITIAL, ATTR_MODE, ATTR_BACKDROP, ATTR_STYLESHEET, ATTR_ANIMATION, ATTR_FOCUS_TRAP, ATTR_CLOSE_ON_ESCAPE, ATTR_LOCK_BODY_SCROLL, isValidSnapPoint, DEFAULT_SNAP_POINTS, parseSnapPoints, VALID_MODES, VALID_ANIMATIONS, parseMode, parseAnimation, parseList, buildSlot, buildShadowTree; exports.BottomSheetElement = void 0; var registered; exports.defineBottomSheet = void 0;
3337
+ var ATTR_SNAP_POINTS, ATTR_ALLOWED, ATTR_INITIAL, ATTR_MODE, ATTR_BACKDROP, ATTR_STYLESHEET, ATTR_ANIMATION, ATTR_FOCUS_TRAP, ATTR_CLOSE_ON_ESCAPE, ATTR_LOCK_BODY_SCROLL, ATTR_SHEET_LABEL, ATTR_STACK_EFFECT, isValidSnapPoint, DEFAULT_SNAP_POINTS, parseSnapPoints, VALID_MODES, VALID_ANIMATIONS, parseMode, parseAnimation, parseList, ANCHOR_POSITIONS, ANCHOR_ANIMATIONS, parseAnchorOptions, buildSlot, buildShadowTree, BaseHTMLElement; exports.BottomSheetElement = void 0; exports.defineBottomSheet = void 0;
3176
3338
  var init_BottomSheetElement = __esm({
3177
3339
  "src/web-component/BottomSheetElement.ts"() {
3178
3340
  init_BottomSheetEngine();
@@ -3187,6 +3349,8 @@ var init_BottomSheetElement = __esm({
3187
3349
  ATTR_FOCUS_TRAP = "focus-trap";
3188
3350
  ATTR_CLOSE_ON_ESCAPE = "close-on-escape";
3189
3351
  ATTR_LOCK_BODY_SCROLL = "lock-body-scroll";
3352
+ ATTR_SHEET_LABEL = "sheet-label";
3353
+ ATTR_STACK_EFFECT = "stack-effect";
3190
3354
  isValidSnapPoint = (p) => !!p && typeof p === "object" && typeof p.id === "string" && (typeof p.size === "number" || typeof p.size === "string");
3191
3355
  DEFAULT_SNAP_POINTS = [{ id: "full", size: "full" }];
3192
3356
  parseSnapPoints = (raw) => {
@@ -3216,6 +3380,44 @@ var init_BottomSheetElement = __esm({
3216
3380
  parseMode = (raw) => raw && VALID_MODES.includes(raw) ? raw : "bottom";
3217
3381
  parseAnimation = (raw) => raw && VALID_ANIMATIONS.includes(raw) ? raw : void 0;
3218
3382
  parseList = (raw) => raw ? raw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
3383
+ ANCHOR_POSITIONS = [
3384
+ "top-left",
3385
+ "top-center",
3386
+ "top-right",
3387
+ "center-left",
3388
+ "center",
3389
+ "center-right",
3390
+ "bottom-left",
3391
+ "bottom-center",
3392
+ "bottom-right",
3393
+ "sheet-top-left",
3394
+ "sheet-top-center",
3395
+ "sheet-top-right",
3396
+ "dock-bottom",
3397
+ "dock-top"
3398
+ ];
3399
+ ANCHOR_ANIMATIONS = [
3400
+ "fade",
3401
+ "scale",
3402
+ "slide",
3403
+ "pop",
3404
+ "none"
3405
+ ];
3406
+ parseAnchorOptions = (el) => {
3407
+ const position = el.getAttribute("data-position");
3408
+ const animation = el.getAttribute("data-animation");
3409
+ const showOn = parseList(el.getAttribute("data-show-on"));
3410
+ const fadeRangeRaw = parseList(el.getAttribute("data-fade-range"));
3411
+ const fadeRange = fadeRangeRaw?.length === 2 && fadeRangeRaw.every((v) => Number.isFinite(Number(v))) ? [Number(fadeRangeRaw[0]), Number(fadeRangeRaw[1])] : void 0;
3412
+ return {
3413
+ position: position && ANCHOR_POSITIONS.includes(position) ? position : void 0,
3414
+ inset: el.getAttribute("data-inset") ?? void 0,
3415
+ showOn,
3416
+ fadeRange,
3417
+ interactive: el.getAttribute("data-interactive") !== "false",
3418
+ animation: animation && ANCHOR_ANIMATIONS.includes(animation) ? animation : void 0
3419
+ };
3420
+ };
3219
3421
  buildSlot = (name) => {
3220
3422
  const slot = document.createElement("slot");
3221
3423
  slot.name = name;
@@ -3285,7 +3487,9 @@ var init_BottomSheetElement = __esm({
3285
3487
  }
3286
3488
  };
3287
3489
  };
3288
- exports.BottomSheetElement = class extends HTMLElement {
3490
+ BaseHTMLElement = typeof HTMLElement !== "undefined" ? HTMLElement : class {
3491
+ };
3492
+ exports.BottomSheetElement = class extends BaseHTMLElement {
3289
3493
  constructor() {
3290
3494
  super();
3291
3495
  __publicField(this, "engine", null);
@@ -3293,17 +3497,12 @@ var init_BottomSheetElement = __esm({
3293
3497
  __publicField(this, "offHandlers", []);
3294
3498
  __publicField(this, "refs");
3295
3499
  __publicField(this, "backdropClickHandler", null);
3500
+ __publicField(this, "stylesheetLinked", false);
3501
+ __publicField(this, "declarativeAnchors", []);
3296
3502
  this.root = this.attachShadow({ mode: "open" });
3297
3503
  const baseStyle = document.createElement("style");
3298
3504
  baseStyle.textContent = baseStyles;
3299
3505
  this.root.appendChild(baseStyle);
3300
- const stylesheet = this.getAttribute(ATTR_STYLESHEET);
3301
- if (stylesheet) {
3302
- const link = document.createElement("link");
3303
- link.rel = "stylesheet";
3304
- link.href = stylesheet;
3305
- this.root.appendChild(link);
3306
- }
3307
3506
  const inlineStyle = document.createElement("style");
3308
3507
  inlineStyle.textContent = [
3309
3508
  ":host { display: contents; }",
@@ -3331,10 +3530,22 @@ var init_BottomSheetElement = __esm({
3331
3530
  ATTR_ANIMATION,
3332
3531
  ATTR_FOCUS_TRAP,
3333
3532
  ATTR_CLOSE_ON_ESCAPE,
3334
- ATTR_LOCK_BODY_SCROLL
3533
+ ATTR_LOCK_BODY_SCROLL,
3534
+ ATTR_SHEET_LABEL,
3535
+ ATTR_STACK_EFFECT
3335
3536
  ];
3336
3537
  }
3337
3538
  connectedCallback() {
3539
+ if (!this.stylesheetLinked) {
3540
+ this.stylesheetLinked = true;
3541
+ const stylesheet = this.getAttribute(ATTR_STYLESHEET);
3542
+ if (stylesheet) {
3543
+ const link = document.createElement("link");
3544
+ link.rel = "stylesheet";
3545
+ link.href = stylesheet;
3546
+ this.root.appendChild(link);
3547
+ }
3548
+ }
3338
3549
  this.initEngine();
3339
3550
  }
3340
3551
  disconnectedCallback() {
@@ -3345,6 +3556,9 @@ var init_BottomSheetElement = __esm({
3345
3556
  this.teardownEngine();
3346
3557
  this.initEngine();
3347
3558
  }
3559
+ getEngine() {
3560
+ return this.engine;
3561
+ }
3348
3562
  teardownEngine() {
3349
3563
  this.offHandlers.forEach((off) => off());
3350
3564
  this.offHandlers = [];
@@ -3354,6 +3568,10 @@ var init_BottomSheetElement = __esm({
3354
3568
  }
3355
3569
  this.engine?.destroy();
3356
3570
  this.engine = null;
3571
+ for (const el of this.declarativeAnchors) {
3572
+ this.appendChild(el);
3573
+ }
3574
+ this.declarativeAnchors = [];
3357
3575
  }
3358
3576
  initEngine() {
3359
3577
  const { sheet, handle, content, backdrop, announcer } = this.refs;
@@ -3365,6 +3583,10 @@ var init_BottomSheetElement = __esm({
3365
3583
  } else {
3366
3584
  sheet.removeAttribute("aria-modal");
3367
3585
  }
3586
+ sheet.setAttribute(
3587
+ "aria-label",
3588
+ this.getAttribute(ATTR_SHEET_LABEL) ?? "Bottom sheet"
3589
+ );
3368
3590
  const showBackdrop = this.getAttribute(ATTR_BACKDROP) !== "false";
3369
3591
  backdrop.style.display = showBackdrop ? "" : "none";
3370
3592
  if (showBackdrop) {
@@ -3376,7 +3598,7 @@ var init_BottomSheetElement = __esm({
3376
3598
  handle,
3377
3599
  scrollContainer: content,
3378
3600
  backdrop: showBackdrop ? backdrop : void 0,
3379
- screenComponent: this.refs.screen,
3601
+ scrim: this.refs.screen,
3380
3602
  mode,
3381
3603
  snapPoints: parseSnapPoints(this.getAttribute(ATTR_SNAP_POINTS)),
3382
3604
  allowed: parseList(this.getAttribute(ATTR_ALLOWED)),
@@ -3384,11 +3606,20 @@ var init_BottomSheetElement = __esm({
3384
3606
  animation: parseAnimation(this.getAttribute(ATTR_ANIMATION)),
3385
3607
  focusTrap,
3386
3608
  closeOnEscape: this.getAttribute(ATTR_CLOSE_ON_ESCAPE) !== "false",
3387
- lockBodyScroll: this.getAttribute(ATTR_LOCK_BODY_SCROLL) !== "false"
3609
+ lockBodyScroll: this.getAttribute(ATTR_LOCK_BODY_SCROLL) !== "false",
3610
+ stackEffect: this.getAttribute(ATTR_STACK_EFFECT) === "true"
3388
3611
  };
3389
3612
  this.engine = new BottomSheetEngine(opts);
3613
+ sheet.setAttribute("data-active", this.engine.state.activeId);
3614
+ for (const el of Array.from(
3615
+ this.querySelectorAll('[slot="anchor"]')
3616
+ )) {
3617
+ this.declarativeAnchors.push(el);
3618
+ this.engine.addAnchor({ ...parseAnchorOptions(el), element: el });
3619
+ }
3390
3620
  this.offHandlers = [
3391
3621
  this.engine.on("snap", (payload) => {
3622
+ sheet.setAttribute("data-active", payload.id);
3392
3623
  announcer.textContent = payload.id;
3393
3624
  this.dispatchEvent(new CustomEvent("snap", { detail: payload }));
3394
3625
  }),
@@ -3418,15 +3649,22 @@ var init_BottomSheetElement = __esm({
3418
3649
  setAllowed(ids, snap) {
3419
3650
  this.engine?.setAllowed(ids, snap);
3420
3651
  }
3652
+ addAnchor(opts) {
3653
+ return this.engine?.addAnchor(opts) ?? (() => {
3654
+ });
3655
+ }
3656
+ setScrimStages(opts) {
3657
+ return this.engine?.setScrimStages(opts) ?? (() => {
3658
+ });
3659
+ }
3421
3660
  get sheetState() {
3422
3661
  return this.engine?.state ?? null;
3423
3662
  }
3424
3663
  };
3425
- registered = false;
3426
3664
  exports.defineBottomSheet = (tag = "bottom-sheet") => {
3427
- if (registered || customElements.get(tag)) return;
3428
- customElements.define(tag, exports.BottomSheetElement);
3429
- registered = true;
3665
+ if (typeof customElements === "undefined" || customElements.get(tag)) return;
3666
+ customElements.define(tag, class extends exports.BottomSheetElement {
3667
+ });
3430
3668
  };
3431
3669
  }
3432
3670
  });