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