@surdeddd/bottom-sheet 0.1.0 → 0.2.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 (50) hide show
  1. package/README.md +6 -0
  2. package/dist/BottomSheetEngine-CIT9Htti.d.cts +219 -0
  3. package/dist/BottomSheetEngine-Df9MwBbm.d.ts +219 -0
  4. package/dist/element.cjs +95 -440
  5. package/dist/element.d.cts +3 -56
  6. package/dist/element.d.ts +3 -56
  7. package/dist/element.iife.global.js +2 -2
  8. package/dist/element.js +95 -440
  9. package/dist/index.cjs +84 -494
  10. package/dist/index.d.cts +3 -84
  11. package/dist/index.d.ts +3 -84
  12. package/dist/index.js +84 -494
  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 +0 -87
  18. package/dist/overlay.d.cts +1 -137
  19. package/dist/overlay.d.ts +1 -137
  20. package/dist/overlay.js +0 -87
  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 +87 -439
  25. package/dist/qwik.d.cts +3 -24
  26. package/dist/qwik.d.ts +3 -24
  27. package/dist/qwik.js +87 -439
  28. package/dist/react.cjs +91 -496
  29. package/dist/react.d.cts +16 -158
  30. package/dist/react.d.ts +16 -158
  31. package/dist/react.js +91 -496
  32. package/dist/solid.cjs +87 -439
  33. package/dist/solid.d.cts +4 -31
  34. package/dist/solid.d.ts +4 -31
  35. package/dist/solid.js +87 -439
  36. package/dist/styles.css +27 -102
  37. package/dist/svelte.cjs +139 -462
  38. package/dist/svelte.d.ts +9 -2
  39. package/dist/svelte.js +139 -462
  40. package/dist/themes/ios.css +0 -3
  41. package/dist/themes/material.css +0 -3
  42. package/dist/themes/vercel.css +0 -3
  43. package/dist/{transform-Brkf-wzo.d.cts → transform-jVfAikE9.d.cts} +0 -1
  44. package/dist/{transform-Brkf-wzo.d.ts → transform-jVfAikE9.d.ts} +0 -1
  45. package/dist/vue.cjs +104 -446
  46. package/dist/vue.d.ts +7 -1
  47. package/dist/vue.js +105 -447
  48. package/package.json +3 -3
  49. package/dist/BottomSheetEngine-CvSVbo6c.d.cts +0 -704
  50. package/dist/BottomSheetEngine-DbO8rW_3.d.ts +0 -704
package/dist/index.cjs CHANGED
@@ -236,11 +236,6 @@ var SnapResolver = class {
236
236
  this.allowedIds = allowed ?? raw.map((p) => p.id);
237
237
  this.recompute();
238
238
  }
239
- /**
240
- * Re-resolve raw → resolved against the current viewport / fit-measure,
241
- * recompute maxAxisSize, invalidate range cache, notify engine. Called on
242
- * resize, orientationchange, visualViewport changes, and snap-list swaps.
243
- */
244
239
  recompute() {
245
240
  this.resolved = resolveSnapList(this.raw, this.mode, this.measureFit);
246
241
  this.maxAxisSize = this.resolved.reduce(
@@ -250,15 +245,10 @@ var SnapResolver = class {
250
245
  this.rangeCache = null;
251
246
  this.onMaxAxisSizeChange?.(this.maxAxisSize);
252
247
  }
253
- /** Replace the raw snap list and recompute. Engine calls on `setSnapPoints`. */
254
248
  setRaw(raw) {
255
249
  this.raw = raw;
256
250
  this.recompute();
257
251
  }
258
- /**
259
- * Replace the allow-list. Range cache depends on `(resolvedSnaps ×
260
- * allowedIds)` so flipping ids alone invalidates without a full recompute.
261
- */
262
252
  setAllowedIds(ids) {
263
253
  this.allowedIds = ids;
264
254
  this.rangeCache = null;
@@ -266,11 +256,6 @@ var SnapResolver = class {
266
256
  findById(id) {
267
257
  return findById(id, this.resolved);
268
258
  }
269
- /**
270
- * Pure drag-settle target resolution. Engine passes the gesture-specific
271
- * fields (delta, velocity, pointerKind, flickVelocity, dragThreshold) plus
272
- * `size`/`activeId` since those are still engine-owned.
273
- */
274
259
  findDragSettleTarget(args) {
275
260
  return findDragSettleTarget({
276
261
  ...args,
@@ -279,18 +264,9 @@ var SnapResolver = class {
279
264
  maxAxisSize: this.maxAxisSize
280
265
  });
281
266
  }
282
- /**
283
- * Memoised allowed-range query. Twice-per-drag-frame (gesture onMove +
284
- * applySize→computeProgress); cache collapses 2× iteration of resolvedSnaps
285
- * to one recompute per geometry/allow-list change.
286
- */
287
267
  getAllowedRange() {
288
268
  return this.rangeCache ?? (this.rangeCache = allowedRange(this.resolved, this.allowedIds));
289
269
  }
290
- /**
291
- * Read-only view of resolved snaps. Returned as `readonly` to discourage
292
- * accidental mutation by feature install sites that close over this list.
293
- */
294
270
  getResolvedSnaps() {
295
271
  return this.resolved;
296
272
  }
@@ -300,18 +276,6 @@ var SnapResolver = class {
300
276
  getMaxAxisSize() {
301
277
  return this.maxAxisSize;
302
278
  }
303
- /**
304
- * External clamp override. The resize-observer feature compares
305
- * `getMaxAxisSize()` against the live viewport and shrinks if the largest
306
- * resolved snap exceeds it (mobile address-bar collapse / soft-keyboard
307
- * window resize). The next `recompute()` overwrites this clamp from the
308
- * resolved-snap reduction.
309
- *
310
- * Does NOT invalidate the range cache — viewport clamping doesn't change
311
- * `(resolvedSnaps × allowedIds) → range`. DOES fire `onMaxAxisSizeChange`
312
- * so the resolver owns the layout-axis style write end-to-end (symmetric
313
- * with `recompute()` — feature call sites no longer duplicate the write).
314
- */
315
279
  setMaxAxisSize(n) {
316
280
  this.maxAxisSize = n;
317
281
  this.onMaxAxisSizeChange?.(n);
@@ -686,73 +650,49 @@ var WriteSentinel = class {
686
650
  constructor() {
687
651
  __publicField(this, "last", -1);
688
652
  }
689
- /**
690
- * Returns `true` (and updates the cached value) if `next` differs from the
691
- * cached `last` by more than `epsilon`. Returns `false` (and leaves the
692
- * cache untouched) when the delta is within epsilon — caller skips the
693
- * CSSOM write.
694
- */
695
653
  shouldWrite(next, epsilon) {
696
654
  if (Math.abs(next - this.last) <= epsilon) return false;
697
655
  this.last = next;
698
656
  return true;
699
657
  }
700
- /**
701
- * Force the next `shouldWrite` to pass. Resets the cache to `-1` so any
702
- * non-negative `next` exceeds the epsilon comparison. Called from setters
703
- * that mutate state under the cache (range/mode/enabled flips that change
704
- * the progress→opacity mapping but don't move `progress` itself).
705
- */
706
658
  invalidate() {
707
659
  this.last = -1;
708
660
  }
709
- /**
710
- * Record `value` as the last-written without performing a write. Used
711
- * when a setter writes a literal value through a different path (e.g.
712
- * `setScrimMode("off")` writes `opacity: "0"` inline) and wants future
713
- * `shouldWrite` calls to short-circuit against that known cached state.
714
- */
715
661
  setLastWritten(value) {
716
662
  this.last = value;
717
663
  }
718
- /** Read-only accessor for diagnostic / dedup-aware peek. */
719
664
  get value() {
720
665
  return this.last;
721
666
  }
722
667
  };
723
668
 
669
+ // src/core/primitives/devWarn.ts
670
+ function devWarn(...args) {
671
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && typeof console !== "undefined") {
672
+ console.warn(...args);
673
+ }
674
+ }
675
+
724
676
  // src/core/controllers/scrim-controller.ts
677
+ var VALID_CSS_LENGTH = /^-?\d+(?:\.\d+)?(?:px|em|rem|%)$/;
725
678
  var ScrimController = class {
726
679
  constructor(deps, opts) {
727
- // ── DOM refs (mirror engine's; cached for hot path) ─────────────────
728
680
  __publicField(this, "screenComponent");
729
681
  __publicField(this, "backdrop");
730
682
  __publicField(this, "mode");
731
- // ── Engine callbacks ────────────────────────────────────────────────
732
683
  __publicField(this, "isDestroyed");
733
684
  __publicField(this, "isTopSheet");
734
685
  __publicField(this, "getAllowedIds");
735
686
  __publicField(this, "getResolvedSnaps");
736
687
  __publicField(this, "snapToFn");
737
- // ── Mode + state ────────────────────────────────────────────────────
738
688
  __publicField(this, "scrimMode");
739
689
  __publicField(this, "backdropRange");
740
690
  __publicField(this, "screenRange");
741
- // setScrimEnabled toggle — gated through `scrimEnabled` rather than nuking
742
- // ranges so re-enable can restore prior values without a remount. Public
743
- // so tests / introspection can read it without an `as any` cast.
744
691
  __publicField(this, "scrimEnabled", true);
745
692
  __publicField(this, "savedScrimRanges", null);
746
- // ── Tap-to-close ────────────────────────────────────────────────────
747
693
  __publicField(this, "scrimTapToCloseEnabled", false);
748
694
  __publicField(this, "detachScrimTap", null);
749
- // ── Scrim overlay slot (positioned floating element) ────────────────
750
695
  __publicField(this, "scrimOverlayTeardown", null);
751
- // ── Hot-path opacity dedup caches (per-frame writes) ────────────────
752
- // Numeric sentinels migrated to WriteSentinel primitive — single
753
- // invalidation surface, shared with the engine's size/progress sentinels.
754
- // The two string-typed sentinels (`Pointer`, `Display`) keep their inline
755
- // pattern: equality on string union, no epsilon, different reset semantics.
756
696
  __publicField(this, "backdropOpacitySentinel", new WriteSentinel());
757
697
  __publicField(this, "screenOpacitySentinel", new WriteSentinel());
758
698
  __publicField(this, "lastBackdropPointer", "");
@@ -787,41 +727,15 @@ var ScrimController = class {
787
727
  }
788
728
  }
789
729
  }
790
- /**
791
- * Invalidate the opacity dedup caches. Called from any setter that mutates
792
- * range/mode/enabled — opacity is a pure function of (progress, range,
793
- * enabled), so a config change makes the cached values stale even if
794
- * `progress` itself didn't move. Next `applyOpacity(progress, false)` call
795
- * sees the -1 sentinel and re-runs the math instead of short-circuiting.
796
- *
797
- * Public for the engine: `setSnapPoints` mutates resolved geometry which
798
- * shifts `progress→opacity` mapping under the cache; engine calls this to
799
- * keep the next render frame correct on a static sheet.
800
- * @internal
801
- */
802
730
  invalidateOpacityCache() {
803
731
  this.backdropOpacitySentinel.invalidate();
804
732
  this.screenOpacitySentinel.invalidate();
805
733
  }
806
- /** Engine calls this when its `attach()` runs — installs scrim-tap-click if requested. */
807
734
  attach() {
808
735
  if (this.scrimTapToCloseEnabled) {
809
736
  this.setScrimTapToClose(true);
810
737
  }
811
738
  }
812
- // ── Hot-path: opacity computation called per-frame from engine.applySize ──
813
- /**
814
- * Per-frame opacity writer. Computes backdrop + screen opacity from the
815
- * sheet's progress (0..1 within allowed range), gated by sub-pixel dedup
816
- * caches so steady-state frames don't trigger style recalc.
817
- *
818
- * `progressChanged` is the engine's already-computed dedup gate — when
819
- * `false`, opacity is by construction a pure function of `progress` so
820
- * cached values are still valid; we short-circuit the Math.min/max +
821
- * division work entirely. Setters that mutate ranges/mode/enabled
822
- * invalidate the cache by writing -1 sentinels so the next call re-runs
823
- * the math.
824
- */
825
739
  applyOpacity(progress, progressChanged) {
826
740
  if (!progressChanged && this.backdropOpacitySentinel.value !== -1 && this.screenOpacitySentinel.value !== -1) {
827
741
  return;
@@ -867,80 +781,30 @@ var ScrimController = class {
867
781
  }
868
782
  }
869
783
  }
870
- // ── Public setters (engine forwards these 1:1) ─────────────────────
871
- /**
872
- * Replace the [start, end] progress window in which the backdrop fades 0→1.
873
- * @param range - Tuple of two progress values in 0..1 (start <= end).
874
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
875
- * @example
876
- * // Backdrop only starts darkening after the sheet crosses 30% open.
877
- * engine.setBackdropRange([0.3, 1]);
878
- */
879
784
  setBackdropRange(range, applySize) {
880
785
  if (this.isDestroyed()) return;
881
786
  this.backdropRange = range;
882
787
  this.invalidateOpacityCache();
883
788
  applySize();
884
789
  }
885
- /**
886
- * Replace the [start, end] progress window in which the screen scrim fades 0→1.
887
- * @param range - Tuple of two progress values in 0..1 (start <= end).
888
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
889
- * @example
890
- * // Scrim is fully opaque only when the sheet is past 50% — softer feel.
891
- * engine.setScreenRange([0, 0.5]);
892
- */
893
790
  setScreenRange(range, applySize) {
894
791
  if (this.isDestroyed()) return;
895
792
  this.screenRange = range;
896
793
  this.invalidateOpacityCache();
897
794
  applySize();
898
795
  }
899
- /**
900
- * Set the scrim background color. Pass `null`/`undefined` to clear the
901
- * inline write and let consumer CSS take over.
902
- * @param color - Any valid CSS color string, or `null`/`undefined` to clear.
903
- * @example
904
- * engine.setScrimColor("rgba(0, 0, 0, 0.6)");
905
- * engine.setScrimColor(null); // hand back to consumer CSS
906
- */
907
796
  setScrimColor(color) {
908
797
  if (this.isDestroyed()) return;
909
798
  this.applyScrimStyles({ color: color === void 0 ? null : color });
910
799
  }
911
- /**
912
- * Set the scrim backdrop-filter blur radius. Pass `null`/`undefined` to clear.
913
- * @param blur - CSS length (e.g. `"8px"`) consumed as `blur(<value>)`, or
914
- * `null`/`undefined` to remove the filter.
915
- * @example
916
- * engine.setScrimBlur("12px"); // frosted-glass effect
917
- * engine.setScrimBlur(null); // disable blur
918
- */
919
800
  setScrimBlur(blur) {
920
801
  if (this.isDestroyed()) return;
921
802
  this.applyScrimStyles({ blur: blur === void 0 ? null : blur });
922
803
  }
923
- /**
924
- * Toggle whether the scrim layer captures pointer events.
925
- * @param interactive - `true` → `pointer-events: auto`; `false` → `none`.
926
- * @example
927
- * engine.setScrimInteractive(true); // scrim swallows clicks
928
- * engine.setScrimInteractive(false); // clicks pass through to content below
929
- */
930
804
  setScrimInteractive(interactive) {
931
805
  if (this.isDestroyed()) return;
932
806
  this.applyScrimStyles({ interactive });
933
807
  }
934
- /**
935
- * Switch scrim positioning at runtime.
936
- * @param mode - `"full"` (consumer CSS), `"above-sheet"` (engine-pinned inset
937
- * so only the area NOT covered by the sheet is dimmed), or
938
- * `"off"` (display:none + opacity:0 — no remount needed).
939
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
940
- * @example
941
- * engine.setScrimMode("above-sheet"); // dim only the area above the sheet
942
- * engine.setScrimMode("off"); // disable scrim layer
943
- */
944
808
  setScrimMode(mode, applySize) {
945
809
  if (this.isDestroyed()) return;
946
810
  if (mode === this.scrimMode) return;
@@ -970,15 +834,6 @@ var ScrimController = class {
970
834
  }
971
835
  applySize();
972
836
  }
973
- /**
974
- * Enable or disable click-on-scrim → snap to first non-zero allowed snap.
975
- * Auto-promotes `pointer-events: auto` when scrim is non-interactive so the
976
- * click listener actually fires.
977
- * @param enabled - `true` installs the click listener; `false` removes it.
978
- * @example
979
- * engine.setScrimTapToClose(true); // tap outside sheet collapses it
980
- * engine.setScrimTapToClose(false); // require explicit close button
981
- */
982
837
  setScrimTapToClose(enabled) {
983
838
  if (this.isDestroyed()) return;
984
839
  if (!this.screenComponent) return;
@@ -1008,16 +863,6 @@ var ScrimController = class {
1008
863
  this.scrimTapToCloseEnabled = false;
1009
864
  }
1010
865
  }
1011
- /**
1012
- * Toggle the scrim layer without remounting. Disabling stashes current
1013
- * `screenRange`/`backdropRange` and degenerates them to `[1, 1]` (opacity
1014
- * formula → 0 across the whole range); re-enabling restores the saved values.
1015
- * @param enabled - `false` hides scrim, `true` restores prior ranges.
1016
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
1017
- * @example
1018
- * engine.setScrimEnabled(false); // hide scrim, ranges remembered
1019
- * engine.setScrimEnabled(true); // restore prior ranges (no remount)
1020
- */
1021
866
  setScrimEnabled(enabled, applySize) {
1022
867
  if (this.isDestroyed()) return;
1023
868
  if (enabled === this.scrimEnabled) return;
@@ -1036,20 +881,6 @@ var ScrimController = class {
1036
881
  }
1037
882
  applySize();
1038
883
  }
1039
- /**
1040
- * Batch-update multiple scrim fields with a single applySize() at the end.
1041
- * If `preset` is set, it seeds color/blur/interactive/range first; explicit
1042
- * fields override.
1043
- * @param opts - Partial scrim config: preset, mode, enabled, tapToClose,
1044
- * color, blur, interactive, range.
1045
- * @param applySize - Engine-supplied recompute hook; runs ONCE per call.
1046
- * @example
1047
- * engine.setScrim({
1048
- * preset: "frosted",
1049
- * mode: "above-sheet",
1050
- * tapToClose: true,
1051
- * });
1052
- */
1053
884
  setScrim(opts, applySize) {
1054
885
  if (this.isDestroyed()) return;
1055
886
  if (opts.preset) {
@@ -1072,27 +903,18 @@ var ScrimController = class {
1072
903
  interactive: opts.interactive
1073
904
  });
1074
905
  if (opts.range !== void 0) {
1075
- this.screenRange = opts.range;
906
+ if (!this.scrimEnabled && this.savedScrimRanges) {
907
+ this.savedScrimRanges = {
908
+ screen: opts.range,
909
+ backdrop: this.savedScrimRanges.backdrop
910
+ };
911
+ } else {
912
+ this.screenRange = opts.range;
913
+ }
1076
914
  }
1077
915
  this.invalidateOpacityCache();
1078
916
  applySize();
1079
917
  }
1080
- /**
1081
- * Inject a positioned floating element into the scrim layer (e.g. a close
1082
- * button anchored top-right). Replaces any prior overlay so repeated calls
1083
- * (reactive effects re-running) don't accumulate wrappers.
1084
- * @param opts - Children element, optional `inset` (default `"16px"`),
1085
- * `position` (9-way grid, default `"top-right"`), and
1086
- * `interactive` flag (default `true`).
1087
- * @returns Teardown function that removes the wrapper from the DOM.
1088
- * @example
1089
- * const teardown = engine.setScrimOverlay({
1090
- * children: closeBtn,
1091
- * position: "top-right",
1092
- * inset: "20px",
1093
- * });
1094
- * // later: teardown(); // remove the floating button
1095
- */
1096
918
  setScrimOverlay(opts) {
1097
919
  if (this.isDestroyed()) return () => {
1098
920
  };
@@ -1100,7 +922,18 @@ var ScrimController = class {
1100
922
  };
1101
923
  this.scrimOverlayTeardown?.();
1102
924
  const screen = this.screenComponent;
1103
- const inset = opts.inset ?? "16px";
925
+ const insetRaw = opts.inset ?? "16px";
926
+ const inset = VALID_CSS_LENGTH.test(insetRaw) ? insetRaw : "16px";
927
+ if (inset !== insetRaw) {
928
+ devWarn(
929
+ `[bottom-sheet] setScrimOverlay: invalid inset ${JSON.stringify(insetRaw)}; expected a CSS length like "16px" / "1rem" / "5%". Falling back to "16px".`
930
+ );
931
+ }
932
+ if (opts.children.ownerDocument && opts.children.ownerDocument !== screen.ownerDocument) {
933
+ devWarn(
934
+ "[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."
935
+ );
936
+ }
1104
937
  const position = opts.position ?? "top-right";
1105
938
  const interactive = opts.interactive ?? true;
1106
939
  const wrapper = screen.ownerDocument.createElement("div");
@@ -1149,12 +982,30 @@ var ScrimController = class {
1149
982
  ws.bottom = inset;
1150
983
  ws.right = inset;
1151
984
  break;
985
+ case "sheet-top-left":
986
+ case "sheet-top-center":
987
+ case "sheet-top-right":
988
+ applySheetAnchoredPosition(
989
+ ws,
990
+ this.mode,
991
+ position,
992
+ inset
993
+ );
994
+ break;
1152
995
  }
1153
996
  if (interactive) {
1154
997
  ws.pointerEvents = "auto";
1155
998
  }
1156
999
  wrapper.appendChild(opts.children);
1157
- screen.appendChild(wrapper);
1000
+ const host = screen.parentElement;
1001
+ if (!host) {
1002
+ devWarn(
1003
+ "[bottom-sheet] setScrimOverlay: scrim element has no parent, skipping overlay injection. Mount the sheet root before calling setScrimOverlay."
1004
+ );
1005
+ return () => {
1006
+ };
1007
+ }
1008
+ host.appendChild(wrapper);
1158
1009
  const teardown = () => {
1159
1010
  if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
1160
1011
  if (this.scrimOverlayTeardown === teardown) {
@@ -1164,7 +1015,6 @@ var ScrimController = class {
1164
1015
  this.scrimOverlayTeardown = teardown;
1165
1016
  return teardown;
1166
1017
  }
1167
- /** Tear down toggle-state listeners + remove any injected overlay wrapper. */
1168
1018
  destroy() {
1169
1019
  this.detachScrimTap?.();
1170
1020
  this.detachScrimTap = null;
@@ -1183,8 +1033,6 @@ var ScrimController = class {
1183
1033
  this.lastBackdropPointer = "";
1184
1034
  this.lastScreenDisplay = "";
1185
1035
  }
1186
- // ── Private helpers ────────────────────────────────────────────────
1187
- /** Centralized scrim style writer — atomic 3-property write across all setters. */
1188
1036
  applyScrimStyles(opts) {
1189
1037
  if (!this.screenComponent) return;
1190
1038
  const s = this.screenComponent.style;
@@ -1206,7 +1054,6 @@ var ScrimController = class {
1206
1054
  s.pointerEvents = opts.interactive ? "auto" : "none";
1207
1055
  }
1208
1056
  }
1209
- /** Pin the screen overlay to the area NOT occupied by the sheet. */
1210
1057
  applyAboveSheetInset() {
1211
1058
  if (!this.screenComponent) return;
1212
1059
  const insetByMode = {
@@ -1221,6 +1068,35 @@ var ScrimController = class {
1221
1068
  s.pointerEvents = "none";
1222
1069
  }
1223
1070
  };
1071
+ function resolveSheetAnchoredStyle(mode, position, inset) {
1072
+ const sizeOffset = `calc(var(--bs-size, 0px) + ${inset})`;
1073
+ switch (mode) {
1074
+ case "bottom":
1075
+ if (position === "sheet-top-left") return { bottom: sizeOffset, left: inset };
1076
+ if (position === "sheet-top-right") return { bottom: sizeOffset, right: inset };
1077
+ return { bottom: sizeOffset, left: "50%", transform: "translateX(-50%)" };
1078
+ case "top":
1079
+ if (position === "sheet-top-left") return { top: sizeOffset, left: inset };
1080
+ if (position === "sheet-top-right") return { top: sizeOffset, right: inset };
1081
+ return { top: sizeOffset, left: "50%", transform: "translateX(-50%)" };
1082
+ case "left":
1083
+ if (position === "sheet-top-left") return { left: sizeOffset, top: inset };
1084
+ if (position === "sheet-top-right") return { left: sizeOffset, bottom: inset };
1085
+ return { left: sizeOffset, top: "50%", transform: "translateY(-50%)" };
1086
+ case "right":
1087
+ if (position === "sheet-top-left") return { right: sizeOffset, top: inset };
1088
+ if (position === "sheet-top-right") return { right: sizeOffset, bottom: inset };
1089
+ return { right: sizeOffset, top: "50%", transform: "translateY(-50%)" };
1090
+ }
1091
+ }
1092
+ function applySheetAnchoredPosition(ws, mode, position, inset) {
1093
+ const style = resolveSheetAnchoredStyle(mode, position, inset);
1094
+ if (style.top !== void 0) ws.top = style.top;
1095
+ if (style.bottom !== void 0) ws.bottom = style.bottom;
1096
+ if (style.left !== void 0) ws.left = style.left;
1097
+ if (style.right !== void 0) ws.right = style.right;
1098
+ if (style.transform !== void 0) ws.transform = style.transform;
1099
+ }
1224
1100
 
1225
1101
  // src/core/animation/animation.ts
1226
1102
  var easeOutBack = (t) => {
@@ -1377,13 +1253,9 @@ var AnimationRunner = class {
1377
1253
  __publicField(this, "duration");
1378
1254
  __publicField(this, "easing");
1379
1255
  __publicField(this, "respectReducedMotion");
1380
- /** Live-tracked via mq listener so animateTo skips matchMedia per snap. */
1381
1256
  __publicField(this, "reducedMotion", false);
1382
- /** Disposes the matchMedia change listener — drained from destroy(). */
1383
1257
  __publicField(this, "detachReducedMotion", null);
1384
- /** Computed once — both inputs (option + API presence) stable for life. */
1385
1258
  __publicField(this, "viewTransitionsAvailable");
1386
- /** In-flight handles. Public for tests / debugging only — engine drives via cancel(). */
1387
1259
  __publicField(this, "currentTween", null);
1388
1260
  __publicField(this, "currentSpring", null);
1389
1261
  this.element = deps.element;
@@ -1414,26 +1286,13 @@ var AnimationRunner = class {
1414
1286
  }
1415
1287
  }
1416
1288
  }
1417
- /**
1418
- * Live derived flag — `true` whenever a spring/tween handle is in flight.
1419
- * Reading the canonical handles (instead of mirroring into a separate
1420
- * boolean) eliminates the reduced-motion early-return latent bug where
1421
- * the flag stayed true forever, and removes the cross-controller
1422
- * setIsAnimating callback.
1423
- */
1424
1289
  get isAnimating() {
1425
1290
  return this.currentTween !== null || this.currentSpring !== null;
1426
1291
  }
1427
- /** Cancel any in-flight tween/spring without touching engine state. */
1428
1292
  cancel() {
1429
1293
  this.currentTween?.cancel();
1430
1294
  this.currentSpring?.cancel();
1431
1295
  }
1432
- /**
1433
- * Animate from current size to `target`. Resolves when the animation
1434
- * completes; resolves early if cancelled mid-flight (caller checks an
1435
- * abort signal post-await for cycle-invariant correctness).
1436
- */
1437
1296
  async animateTo(target, velocityPxPerMs) {
1438
1297
  if (target === this.getSize()) return;
1439
1298
  this.currentTween?.cancel();
@@ -1474,7 +1333,6 @@ var AnimationRunner = class {
1474
1333
  if (!this.isDragging()) this.element.style.willChange = "auto";
1475
1334
  }
1476
1335
  }
1477
- /** Tear down the matchMedia listener. Engine calls from destroy(). */
1478
1336
  destroy() {
1479
1337
  this.detachReducedMotion?.();
1480
1338
  this.detachReducedMotion = null;
@@ -1634,12 +1492,8 @@ var LifecycleController = class {
1634
1492
  __publicField(this, "inertSiblings");
1635
1493
  __publicField(this, "shouldApplyInertSiblings");
1636
1494
  __publicField(this, "returnFocus");
1637
- /** Toggle-state teardowns — fire on close, NOT in destroy-only stack. */
1638
1495
  __publicField(this, "releaseFocusTrap", null);
1639
1496
  __publicField(this, "releaseScrollLock", null);
1640
- /** True between install() and release(). Guards release()/destroy() from
1641
- * re-running side effects (notably `returnFocus.focus()`) on already-
1642
- * released or never-installed instances. */
1643
1497
  __publicField(this, "installed", false);
1644
1498
  __publicField(this, "destroyed", false);
1645
1499
  this.element = deps.element;
@@ -1653,14 +1507,6 @@ var LifecycleController = class {
1653
1507
  this.returnFocus = opts.returnFocus;
1654
1508
  this.inertSiblings = createInertSiblings(() => this.element);
1655
1509
  }
1656
- /**
1657
- * Run the open-time install steps. Idempotent: each step checks its
1658
- * existing teardown ref so a re-open during a half-cancelled close
1659
- * doesn't double-install. Sets `installed=true` BEFORE the first side
1660
- * effect so a partial throw (detached / shadow-DOM target) leaves the
1661
- * caller's `release()` rollback path armed to undo whatever did succeed.
1662
- * No-op when destroyed.
1663
- */
1664
1510
  install() {
1665
1511
  if (this.destroyed) return;
1666
1512
  this.installed = true;
@@ -1677,22 +1523,10 @@ var LifecycleController = class {
1677
1523
  this.inertSiblings.apply();
1678
1524
  }
1679
1525
  }
1680
- /**
1681
- * Update the post-release focus target. Overlay re-binds this when the
1682
- * consumer changes `returnFocus` after construction; the new value applies
1683
- * on the next `release()`. No-op when destroyed.
1684
- */
1685
1526
  setReturnFocus(target) {
1686
1527
  if (this.destroyed) return;
1687
1528
  this.returnFocus = target;
1688
1529
  }
1689
- /**
1690
- * Release all install-time teardowns. Short-circuits when not installed,
1691
- * so `close() → destroy()` (each routing through here) only restores
1692
- * focus once. The `installed` guard lets `inertSiblings.remove()` and
1693
- * the focus-trap/scroll-lock teardown nullings stay simple — they were
1694
- * already idempotent, but `returnFocus.focus()` was firing on every call.
1695
- */
1696
1530
  release() {
1697
1531
  if (!this.installed) return;
1698
1532
  this.installed = false;
@@ -1706,12 +1540,6 @@ var LifecycleController = class {
1706
1540
  target?.focus?.();
1707
1541
  }
1708
1542
  }
1709
- /**
1710
- * Final teardown — releases any still-installed listeners and seals the
1711
- * controller. After destroy(), install() is a no-op and setReturnFocus()
1712
- * is rejected, so post-destroy mutations can't queue up a stale focus
1713
- * restore.
1714
- */
1715
1543
  destroy() {
1716
1544
  this.release();
1717
1545
  this.destroyed = true;
@@ -1721,12 +1549,10 @@ var LifecycleController = class {
1721
1549
  // src/core/gestures.ts
1722
1550
  var isAxisVertical = (mode) => mode === "bottom" || mode === "top";
1723
1551
  var SampleRing = class {
1724
- // current count, ≤ capacity
1725
1552
  constructor(capacity) {
1726
1553
  __publicField(this, "slots");
1727
1554
  __publicField(this, "capacity");
1728
1555
  __publicField(this, "head", 0);
1729
- // next write index
1730
1556
  __publicField(this, "size", 0);
1731
1557
  this.capacity = capacity;
1732
1558
  this.slots = new Array(capacity);
@@ -1739,12 +1565,6 @@ var SampleRing = class {
1739
1565
  this.head = (this.head + 1) % this.capacity;
1740
1566
  if (this.size < this.capacity) this.size++;
1741
1567
  }
1742
- /**
1743
- * Drop oldest entries whose timestamp is below `cutoff`, but never shrink
1744
- * below `minKeep`. Mirrors the original
1745
- * `while (samples.length > minKeep && samples[0].t < cutoff) samples.shift()`
1746
- * but without copying — just advances the logical tail.
1747
- */
1748
1568
  pruneBefore(cutoff, minKeep) {
1749
1569
  while (this.size > minKeep) {
1750
1570
  const oldestIdx = (this.head - this.size + this.capacity) % this.capacity;
@@ -1881,28 +1701,14 @@ var GestureController = class {
1881
1701
  __publicField(this, "detach", null);
1882
1702
  __publicField(this, "isDragging_", false);
1883
1703
  __publicField(this, "dragStartSize", 0);
1884
- // Only "touch" gestures are eligible for soft-keyboard dismissal —
1885
- // mouse/trackpad users don't have a keyboard to dismiss.
1886
1704
  __publicField(this, "currentPointerKind", "touch");
1887
- // One blur per drag — flipping focus every move event races the keyboard's
1888
- // own animations on iOS and makes the page jitter.
1889
1705
  __publicField(this, "keyboardDismissed", false);
1890
- // Reused payload object for the "drag" event — mutated each onMove instead
1891
- // of allocating a fresh `{size, delta}` per frame. At 60-120Hz a long drag
1892
- // would otherwise churn tens of KB through the GC. Mirrors browser
1893
- // PointerEvent semantics: consumers must NOT retain the reference across
1894
- // frames (clone if needed). See SheetEventMap.drag JSDoc.
1895
1706
  __publicField(this, "dragPayload", { size: 0, delta: 0 });
1896
1707
  this.deps = deps;
1897
1708
  }
1898
- /** True between `dragstart` and `dragend`/`onCancel`. */
1899
1709
  get isDragging() {
1900
1710
  return this.isDragging_;
1901
1711
  }
1902
- /**
1903
- * Bind pointer listeners on the handle and return a teardown. Engine pushes
1904
- * the teardown into its TeardownStack so destroy() drains it once.
1905
- */
1906
1712
  install() {
1907
1713
  if (this.detach) return this.detach;
1908
1714
  this.detach = installGestures(this.deps.handle, this.deps.mode, {
@@ -1959,11 +1765,6 @@ var GestureController = class {
1959
1765
  });
1960
1766
  return this.detach;
1961
1767
  }
1962
- /**
1963
- * Force-clear drag visual state. Engine calls from destroy() when destroyed
1964
- * mid-drag — gesture detach won't synthesize an onCancel, so we have to
1965
- * roll back the data-attribute + will-change ourselves.
1966
- */
1967
1768
  forceClearDragState() {
1968
1769
  if (!this.isDragging_) return;
1969
1770
  this.isDragging_ = false;
@@ -1982,11 +1783,6 @@ var AriaSliderWriter = class {
1982
1783
  mode === "bottom" || mode === "top" ? "vertical" : "horizontal"
1983
1784
  );
1984
1785
  }
1985
- /**
1986
- * Update slider position. `allowedIds` length determines `aria-valuemax`;
1987
- * `activeId` indexes into it for `aria-valuenow` + `aria-valuetext`.
1988
- * Empty list → strip every dynamic attr so AT skips the handle entirely.
1989
- */
1990
1786
  setValue(allowedIds, activeId) {
1991
1787
  if (allowedIds.length === 0) {
1992
1788
  this.handle.removeAttribute("aria-valuemin");
@@ -2060,9 +1856,6 @@ function resolveEngineOptions(opts, extras) {
2060
1856
  scrimMode: opts.scrimMode,
2061
1857
  scrimColor: opts.scrimColor,
2062
1858
  scrimBlur: opts.scrimBlur,
2063
- // Auto-promote pointer-events when tap-to-close is on so the click
2064
- // listener actually receives events. Caller can still force `false`
2065
- // explicitly to opt out — `??` only kicks in for `undefined`.
2066
1859
  scrimInteractive: opts.scrimInteractive ?? (opts.scrimTapToClose ? true : void 0),
2067
1860
  scrimTapToClose: opts.scrimTapToClose,
2068
1861
  scrimPreset: opts.scrimPreset,
@@ -2083,10 +1876,6 @@ function resolveEngineOptions(opts, extras) {
2083
1876
  closeOnEscape: opts.closeOnEscape,
2084
1877
  lockBodyScroll: opts.lockBodyScroll,
2085
1878
  inertSiblings: opts.inertSiblings,
2086
- // Folded in from `extras` so the lifecycle bag is fully assembled at
2087
- // the call site — engine no longer needs a spread+override at
2088
- // controller construction. When `extras` is omitted, the field stays
2089
- // undefined and the controller uses its own default predicate.
2090
1879
  shouldApplyInertSiblings: extras?.shouldApplyInertSiblings
2091
1880
  }
2092
1881
  };
@@ -2121,6 +1910,7 @@ var BottomSheetEngine = class {
2121
1910
  __publicField(this, "scrollContainer");
2122
1911
  __publicField(this, "backdrop");
2123
1912
  __publicField(this, "screenComponent");
1913
+ __publicField(this, "scrimParent", null);
2124
1914
  __publicField(this, "mode");
2125
1915
  __publicField(this, "flickVelocity");
2126
1916
  __publicField(this, "dragThreshold");
@@ -2131,40 +1921,22 @@ var BottomSheetEngine = class {
2131
1921
  __publicField(this, "lifecycle");
2132
1922
  __publicField(this, "closeOnBack");
2133
1923
  __publicField(this, "routedTo");
2134
- // Engine retains `snapPointsRaw` for the "find a closed-id" query in
2135
- // close() (raw input, not resolved geometry). Resolver owns geometry.
2136
1924
  __publicField(this, "snapPointsRaw");
2137
1925
  __publicField(this, "snaps");
2138
1926
  __publicField(this, "activeId");
2139
1927
  __publicField(this, "size", 0);
2140
- // Constructed in attach(); a plugin's install() could read `state` before
2141
- // attach completes if the constructor throws partway through. State getter
2142
- // null-guards via `this.gesture?.isDragging ?? false`.
2143
1928
  __publicField(this, "gesture");
2144
1929
  __publicField(this, "rootEl", null);
2145
1930
  __publicField(this, "destroyed", false);
2146
- // In-flight ViewTransition handle. A second snapTo while a VT is in flight
2147
- // would otherwise let Chromium reject the second's promise and flicker —
2148
- // newCycle() calls skipTransition() to abort cleanly.
2149
1931
  __publicField(this, "currentViewTransition", null);
2150
- // Each cycle captures the signal at entry; when a newer cycle aborts the
2151
- // controller, the captured signal flips to `aborted=true` and post-await
2152
- // guards short-circuit (prevents stale snap/open/close emits).
2153
1932
  __publicField(this, "currentAbort", new AbortController());
2154
- // Sub-pixel `--bs-size` deltas and <1% `--bs-progress` deltas are not
2155
- // visually distinguishable; gating CSSOM writes drops 30-50% of writes
2156
- // during the spring's settle tail.
2157
1933
  __publicField(this, "sizeWriteSentinel", new WriteSentinel());
2158
1934
  __publicField(this, "progressWriteSentinel", new WriteSentinel());
2159
1935
  __publicField(this, "isTopSheet", true);
2160
- // Pooled payload — identity documented in SheetEventMap.progress JSDoc;
2161
- // consumers must not retain across frames.
2162
1936
  __publicField(this, "progressPayload", {
2163
1937
  value: 0,
2164
1938
  size: 0
2165
1939
  });
2166
- // Pooled buffer for `getDragContext()` — onMove calls this 60-120×/s.
2167
- // Inner `range` object identity is also stable (mutated in place).
2168
1940
  __publicField(this, "dragContextBuf", {
2169
1941
  size: 0,
2170
1942
  maxAxisSize: 0,
@@ -2175,13 +1947,7 @@ var BottomSheetEngine = class {
2175
1947
  __publicField(this, "persistKey");
2176
1948
  __publicField(this, "linkedSheets", []);
2177
1949
  __publicField(this, "scrollCache");
2178
- // Single LIFO stack for ALL destroy-only teardowns. Plugins (added last)
2179
- // tear down first before the infrastructure they depend on. Toggle-semantic
2180
- // listeners (focus-trap / scroll-lock / scrim tap-to-close) stay as named
2181
- // fields on their controllers.
2182
1950
  __publicField(this, "teardowns", new TeardownStack());
2183
- // Pre-computed transform template — built once per engine since `mode` is
2184
- // fixed for its lifetime. Avoids the 4-arm switch at 60-120 Hz.
2185
1951
  __publicField(this, "transformTemplate");
2186
1952
  const resolved = resolveEngineOptions(opts, {
2187
1953
  shouldApplyInertSiblings: () => {
@@ -2196,7 +1962,8 @@ var BottomSheetEngine = class {
2196
1962
  this.handle = opts.handle ?? opts.element;
2197
1963
  this.scrollContainer = opts.scrollContainer;
2198
1964
  this.backdrop = opts.backdrop;
2199
- this.screenComponent = opts.screenComponent;
1965
+ this.screenComponent = opts.scrim ?? opts.screenComponent;
1966
+ this.scrimParent = this.screenComponent?.parentElement ?? null;
2200
1967
  this.mode = resolved.mode;
2201
1968
  this.transformTemplate = buildTransformTemplate(this.mode);
2202
1969
  this.aria = new AriaSliderWriter(this.handle, this.mode);
@@ -2224,8 +1991,6 @@ var BottomSheetEngine = class {
2224
1991
  getRootEl: () => this.rootEl,
2225
1992
  applySize: (size) => this.applySize(size),
2226
1993
  getSize: () => this.size,
2227
- // Optional chain protects synthetic test paths that drive animateTo
2228
- // before attach() (gesture is constructed there).
2229
1994
  isDragging: () => this.gesture?.isDragging ?? false
2230
1995
  },
2231
1996
  resolved.animation
@@ -2330,21 +2095,6 @@ var BottomSheetEngine = class {
2330
2095
  on(event, fn) {
2331
2096
  return this.bus.on(event, fn);
2332
2097
  }
2333
- /**
2334
- * Register a plugin. `install` runs synchronously; if it returns a function,
2335
- * it's captured for teardown during `destroy()`. No-op if already destroyed
2336
- * — installing into a torn-down engine would leak listeners.
2337
- *
2338
- * Throws from `plugin.install` are caught and rethrown via `queueMicrotask`
2339
- * so a buggy plugin can't take the engine down with it: the engine stays
2340
- * usable, sibling plugins still install, and the error surfaces in dev tools
2341
- * on the next tick instead of being swallowed. Mirrors the per-fn isolation
2342
- * model used for teardowns.
2343
- *
2344
- * Plugins that accept the second `scope` argument get transactional cleanup:
2345
- * push partial-install cleanups via `scope.add(fn)` and the engine drains
2346
- * them on throw, OR merges them into destroy-time teardowns on success.
2347
- */
2348
2098
  use(plugin) {
2349
2099
  if (this.destroyed) {
2350
2100
  console.warn(
@@ -2354,9 +2104,6 @@ var BottomSheetEngine = class {
2354
2104
  }
2355
2105
  const scoped = [];
2356
2106
  const scope = {
2357
- // typeof check — not just truthiness — so a JS-only plugin pushing a
2358
- // bogus value is silently rejected instead of queueing a confusing
2359
- // TypeError on destroy() drain.
2360
2107
  add: (fn) => {
2361
2108
  if (typeof fn === "function") scoped.push(fn);
2362
2109
  }
@@ -2385,36 +2132,13 @@ var BottomSheetEngine = class {
2385
2132
  }
2386
2133
  return this;
2387
2134
  }
2388
- /**
2389
- * Replace the linked-sheets list at runtime. Use when peer engines aren't
2390
- * available at construction time (mounted later in the tree). The wiring
2391
- * to the `open` event is independent of this list — it reads `linkedSheets`
2392
- * lazily on emit, so this setter is a simple field swap.
2393
- */
2394
2135
  setLinkedSheets(sheets) {
2395
2136
  if (this.destroyed) return;
2396
2137
  this.linkedSheets = sheets;
2397
2138
  }
2398
- /** Read-only view of the current allow-list, for cross-sheet target negotiation. */
2399
2139
  getAllowedIds() {
2400
2140
  return this.snaps.getAllowedIds().slice();
2401
2141
  }
2402
- /**
2403
- * Animate to a snap point by id. Resolves when the animation completes.
2404
- *
2405
- * Two call shapes:
2406
- * ```ts
2407
- * engine.snapTo("full"); // default velocity
2408
- * engine.snapTo("full", 0.5); // px/ms initial velocity
2409
- * engine.snapTo("full", { velocity: 0.5, signal }); // abortable
2410
- * ```
2411
- *
2412
- * When `opts.signal` is provided, calling `controller.abort()` cancels the
2413
- * in-flight tween/spring and the returned promise resolves without firing
2414
- * `snap`. The captured `AbortSignal` is checked after each await so external
2415
- * aborts ride the same cycle invariant as internal ones (resize, destroy,
2416
- * concurrent snapTo).
2417
- */
2418
2142
  async snapTo(id, velocityOrOpts = 0, _skipBeforeSnap = false) {
2419
2143
  if (this.destroyed) return;
2420
2144
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2473,16 +2197,6 @@ var BottomSheetEngine = class {
2473
2197
  this.handleClose();
2474
2198
  }
2475
2199
  }
2476
- /**
2477
- * Drive the sheet to an arbitrary px size, bypassing snap matching.
2478
- * Useful for preview scrubs, animation chaining, or programmatic gestures.
2479
- * Animates with the configured spring/tween. Resolves when the animation
2480
- * completes. Does NOT update `activeId` — call `snapTo(id)` after to commit.
2481
- *
2482
- * Unlike `snapTo`, this does not fire `before-snap` (it isn't a snap commit)
2483
- * nor `snap` (the sheet isn't settling to a known snap). `progress` events
2484
- * still fire via applySize. Target is clamped to `[0, maxAxisSize]`.
2485
- */
2486
2200
  async dragTo(targetSize, velocityOrOpts = 0) {
2487
2201
  if (this.destroyed) return;
2488
2202
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2520,20 +2234,10 @@ var BottomSheetEngine = class {
2520
2234
  this.snaps.setAllowedIds(ids);
2521
2235
  this.updateAriaSlider();
2522
2236
  if (snap && ids.includes(snap)) {
2523
- void this.snapTo(
2524
- snap,
2525
- 0,
2526
- /* internal */
2527
- true
2528
- );
2237
+ void this.snapTo(snap, 0, true);
2529
2238
  } else if (!ids.includes(this.activeId)) {
2530
2239
  const fallback = ids[0];
2531
- if (fallback) void this.snapTo(
2532
- fallback,
2533
- 0,
2534
- /* internal */
2535
- true
2536
- );
2240
+ if (fallback) void this.snapTo(fallback, 0, true);
2537
2241
  }
2538
2242
  }
2539
2243
  setBackdropRange(range) {
@@ -2551,21 +2255,6 @@ var BottomSheetEngine = class {
2551
2255
  setScrimInteractive(interactive) {
2552
2256
  this.scrim.setScrimInteractive(interactive);
2553
2257
  }
2554
- /**
2555
- * Batch-update multiple scrim fields in one call. Apply order is:
2556
- * 1. `preset` — fills baseline color/blur/range/interactive.
2557
- * 2. `mode` — re-positions the scrim (fixed inset / clear / hide).
2558
- * 3. `enabled` — flips the opacity contribution on/off.
2559
- * 4. `tapToClose` — installs/tears the click listener.
2560
- * 5. `color` / `blur` / `interactive` / `range` — explicit overrides.
2561
- * 6. ONE `applySize` so the per-frame opacity stays consistent.
2562
- *
2563
- * Performance: collecting changes here (vs calling individual setters)
2564
- * avoids repeated `applySize` calls during a multi-field swap. Setters
2565
- * called here suppress their own internal `applySize` either by passing
2566
- * through fast-paths (mode/enabled no-op when value unchanged) or by
2567
- * being visual-only (color/blur/interactive don't influence opacity).
2568
- */
2569
2258
  setScrim(opts) {
2570
2259
  this.scrim.setScrim(opts, () => this.applySize(this.size));
2571
2260
  }
@@ -2581,11 +2270,6 @@ var BottomSheetEngine = class {
2581
2270
  setScrimOverlay(opts) {
2582
2271
  return this.scrim.setScrimOverlay(opts);
2583
2272
  }
2584
- /**
2585
- * Read-only snapshot of scrim state. Use the `setScrim*` setters to mutate.
2586
- * Stable public API — internals (e.g. cached opacity sentinels) are NOT
2587
- * exposed so consumers can't accidentally couple to ScrimController shape.
2588
- */
2589
2273
  getScrimState() {
2590
2274
  return { mode: this.scrim.scrimMode, enabled: this.scrim.scrimEnabled };
2591
2275
  }
@@ -2631,24 +2315,11 @@ var BottomSheetEngine = class {
2631
2315
  emit(event, payload) {
2632
2316
  this.bus.emit(event, payload);
2633
2317
  }
2634
- /**
2635
- * Abort the current cycle's signal and replace the controller. Returns the
2636
- * fresh signal so the caller can capture it locally before any await — when
2637
- * a future newCycle() / destroy aborts our controller, the captured signal
2638
- * flips to aborted and post-await guards short-circuit.
2639
- */
2640
2318
  newCycle() {
2641
2319
  this.currentAbort.abort();
2642
2320
  this.currentAbort = new AbortController();
2643
2321
  return this.currentAbort.signal;
2644
2322
  }
2645
- /**
2646
- * Fire `before-snap` and report whether a listener cancelled it. The
2647
- * `cancel()` callback exposed to listeners is frozen after the synchronous
2648
- * emit phase so async callers can't silently miss — they get a console
2649
- * warning. Returns `true` when cancelled so callers can early-return /
2650
- * spring-back without re-implementing the cancel/frozen pattern.
2651
- */
2652
2323
  emitBeforeSnap(target, previousId) {
2653
2324
  let cancelled = false;
2654
2325
  let frozen = false;
@@ -2682,30 +2353,9 @@ var BottomSheetEngine = class {
2682
2353
  this.snaps.setRaw(this.snapPointsRaw);
2683
2354
  this.scrim.invalidateOpacityCache();
2684
2355
  }
2685
- /**
2686
- * Memoised `allowedRange()` — delegates to SnapResolver which owns the cache.
2687
- * Engine wrapper kept so existing call sites don't all need updating;
2688
- * resolver-level memoization survives across method invocations.
2689
- */
2690
2356
  getAllowedRange() {
2691
2357
  return this.snaps.getAllowedRange();
2692
2358
  }
2693
- /**
2694
- * Write ARIA slider value attributes onto the handle so the engine itself
2695
- * is the source of truth for screen-reader announcements. Called from the
2696
- * constructor and from every code path that mutates `activeId` /
2697
- * `allowedIds`. WAI-ARIA 1.2 requires `aria-valuenow` for `role="slider"` —
2698
- * without it screen readers can't announce drag/snap progress to keyboard
2699
- * + AT users (WCAG 4.1.2 Name, Role, Value).
2700
- *
2701
- * Engine does NOT set `role="slider"` itself — that's the adapter's or
2702
- * consumer markup's responsibility. We only decorate with dynamic values.
2703
- *
2704
- * Empty allowed-list is malformed input (settable via `setAllowed([])`);
2705
- * short-circuit to remove ARIA value attributes so screen readers see a
2706
- * no-value slider rather than a degenerate `min=max=now=0` (WAI-ARIA 1.2
2707
- * requires `valuemax > valuemin` for `role="slider"`).
2708
- */
2709
2359
  updateAriaSlider() {
2710
2360
  this.aria.setValue(this.snaps.getAllowedIds(), this.activeId);
2711
2361
  }
@@ -2730,11 +2380,6 @@ var BottomSheetEngine = class {
2730
2380
  element: this.element,
2731
2381
  mode: this.mode,
2732
2382
  getRoot: () => this.rootEl,
2733
- // Single coalesced reader — narrows the DI surface and removes 3 getter
2734
- // dispatches per onMove frame at 60-120Hz. Returns a SHARED buffer
2735
- // (this.dragContextBuf) — see field JSDoc for the retention contract.
2736
- // Inner `range` object is mutated in place too so callers caching its
2737
- // identity between frames stay valid.
2738
2383
  getDragContext: () => {
2739
2384
  const buf = this.dragContextBuf;
2740
2385
  const r = this.getAllowedRange();
@@ -2900,7 +2545,6 @@ var BottomSheetEngine = class {
2900
2545
  }
2901
2546
  });
2902
2547
  }
2903
- /** Trigger a brief haptic tick (iOS Safari + Android Chrome support). */
2904
2548
  haptic() {
2905
2549
  if (typeof navigator !== "undefined" && "vibrate" in navigator) {
2906
2550
  try {
@@ -2916,6 +2560,9 @@ var BottomSheetEngine = class {
2916
2560
  style.transform = this.transformTemplate(offset);
2917
2561
  if (this.sizeWriteSentinel.shouldWrite(size, SIZE_WRITE_EPSILON)) {
2918
2562
  style.setProperty("--bs-size", `${size}px`);
2563
+ if (this.scrimParent) {
2564
+ this.scrimParent.style.setProperty("--bs-size", `${size}px`);
2565
+ }
2919
2566
  }
2920
2567
  const progress = this.computeProgress(size);
2921
2568
  const progressChanged = this.progressWriteSentinel.shouldWrite(
@@ -3047,8 +2694,6 @@ var OverlayEngine = class {
3047
2694
  __publicField(this, "exitEasing");
3048
2695
  __publicField(this, "backdropOpacity");
3049
2696
  __publicField(this, "backdropFilter");
3050
- // Definite-assignment: written via assignSwipeFromConfig in the
3051
- // constructor — TS can't follow the helper, mark the assertion explicit.
3052
2697
  __publicField(this, "swipeToCloseEnabled");
3053
2698
  __publicField(this, "swipeThreshold");
3054
2699
  __publicField(this, "swipeVelocityThreshold");
@@ -3068,13 +2713,7 @@ var OverlayEngine = class {
3068
2713
  __publicField(this, "isOpen_", false);
3069
2714
  __publicField(this, "isAnimating_", false);
3070
2715
  __publicField(this, "destroyed", false);
3071
- // Tracks whether setOverlayChildren ever populated the panel — lets
3072
- // clearOverlayChildren skip the DOM mutation when nothing was injected
3073
- // (so the field also acts as a zero-allocation feature flag).
3074
2716
  __publicField(this, "hasInjectedChildren", false);
3075
- // Bumped on every open/close. awaitTransition captures it and ignores
3076
- // transitionend/timeout fired by older cycles — prevents cross-cycle
3077
- // listener bleed during rapid toggling.
3078
2717
  __publicField(this, "cycleNonce", 0);
3079
2718
  __publicField(this, "bus", createEventBus());
3080
2719
  __publicField(this, "detachEscape", null);
@@ -3127,9 +2766,6 @@ var OverlayEngine = class {
3127
2766
  closeOnEscape: this.closeOnEscape,
3128
2767
  lockBodyScroll: this.bodyScrollLockEnabled,
3129
2768
  inertSiblings: this.inertSiblingsEnabled,
3130
- // Overlay-specific guard: only paint body siblings inert when the
3131
- // overlay element is actually a body descendant. Detached / shadow-
3132
- // DOM / portal overlays would otherwise lock unrelated trees.
3133
2769
  shouldApplyInertSiblings: () => {
3134
2770
  const bodyChildren = Array.from(document.body.children);
3135
2771
  return bodyChildren.some(
@@ -3283,12 +2919,6 @@ var OverlayEngine = class {
3283
2919
  this.releaseStackEntry = null;
3284
2920
  this.restoreMount();
3285
2921
  }
3286
- /**
3287
- * Runtime visual setter — opacity is applied immediately when open so a
3288
- * live-controls panel can preview the change without re-opening. When
3289
- * closed we only stash the field; next open() picks it up so we don't
3290
- * mutate hidden DOM (avoids paint thrash on detached panels).
3291
- */
3292
2922
  setBackdropOpacity(opacity) {
3293
2923
  if (this.destroyed) return;
3294
2924
  this.backdropOpacity = opacity;
@@ -3296,11 +2926,6 @@ var OverlayEngine = class {
3296
2926
  this.backdrop.style.opacity = String(opacity);
3297
2927
  }
3298
2928
  }
3299
- /**
3300
- * Runtime visual setter for `backdrop-filter`. `undefined` clears the
3301
- * filter (mirrors the close() behaviour). Defers to next open() when the
3302
- * overlay is currently closed.
3303
- */
3304
2929
  setBackdropFilter(filter) {
3305
2930
  if (this.destroyed) return;
3306
2931
  this.backdropFilter = filter;
@@ -3314,12 +2939,6 @@ var OverlayEngine = class {
3314
2939
  style.webkitBackdropFilter = "";
3315
2940
  }
3316
2941
  }
3317
- /**
3318
- * Toggle / reconfigure swipe-to-close at runtime. Re-resolves the flat
3319
- * threshold fields, then installs or tears down the pointer listener so
3320
- * the change applies to the CURRENT open cycle. Idempotent: safe to call
3321
- * with the same config repeatedly.
3322
- */
3323
2942
  setSwipeToClose(config) {
3324
2943
  if (this.destroyed) return;
3325
2944
  const wasEnabled = this.swipeToCloseEnabled;
@@ -3332,7 +2951,6 @@ var OverlayEngine = class {
3332
2951
  this.detachSwipe = null;
3333
2952
  }
3334
2953
  }
3335
- /** Animation setters only stage the field — applied on next open/close. */
3336
2954
  setEnterAnimation(animation) {
3337
2955
  if (this.destroyed) return;
3338
2956
  this.enterAnimation = animation;
@@ -3341,23 +2959,15 @@ var OverlayEngine = class {
3341
2959
  if (this.destroyed) return;
3342
2960
  this.exitAnimation = animation;
3343
2961
  }
3344
- /** Updates the field used at next close() — no immediate side effect. */
3345
2962
  setReturnFocus(target) {
3346
2963
  if (this.destroyed) return;
3347
2964
  this.lifecycle.setReturnFocus(target);
3348
2965
  }
3349
- /**
3350
- * Batch update — preset first, then individual overrides. Applied as a
3351
- * single DOM-write phase when open so live-controls don't trigger
3352
- * cascading reflows. Safe to call when closed (defers to next open).
3353
- */
3354
2966
  setOverlay(opts) {
3355
2967
  if (this.destroyed) return;
3356
2968
  const preset = opts.preset ? OVERLAY_PRESETS[opts.preset] : void 0;
3357
2969
  const merged = preset ? {
3358
2970
  backdropOpacity: preset.backdropOpacity,
3359
- // Preset's `undefined` filter means "no filter" — surface as null
3360
- // so the merged step clears the field instead of inheriting old.
3361
2971
  backdropFilter: preset.backdropFilter === void 0 ? null : preset.backdropFilter,
3362
2972
  swipeToClose: preset.swipeToClose,
3363
2973
  enterAnimation: preset.enterAnimation,
@@ -3416,30 +3026,17 @@ var OverlayEngine = class {
3416
3026
  }
3417
3027
  }
3418
3028
  }
3419
- /**
3420
- * Runtime children injection — replace the panel's contents without
3421
- * unmounting the engine. `replaceChildren()` is a single atomic DOM op,
3422
- * so callers can swap content rapidly without intermediate flicker.
3423
- * Function form is resolved exactly ONCE per call (cheap lazy content).
3424
- */
3425
3029
  setOverlayChildren(children) {
3426
3030
  if (this.destroyed) return;
3427
3031
  const node = typeof children === "function" ? children() : children;
3428
3032
  this.element.replaceChildren(node);
3429
3033
  this.hasInjectedChildren = true;
3430
3034
  }
3431
- /**
3432
- * Clear injected children. No-op when nothing was injected so consumers
3433
- * who never used setOverlayChildren can call this defensively without
3434
- * paying for a DOM mutation.
3435
- */
3436
3035
  clearOverlayChildren() {
3437
3036
  if (this.destroyed || !this.hasInjectedChildren) return;
3438
3037
  this.element.replaceChildren();
3439
3038
  this.hasInjectedChildren = false;
3440
3039
  }
3441
- // Shared swipe-config normalizer — used by constructor, setSwipeToClose,
3442
- // and setOverlay so the union/object shape is decoded in exactly one place.
3443
3040
  assignSwipeFromConfig(swipe) {
3444
3041
  if (typeof swipe === "object" && swipe !== null) {
3445
3042
  this.swipeToCloseEnabled = swipe.enabled ?? true;
@@ -3583,13 +3180,6 @@ var OverlayEngine = class {
3583
3180
  this.detachOutsidePointer = null;
3584
3181
  this.lifecycle.release();
3585
3182
  }
3586
- /**
3587
- * Light pointer-driven dismiss. Tracks finger along the edge axis, follows
3588
- * with translate, dismisses if past `swipeThreshold` (fraction of panel
3589
- * size) or release velocity exceeds `swipeVelocityThreshold` (px/ms).
3590
- * Pure CSS-transform during drag — no rAF or physics required since the
3591
- * panel only moves one direction.
3592
- */
3593
3183
  installSwipeToClose() {
3594
3184
  const el = this.element;
3595
3185
  const edge = this.edge;