@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/react.cjs CHANGED
@@ -239,11 +239,6 @@ var SnapResolver = class {
239
239
  this.allowedIds = allowed ?? raw.map((p) => p.id);
240
240
  this.recompute();
241
241
  }
242
- /**
243
- * Re-resolve raw → resolved against the current viewport / fit-measure,
244
- * recompute maxAxisSize, invalidate range cache, notify engine. Called on
245
- * resize, orientationchange, visualViewport changes, and snap-list swaps.
246
- */
247
242
  recompute() {
248
243
  this.resolved = resolveSnapList(this.raw, this.mode, this.measureFit);
249
244
  this.maxAxisSize = this.resolved.reduce(
@@ -253,15 +248,10 @@ var SnapResolver = class {
253
248
  this.rangeCache = null;
254
249
  this.onMaxAxisSizeChange?.(this.maxAxisSize);
255
250
  }
256
- /** Replace the raw snap list and recompute. Engine calls on `setSnapPoints`. */
257
251
  setRaw(raw) {
258
252
  this.raw = raw;
259
253
  this.recompute();
260
254
  }
261
- /**
262
- * Replace the allow-list. Range cache depends on `(resolvedSnaps ×
263
- * allowedIds)` so flipping ids alone invalidates without a full recompute.
264
- */
265
255
  setAllowedIds(ids) {
266
256
  this.allowedIds = ids;
267
257
  this.rangeCache = null;
@@ -269,11 +259,6 @@ var SnapResolver = class {
269
259
  findById(id) {
270
260
  return findById(id, this.resolved);
271
261
  }
272
- /**
273
- * Pure drag-settle target resolution. Engine passes the gesture-specific
274
- * fields (delta, velocity, pointerKind, flickVelocity, dragThreshold) plus
275
- * `size`/`activeId` since those are still engine-owned.
276
- */
277
262
  findDragSettleTarget(args) {
278
263
  return findDragSettleTarget({
279
264
  ...args,
@@ -282,18 +267,9 @@ var SnapResolver = class {
282
267
  maxAxisSize: this.maxAxisSize
283
268
  });
284
269
  }
285
- /**
286
- * Memoised allowed-range query. Twice-per-drag-frame (gesture onMove +
287
- * applySize→computeProgress); cache collapses 2× iteration of resolvedSnaps
288
- * to one recompute per geometry/allow-list change.
289
- */
290
270
  getAllowedRange() {
291
271
  return this.rangeCache ?? (this.rangeCache = allowedRange(this.resolved, this.allowedIds));
292
272
  }
293
- /**
294
- * Read-only view of resolved snaps. Returned as `readonly` to discourage
295
- * accidental mutation by feature install sites that close over this list.
296
- */
297
273
  getResolvedSnaps() {
298
274
  return this.resolved;
299
275
  }
@@ -303,18 +279,6 @@ var SnapResolver = class {
303
279
  getMaxAxisSize() {
304
280
  return this.maxAxisSize;
305
281
  }
306
- /**
307
- * External clamp override. The resize-observer feature compares
308
- * `getMaxAxisSize()` against the live viewport and shrinks if the largest
309
- * resolved snap exceeds it (mobile address-bar collapse / soft-keyboard
310
- * window resize). The next `recompute()` overwrites this clamp from the
311
- * resolved-snap reduction.
312
- *
313
- * Does NOT invalidate the range cache — viewport clamping doesn't change
314
- * `(resolvedSnaps × allowedIds) → range`. DOES fire `onMaxAxisSizeChange`
315
- * so the resolver owns the layout-axis style write end-to-end (symmetric
316
- * with `recompute()` — feature call sites no longer duplicate the write).
317
- */
318
282
  setMaxAxisSize(n) {
319
283
  this.maxAxisSize = n;
320
284
  this.onMaxAxisSizeChange?.(n);
@@ -689,73 +653,49 @@ var WriteSentinel = class {
689
653
  constructor() {
690
654
  __publicField(this, "last", -1);
691
655
  }
692
- /**
693
- * Returns `true` (and updates the cached value) if `next` differs from the
694
- * cached `last` by more than `epsilon`. Returns `false` (and leaves the
695
- * cache untouched) when the delta is within epsilon — caller skips the
696
- * CSSOM write.
697
- */
698
656
  shouldWrite(next, epsilon) {
699
657
  if (Math.abs(next - this.last) <= epsilon) return false;
700
658
  this.last = next;
701
659
  return true;
702
660
  }
703
- /**
704
- * Force the next `shouldWrite` to pass. Resets the cache to `-1` so any
705
- * non-negative `next` exceeds the epsilon comparison. Called from setters
706
- * that mutate state under the cache (range/mode/enabled flips that change
707
- * the progress→opacity mapping but don't move `progress` itself).
708
- */
709
661
  invalidate() {
710
662
  this.last = -1;
711
663
  }
712
- /**
713
- * Record `value` as the last-written without performing a write. Used
714
- * when a setter writes a literal value through a different path (e.g.
715
- * `setScrimMode("off")` writes `opacity: "0"` inline) and wants future
716
- * `shouldWrite` calls to short-circuit against that known cached state.
717
- */
718
664
  setLastWritten(value) {
719
665
  this.last = value;
720
666
  }
721
- /** Read-only accessor for diagnostic / dedup-aware peek. */
722
667
  get value() {
723
668
  return this.last;
724
669
  }
725
670
  };
726
671
 
672
+ // src/core/primitives/devWarn.ts
673
+ function devWarn(...args) {
674
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && typeof console !== "undefined") {
675
+ console.warn(...args);
676
+ }
677
+ }
678
+
727
679
  // src/core/controllers/scrim-controller.ts
680
+ var VALID_CSS_LENGTH = /^-?\d+(?:\.\d+)?(?:px|em|rem|%)$/;
728
681
  var ScrimController = class {
729
682
  constructor(deps, opts) {
730
- // ── DOM refs (mirror engine's; cached for hot path) ─────────────────
731
683
  __publicField(this, "screenComponent");
732
684
  __publicField(this, "backdrop");
733
685
  __publicField(this, "mode");
734
- // ── Engine callbacks ────────────────────────────────────────────────
735
686
  __publicField(this, "isDestroyed");
736
687
  __publicField(this, "isTopSheet");
737
688
  __publicField(this, "getAllowedIds");
738
689
  __publicField(this, "getResolvedSnaps");
739
690
  __publicField(this, "snapToFn");
740
- // ── Mode + state ────────────────────────────────────────────────────
741
691
  __publicField(this, "scrimMode");
742
692
  __publicField(this, "backdropRange");
743
693
  __publicField(this, "screenRange");
744
- // setScrimEnabled toggle — gated through `scrimEnabled` rather than nuking
745
- // ranges so re-enable can restore prior values without a remount. Public
746
- // so tests / introspection can read it without an `as any` cast.
747
694
  __publicField(this, "scrimEnabled", true);
748
695
  __publicField(this, "savedScrimRanges", null);
749
- // ── Tap-to-close ────────────────────────────────────────────────────
750
696
  __publicField(this, "scrimTapToCloseEnabled", false);
751
697
  __publicField(this, "detachScrimTap", null);
752
- // ── Scrim overlay slot (positioned floating element) ────────────────
753
698
  __publicField(this, "scrimOverlayTeardown", null);
754
- // ── Hot-path opacity dedup caches (per-frame writes) ────────────────
755
- // Numeric sentinels migrated to WriteSentinel primitive — single
756
- // invalidation surface, shared with the engine's size/progress sentinels.
757
- // The two string-typed sentinels (`Pointer`, `Display`) keep their inline
758
- // pattern: equality on string union, no epsilon, different reset semantics.
759
699
  __publicField(this, "backdropOpacitySentinel", new WriteSentinel());
760
700
  __publicField(this, "screenOpacitySentinel", new WriteSentinel());
761
701
  __publicField(this, "lastBackdropPointer", "");
@@ -790,41 +730,15 @@ var ScrimController = class {
790
730
  }
791
731
  }
792
732
  }
793
- /**
794
- * Invalidate the opacity dedup caches. Called from any setter that mutates
795
- * range/mode/enabled — opacity is a pure function of (progress, range,
796
- * enabled), so a config change makes the cached values stale even if
797
- * `progress` itself didn't move. Next `applyOpacity(progress, false)` call
798
- * sees the -1 sentinel and re-runs the math instead of short-circuiting.
799
- *
800
- * Public for the engine: `setSnapPoints` mutates resolved geometry which
801
- * shifts `progress→opacity` mapping under the cache; engine calls this to
802
- * keep the next render frame correct on a static sheet.
803
- * @internal
804
- */
805
733
  invalidateOpacityCache() {
806
734
  this.backdropOpacitySentinel.invalidate();
807
735
  this.screenOpacitySentinel.invalidate();
808
736
  }
809
- /** Engine calls this when its `attach()` runs — installs scrim-tap-click if requested. */
810
737
  attach() {
811
738
  if (this.scrimTapToCloseEnabled) {
812
739
  this.setScrimTapToClose(true);
813
740
  }
814
741
  }
815
- // ── Hot-path: opacity computation called per-frame from engine.applySize ──
816
- /**
817
- * Per-frame opacity writer. Computes backdrop + screen opacity from the
818
- * sheet's progress (0..1 within allowed range), gated by sub-pixel dedup
819
- * caches so steady-state frames don't trigger style recalc.
820
- *
821
- * `progressChanged` is the engine's already-computed dedup gate — when
822
- * `false`, opacity is by construction a pure function of `progress` so
823
- * cached values are still valid; we short-circuit the Math.min/max +
824
- * division work entirely. Setters that mutate ranges/mode/enabled
825
- * invalidate the cache by writing -1 sentinels so the next call re-runs
826
- * the math.
827
- */
828
742
  applyOpacity(progress, progressChanged) {
829
743
  if (!progressChanged && this.backdropOpacitySentinel.value !== -1 && this.screenOpacitySentinel.value !== -1) {
830
744
  return;
@@ -870,80 +784,30 @@ var ScrimController = class {
870
784
  }
871
785
  }
872
786
  }
873
- // ── Public setters (engine forwards these 1:1) ─────────────────────
874
- /**
875
- * Replace the [start, end] progress window in which the backdrop fades 0→1.
876
- * @param range - Tuple of two progress values in 0..1 (start <= end).
877
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
878
- * @example
879
- * // Backdrop only starts darkening after the sheet crosses 30% open.
880
- * engine.setBackdropRange([0.3, 1]);
881
- */
882
787
  setBackdropRange(range, applySize) {
883
788
  if (this.isDestroyed()) return;
884
789
  this.backdropRange = range;
885
790
  this.invalidateOpacityCache();
886
791
  applySize();
887
792
  }
888
- /**
889
- * Replace the [start, end] progress window in which the screen scrim fades 0→1.
890
- * @param range - Tuple of two progress values in 0..1 (start <= end).
891
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
892
- * @example
893
- * // Scrim is fully opaque only when the sheet is past 50% — softer feel.
894
- * engine.setScreenRange([0, 0.5]);
895
- */
896
793
  setScreenRange(range, applySize) {
897
794
  if (this.isDestroyed()) return;
898
795
  this.screenRange = range;
899
796
  this.invalidateOpacityCache();
900
797
  applySize();
901
798
  }
902
- /**
903
- * Set the scrim background color. Pass `null`/`undefined` to clear the
904
- * inline write and let consumer CSS take over.
905
- * @param color - Any valid CSS color string, or `null`/`undefined` to clear.
906
- * @example
907
- * engine.setScrimColor("rgba(0, 0, 0, 0.6)");
908
- * engine.setScrimColor(null); // hand back to consumer CSS
909
- */
910
799
  setScrimColor(color) {
911
800
  if (this.isDestroyed()) return;
912
801
  this.applyScrimStyles({ color: color === void 0 ? null : color });
913
802
  }
914
- /**
915
- * Set the scrim backdrop-filter blur radius. Pass `null`/`undefined` to clear.
916
- * @param blur - CSS length (e.g. `"8px"`) consumed as `blur(<value>)`, or
917
- * `null`/`undefined` to remove the filter.
918
- * @example
919
- * engine.setScrimBlur("12px"); // frosted-glass effect
920
- * engine.setScrimBlur(null); // disable blur
921
- */
922
803
  setScrimBlur(blur) {
923
804
  if (this.isDestroyed()) return;
924
805
  this.applyScrimStyles({ blur: blur === void 0 ? null : blur });
925
806
  }
926
- /**
927
- * Toggle whether the scrim layer captures pointer events.
928
- * @param interactive - `true` → `pointer-events: auto`; `false` → `none`.
929
- * @example
930
- * engine.setScrimInteractive(true); // scrim swallows clicks
931
- * engine.setScrimInteractive(false); // clicks pass through to content below
932
- */
933
807
  setScrimInteractive(interactive) {
934
808
  if (this.isDestroyed()) return;
935
809
  this.applyScrimStyles({ interactive });
936
810
  }
937
- /**
938
- * Switch scrim positioning at runtime.
939
- * @param mode - `"full"` (consumer CSS), `"above-sheet"` (engine-pinned inset
940
- * so only the area NOT covered by the sheet is dimmed), or
941
- * `"off"` (display:none + opacity:0 — no remount needed).
942
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
943
- * @example
944
- * engine.setScrimMode("above-sheet"); // dim only the area above the sheet
945
- * engine.setScrimMode("off"); // disable scrim layer
946
- */
947
811
  setScrimMode(mode, applySize) {
948
812
  if (this.isDestroyed()) return;
949
813
  if (mode === this.scrimMode) return;
@@ -973,15 +837,6 @@ var ScrimController = class {
973
837
  }
974
838
  applySize();
975
839
  }
976
- /**
977
- * Enable or disable click-on-scrim → snap to first non-zero allowed snap.
978
- * Auto-promotes `pointer-events: auto` when scrim is non-interactive so the
979
- * click listener actually fires.
980
- * @param enabled - `true` installs the click listener; `false` removes it.
981
- * @example
982
- * engine.setScrimTapToClose(true); // tap outside sheet collapses it
983
- * engine.setScrimTapToClose(false); // require explicit close button
984
- */
985
840
  setScrimTapToClose(enabled) {
986
841
  if (this.isDestroyed()) return;
987
842
  if (!this.screenComponent) return;
@@ -1011,16 +866,6 @@ var ScrimController = class {
1011
866
  this.scrimTapToCloseEnabled = false;
1012
867
  }
1013
868
  }
1014
- /**
1015
- * Toggle the scrim layer without remounting. Disabling stashes current
1016
- * `screenRange`/`backdropRange` and degenerates them to `[1, 1]` (opacity
1017
- * formula → 0 across the whole range); re-enabling restores the saved values.
1018
- * @param enabled - `false` hides scrim, `true` restores prior ranges.
1019
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
1020
- * @example
1021
- * engine.setScrimEnabled(false); // hide scrim, ranges remembered
1022
- * engine.setScrimEnabled(true); // restore prior ranges (no remount)
1023
- */
1024
869
  setScrimEnabled(enabled, applySize) {
1025
870
  if (this.isDestroyed()) return;
1026
871
  if (enabled === this.scrimEnabled) return;
@@ -1039,20 +884,6 @@ var ScrimController = class {
1039
884
  }
1040
885
  applySize();
1041
886
  }
1042
- /**
1043
- * Batch-update multiple scrim fields with a single applySize() at the end.
1044
- * If `preset` is set, it seeds color/blur/interactive/range first; explicit
1045
- * fields override.
1046
- * @param opts - Partial scrim config: preset, mode, enabled, tapToClose,
1047
- * color, blur, interactive, range.
1048
- * @param applySize - Engine-supplied recompute hook; runs ONCE per call.
1049
- * @example
1050
- * engine.setScrim({
1051
- * preset: "frosted",
1052
- * mode: "above-sheet",
1053
- * tapToClose: true,
1054
- * });
1055
- */
1056
887
  setScrim(opts, applySize) {
1057
888
  if (this.isDestroyed()) return;
1058
889
  if (opts.preset) {
@@ -1075,27 +906,18 @@ var ScrimController = class {
1075
906
  interactive: opts.interactive
1076
907
  });
1077
908
  if (opts.range !== void 0) {
1078
- this.screenRange = opts.range;
909
+ if (!this.scrimEnabled && this.savedScrimRanges) {
910
+ this.savedScrimRanges = {
911
+ screen: opts.range,
912
+ backdrop: this.savedScrimRanges.backdrop
913
+ };
914
+ } else {
915
+ this.screenRange = opts.range;
916
+ }
1079
917
  }
1080
918
  this.invalidateOpacityCache();
1081
919
  applySize();
1082
920
  }
1083
- /**
1084
- * Inject a positioned floating element into the scrim layer (e.g. a close
1085
- * button anchored top-right). Replaces any prior overlay so repeated calls
1086
- * (reactive effects re-running) don't accumulate wrappers.
1087
- * @param opts - Children element, optional `inset` (default `"16px"`),
1088
- * `position` (9-way grid, default `"top-right"`), and
1089
- * `interactive` flag (default `true`).
1090
- * @returns Teardown function that removes the wrapper from the DOM.
1091
- * @example
1092
- * const teardown = engine.setScrimOverlay({
1093
- * children: closeBtn,
1094
- * position: "top-right",
1095
- * inset: "20px",
1096
- * });
1097
- * // later: teardown(); // remove the floating button
1098
- */
1099
921
  setScrimOverlay(opts) {
1100
922
  if (this.isDestroyed()) return () => {
1101
923
  };
@@ -1103,7 +925,18 @@ var ScrimController = class {
1103
925
  };
1104
926
  this.scrimOverlayTeardown?.();
1105
927
  const screen = this.screenComponent;
1106
- const inset = opts.inset ?? "16px";
928
+ const insetRaw = opts.inset ?? "16px";
929
+ const inset = VALID_CSS_LENGTH.test(insetRaw) ? insetRaw : "16px";
930
+ if (inset !== insetRaw) {
931
+ devWarn(
932
+ `[bottom-sheet] setScrimOverlay: invalid inset ${JSON.stringify(insetRaw)}; expected a CSS length like "16px" / "1rem" / "5%". Falling back to "16px".`
933
+ );
934
+ }
935
+ if (opts.children.ownerDocument && opts.children.ownerDocument !== screen.ownerDocument) {
936
+ devWarn(
937
+ "[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."
938
+ );
939
+ }
1107
940
  const position = opts.position ?? "top-right";
1108
941
  const interactive = opts.interactive ?? true;
1109
942
  const wrapper = screen.ownerDocument.createElement("div");
@@ -1152,12 +985,30 @@ var ScrimController = class {
1152
985
  ws.bottom = inset;
1153
986
  ws.right = inset;
1154
987
  break;
988
+ case "sheet-top-left":
989
+ case "sheet-top-center":
990
+ case "sheet-top-right":
991
+ applySheetAnchoredPosition(
992
+ ws,
993
+ this.mode,
994
+ position,
995
+ inset
996
+ );
997
+ break;
1155
998
  }
1156
999
  if (interactive) {
1157
1000
  ws.pointerEvents = "auto";
1158
1001
  }
1159
1002
  wrapper.appendChild(opts.children);
1160
- screen.appendChild(wrapper);
1003
+ const host = screen.parentElement;
1004
+ if (!host) {
1005
+ devWarn(
1006
+ "[bottom-sheet] setScrimOverlay: scrim element has no parent, skipping overlay injection. Mount the sheet root before calling setScrimOverlay."
1007
+ );
1008
+ return () => {
1009
+ };
1010
+ }
1011
+ host.appendChild(wrapper);
1161
1012
  const teardown = () => {
1162
1013
  if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
1163
1014
  if (this.scrimOverlayTeardown === teardown) {
@@ -1167,7 +1018,6 @@ var ScrimController = class {
1167
1018
  this.scrimOverlayTeardown = teardown;
1168
1019
  return teardown;
1169
1020
  }
1170
- /** Tear down toggle-state listeners + remove any injected overlay wrapper. */
1171
1021
  destroy() {
1172
1022
  this.detachScrimTap?.();
1173
1023
  this.detachScrimTap = null;
@@ -1186,8 +1036,6 @@ var ScrimController = class {
1186
1036
  this.lastBackdropPointer = "";
1187
1037
  this.lastScreenDisplay = "";
1188
1038
  }
1189
- // ── Private helpers ────────────────────────────────────────────────
1190
- /** Centralized scrim style writer — atomic 3-property write across all setters. */
1191
1039
  applyScrimStyles(opts) {
1192
1040
  if (!this.screenComponent) return;
1193
1041
  const s = this.screenComponent.style;
@@ -1209,7 +1057,6 @@ var ScrimController = class {
1209
1057
  s.pointerEvents = opts.interactive ? "auto" : "none";
1210
1058
  }
1211
1059
  }
1212
- /** Pin the screen overlay to the area NOT occupied by the sheet. */
1213
1060
  applyAboveSheetInset() {
1214
1061
  if (!this.screenComponent) return;
1215
1062
  const insetByMode = {
@@ -1224,6 +1071,35 @@ var ScrimController = class {
1224
1071
  s.pointerEvents = "none";
1225
1072
  }
1226
1073
  };
1074
+ function resolveSheetAnchoredStyle(mode, position, inset) {
1075
+ const sizeOffset = `calc(var(--bs-size, 0px) + ${inset})`;
1076
+ switch (mode) {
1077
+ case "bottom":
1078
+ if (position === "sheet-top-left") return { bottom: sizeOffset, left: inset };
1079
+ if (position === "sheet-top-right") return { bottom: sizeOffset, right: inset };
1080
+ return { bottom: sizeOffset, left: "50%", transform: "translateX(-50%)" };
1081
+ case "top":
1082
+ if (position === "sheet-top-left") return { top: sizeOffset, left: inset };
1083
+ if (position === "sheet-top-right") return { top: sizeOffset, right: inset };
1084
+ return { top: sizeOffset, left: "50%", transform: "translateX(-50%)" };
1085
+ case "left":
1086
+ if (position === "sheet-top-left") return { left: sizeOffset, top: inset };
1087
+ if (position === "sheet-top-right") return { left: sizeOffset, bottom: inset };
1088
+ return { left: sizeOffset, top: "50%", transform: "translateY(-50%)" };
1089
+ case "right":
1090
+ if (position === "sheet-top-left") return { right: sizeOffset, top: inset };
1091
+ if (position === "sheet-top-right") return { right: sizeOffset, bottom: inset };
1092
+ return { right: sizeOffset, top: "50%", transform: "translateY(-50%)" };
1093
+ }
1094
+ }
1095
+ function applySheetAnchoredPosition(ws, mode, position, inset) {
1096
+ const style = resolveSheetAnchoredStyle(mode, position, inset);
1097
+ if (style.top !== void 0) ws.top = style.top;
1098
+ if (style.bottom !== void 0) ws.bottom = style.bottom;
1099
+ if (style.left !== void 0) ws.left = style.left;
1100
+ if (style.right !== void 0) ws.right = style.right;
1101
+ if (style.transform !== void 0) ws.transform = style.transform;
1102
+ }
1227
1103
 
1228
1104
  // src/core/animation/animation.ts
1229
1105
  var easeOutBack = (t) => {
@@ -1376,13 +1252,9 @@ var AnimationRunner = class {
1376
1252
  __publicField(this, "duration");
1377
1253
  __publicField(this, "easing");
1378
1254
  __publicField(this, "respectReducedMotion");
1379
- /** Live-tracked via mq listener so animateTo skips matchMedia per snap. */
1380
1255
  __publicField(this, "reducedMotion", false);
1381
- /** Disposes the matchMedia change listener — drained from destroy(). */
1382
1256
  __publicField(this, "detachReducedMotion", null);
1383
- /** Computed once — both inputs (option + API presence) stable for life. */
1384
1257
  __publicField(this, "viewTransitionsAvailable");
1385
- /** In-flight handles. Public for tests / debugging only — engine drives via cancel(). */
1386
1258
  __publicField(this, "currentTween", null);
1387
1259
  __publicField(this, "currentSpring", null);
1388
1260
  this.element = deps.element;
@@ -1413,26 +1285,13 @@ var AnimationRunner = class {
1413
1285
  }
1414
1286
  }
1415
1287
  }
1416
- /**
1417
- * Live derived flag — `true` whenever a spring/tween handle is in flight.
1418
- * Reading the canonical handles (instead of mirroring into a separate
1419
- * boolean) eliminates the reduced-motion early-return latent bug where
1420
- * the flag stayed true forever, and removes the cross-controller
1421
- * setIsAnimating callback.
1422
- */
1423
1288
  get isAnimating() {
1424
1289
  return this.currentTween !== null || this.currentSpring !== null;
1425
1290
  }
1426
- /** Cancel any in-flight tween/spring without touching engine state. */
1427
1291
  cancel() {
1428
1292
  this.currentTween?.cancel();
1429
1293
  this.currentSpring?.cancel();
1430
1294
  }
1431
- /**
1432
- * Animate from current size to `target`. Resolves when the animation
1433
- * completes; resolves early if cancelled mid-flight (caller checks an
1434
- * abort signal post-await for cycle-invariant correctness).
1435
- */
1436
1295
  async animateTo(target, velocityPxPerMs) {
1437
1296
  if (target === this.getSize()) return;
1438
1297
  this.currentTween?.cancel();
@@ -1473,7 +1332,6 @@ var AnimationRunner = class {
1473
1332
  if (!this.isDragging()) this.element.style.willChange = "auto";
1474
1333
  }
1475
1334
  }
1476
- /** Tear down the matchMedia listener. Engine calls from destroy(). */
1477
1335
  destroy() {
1478
1336
  this.detachReducedMotion?.();
1479
1337
  this.detachReducedMotion = null;
@@ -1633,12 +1491,8 @@ var LifecycleController = class {
1633
1491
  __publicField(this, "inertSiblings");
1634
1492
  __publicField(this, "shouldApplyInertSiblings");
1635
1493
  __publicField(this, "returnFocus");
1636
- /** Toggle-state teardowns — fire on close, NOT in destroy-only stack. */
1637
1494
  __publicField(this, "releaseFocusTrap", null);
1638
1495
  __publicField(this, "releaseScrollLock", null);
1639
- /** True between install() and release(). Guards release()/destroy() from
1640
- * re-running side effects (notably `returnFocus.focus()`) on already-
1641
- * released or never-installed instances. */
1642
1496
  __publicField(this, "installed", false);
1643
1497
  __publicField(this, "destroyed", false);
1644
1498
  this.element = deps.element;
@@ -1652,14 +1506,6 @@ var LifecycleController = class {
1652
1506
  this.returnFocus = opts.returnFocus;
1653
1507
  this.inertSiblings = createInertSiblings(() => this.element);
1654
1508
  }
1655
- /**
1656
- * Run the open-time install steps. Idempotent: each step checks its
1657
- * existing teardown ref so a re-open during a half-cancelled close
1658
- * doesn't double-install. Sets `installed=true` BEFORE the first side
1659
- * effect so a partial throw (detached / shadow-DOM target) leaves the
1660
- * caller's `release()` rollback path armed to undo whatever did succeed.
1661
- * No-op when destroyed.
1662
- */
1663
1509
  install() {
1664
1510
  if (this.destroyed) return;
1665
1511
  this.installed = true;
@@ -1676,22 +1522,10 @@ var LifecycleController = class {
1676
1522
  this.inertSiblings.apply();
1677
1523
  }
1678
1524
  }
1679
- /**
1680
- * Update the post-release focus target. Overlay re-binds this when the
1681
- * consumer changes `returnFocus` after construction; the new value applies
1682
- * on the next `release()`. No-op when destroyed.
1683
- */
1684
1525
  setReturnFocus(target) {
1685
1526
  if (this.destroyed) return;
1686
1527
  this.returnFocus = target;
1687
1528
  }
1688
- /**
1689
- * Release all install-time teardowns. Short-circuits when not installed,
1690
- * so `close() → destroy()` (each routing through here) only restores
1691
- * focus once. The `installed` guard lets `inertSiblings.remove()` and
1692
- * the focus-trap/scroll-lock teardown nullings stay simple — they were
1693
- * already idempotent, but `returnFocus.focus()` was firing on every call.
1694
- */
1695
1529
  release() {
1696
1530
  if (!this.installed) return;
1697
1531
  this.installed = false;
@@ -1705,12 +1539,6 @@ var LifecycleController = class {
1705
1539
  target?.focus?.();
1706
1540
  }
1707
1541
  }
1708
- /**
1709
- * Final teardown — releases any still-installed listeners and seals the
1710
- * controller. After destroy(), install() is a no-op and setReturnFocus()
1711
- * is rejected, so post-destroy mutations can't queue up a stale focus
1712
- * restore.
1713
- */
1714
1542
  destroy() {
1715
1543
  this.release();
1716
1544
  this.destroyed = true;
@@ -1720,12 +1548,10 @@ var LifecycleController = class {
1720
1548
  // src/core/gestures.ts
1721
1549
  var isAxisVertical = (mode) => mode === "bottom" || mode === "top";
1722
1550
  var SampleRing = class {
1723
- // current count, ≤ capacity
1724
1551
  constructor(capacity) {
1725
1552
  __publicField(this, "slots");
1726
1553
  __publicField(this, "capacity");
1727
1554
  __publicField(this, "head", 0);
1728
- // next write index
1729
1555
  __publicField(this, "size", 0);
1730
1556
  this.capacity = capacity;
1731
1557
  this.slots = new Array(capacity);
@@ -1738,12 +1564,6 @@ var SampleRing = class {
1738
1564
  this.head = (this.head + 1) % this.capacity;
1739
1565
  if (this.size < this.capacity) this.size++;
1740
1566
  }
1741
- /**
1742
- * Drop oldest entries whose timestamp is below `cutoff`, but never shrink
1743
- * below `minKeep`. Mirrors the original
1744
- * `while (samples.length > minKeep && samples[0].t < cutoff) samples.shift()`
1745
- * but without copying — just advances the logical tail.
1746
- */
1747
1567
  pruneBefore(cutoff, minKeep) {
1748
1568
  while (this.size > minKeep) {
1749
1569
  const oldestIdx = (this.head - this.size + this.capacity) % this.capacity;
@@ -1879,28 +1699,14 @@ var GestureController = class {
1879
1699
  __publicField(this, "detach", null);
1880
1700
  __publicField(this, "isDragging_", false);
1881
1701
  __publicField(this, "dragStartSize", 0);
1882
- // Only "touch" gestures are eligible for soft-keyboard dismissal —
1883
- // mouse/trackpad users don't have a keyboard to dismiss.
1884
1702
  __publicField(this, "currentPointerKind", "touch");
1885
- // One blur per drag — flipping focus every move event races the keyboard's
1886
- // own animations on iOS and makes the page jitter.
1887
1703
  __publicField(this, "keyboardDismissed", false);
1888
- // Reused payload object for the "drag" event — mutated each onMove instead
1889
- // of allocating a fresh `{size, delta}` per frame. At 60-120Hz a long drag
1890
- // would otherwise churn tens of KB through the GC. Mirrors browser
1891
- // PointerEvent semantics: consumers must NOT retain the reference across
1892
- // frames (clone if needed). See SheetEventMap.drag JSDoc.
1893
1704
  __publicField(this, "dragPayload", { size: 0, delta: 0 });
1894
1705
  this.deps = deps;
1895
1706
  }
1896
- /** True between `dragstart` and `dragend`/`onCancel`. */
1897
1707
  get isDragging() {
1898
1708
  return this.isDragging_;
1899
1709
  }
1900
- /**
1901
- * Bind pointer listeners on the handle and return a teardown. Engine pushes
1902
- * the teardown into its TeardownStack so destroy() drains it once.
1903
- */
1904
1710
  install() {
1905
1711
  if (this.detach) return this.detach;
1906
1712
  this.detach = installGestures(this.deps.handle, this.deps.mode, {
@@ -1957,11 +1763,6 @@ var GestureController = class {
1957
1763
  });
1958
1764
  return this.detach;
1959
1765
  }
1960
- /**
1961
- * Force-clear drag visual state. Engine calls from destroy() when destroyed
1962
- * mid-drag — gesture detach won't synthesize an onCancel, so we have to
1963
- * roll back the data-attribute + will-change ourselves.
1964
- */
1965
1766
  forceClearDragState() {
1966
1767
  if (!this.isDragging_) return;
1967
1768
  this.isDragging_ = false;
@@ -1980,11 +1781,6 @@ var AriaSliderWriter = class {
1980
1781
  mode === "bottom" || mode === "top" ? "vertical" : "horizontal"
1981
1782
  );
1982
1783
  }
1983
- /**
1984
- * Update slider position. `allowedIds` length determines `aria-valuemax`;
1985
- * `activeId` indexes into it for `aria-valuenow` + `aria-valuetext`.
1986
- * Empty list → strip every dynamic attr so AT skips the handle entirely.
1987
- */
1988
1784
  setValue(allowedIds, activeId) {
1989
1785
  if (allowedIds.length === 0) {
1990
1786
  this.handle.removeAttribute("aria-valuemin");
@@ -2058,9 +1854,6 @@ function resolveEngineOptions(opts, extras) {
2058
1854
  scrimMode: opts.scrimMode,
2059
1855
  scrimColor: opts.scrimColor,
2060
1856
  scrimBlur: opts.scrimBlur,
2061
- // Auto-promote pointer-events when tap-to-close is on so the click
2062
- // listener actually receives events. Caller can still force `false`
2063
- // explicitly to opt out — `??` only kicks in for `undefined`.
2064
1857
  scrimInteractive: opts.scrimInteractive ?? (opts.scrimTapToClose ? true : void 0),
2065
1858
  scrimTapToClose: opts.scrimTapToClose,
2066
1859
  scrimPreset: opts.scrimPreset,
@@ -2081,10 +1874,6 @@ function resolveEngineOptions(opts, extras) {
2081
1874
  closeOnEscape: opts.closeOnEscape,
2082
1875
  lockBodyScroll: opts.lockBodyScroll,
2083
1876
  inertSiblings: opts.inertSiblings,
2084
- // Folded in from `extras` so the lifecycle bag is fully assembled at
2085
- // the call site — engine no longer needs a spread+override at
2086
- // controller construction. When `extras` is omitted, the field stays
2087
- // undefined and the controller uses its own default predicate.
2088
1877
  shouldApplyInertSiblings: extras?.shouldApplyInertSiblings
2089
1878
  }
2090
1879
  };
@@ -2119,6 +1908,7 @@ var BottomSheetEngine = class {
2119
1908
  __publicField(this, "scrollContainer");
2120
1909
  __publicField(this, "backdrop");
2121
1910
  __publicField(this, "screenComponent");
1911
+ __publicField(this, "scrimParent", null);
2122
1912
  __publicField(this, "mode");
2123
1913
  __publicField(this, "flickVelocity");
2124
1914
  __publicField(this, "dragThreshold");
@@ -2129,40 +1919,22 @@ var BottomSheetEngine = class {
2129
1919
  __publicField(this, "lifecycle");
2130
1920
  __publicField(this, "closeOnBack");
2131
1921
  __publicField(this, "routedTo");
2132
- // Engine retains `snapPointsRaw` for the "find a closed-id" query in
2133
- // close() (raw input, not resolved geometry). Resolver owns geometry.
2134
1922
  __publicField(this, "snapPointsRaw");
2135
1923
  __publicField(this, "snaps");
2136
1924
  __publicField(this, "activeId");
2137
1925
  __publicField(this, "size", 0);
2138
- // Constructed in attach(); a plugin's install() could read `state` before
2139
- // attach completes if the constructor throws partway through. State getter
2140
- // null-guards via `this.gesture?.isDragging ?? false`.
2141
1926
  __publicField(this, "gesture");
2142
1927
  __publicField(this, "rootEl", null);
2143
1928
  __publicField(this, "destroyed", false);
2144
- // In-flight ViewTransition handle. A second snapTo while a VT is in flight
2145
- // would otherwise let Chromium reject the second's promise and flicker —
2146
- // newCycle() calls skipTransition() to abort cleanly.
2147
1929
  __publicField(this, "currentViewTransition", null);
2148
- // Each cycle captures the signal at entry; when a newer cycle aborts the
2149
- // controller, the captured signal flips to `aborted=true` and post-await
2150
- // guards short-circuit (prevents stale snap/open/close emits).
2151
1930
  __publicField(this, "currentAbort", new AbortController());
2152
- // Sub-pixel `--bs-size` deltas and <1% `--bs-progress` deltas are not
2153
- // visually distinguishable; gating CSSOM writes drops 30-50% of writes
2154
- // during the spring's settle tail.
2155
1931
  __publicField(this, "sizeWriteSentinel", new WriteSentinel());
2156
1932
  __publicField(this, "progressWriteSentinel", new WriteSentinel());
2157
1933
  __publicField(this, "isTopSheet", true);
2158
- // Pooled payload — identity documented in SheetEventMap.progress JSDoc;
2159
- // consumers must not retain across frames.
2160
1934
  __publicField(this, "progressPayload", {
2161
1935
  value: 0,
2162
1936
  size: 0
2163
1937
  });
2164
- // Pooled buffer for `getDragContext()` — onMove calls this 60-120×/s.
2165
- // Inner `range` object identity is also stable (mutated in place).
2166
1938
  __publicField(this, "dragContextBuf", {
2167
1939
  size: 0,
2168
1940
  maxAxisSize: 0,
@@ -2173,13 +1945,7 @@ var BottomSheetEngine = class {
2173
1945
  __publicField(this, "persistKey");
2174
1946
  __publicField(this, "linkedSheets", []);
2175
1947
  __publicField(this, "scrollCache");
2176
- // Single LIFO stack for ALL destroy-only teardowns. Plugins (added last)
2177
- // tear down first before the infrastructure they depend on. Toggle-semantic
2178
- // listeners (focus-trap / scroll-lock / scrim tap-to-close) stay as named
2179
- // fields on their controllers.
2180
1948
  __publicField(this, "teardowns", new TeardownStack());
2181
- // Pre-computed transform template — built once per engine since `mode` is
2182
- // fixed for its lifetime. Avoids the 4-arm switch at 60-120 Hz.
2183
1949
  __publicField(this, "transformTemplate");
2184
1950
  const resolved = resolveEngineOptions(opts, {
2185
1951
  shouldApplyInertSiblings: () => {
@@ -2194,7 +1960,8 @@ var BottomSheetEngine = class {
2194
1960
  this.handle = opts.handle ?? opts.element;
2195
1961
  this.scrollContainer = opts.scrollContainer;
2196
1962
  this.backdrop = opts.backdrop;
2197
- this.screenComponent = opts.screenComponent;
1963
+ this.screenComponent = opts.scrim ?? opts.screenComponent;
1964
+ this.scrimParent = this.screenComponent?.parentElement ?? null;
2198
1965
  this.mode = resolved.mode;
2199
1966
  this.transformTemplate = buildTransformTemplate(this.mode);
2200
1967
  this.aria = new AriaSliderWriter(this.handle, this.mode);
@@ -2222,8 +1989,6 @@ var BottomSheetEngine = class {
2222
1989
  getRootEl: () => this.rootEl,
2223
1990
  applySize: (size) => this.applySize(size),
2224
1991
  getSize: () => this.size,
2225
- // Optional chain protects synthetic test paths that drive animateTo
2226
- // before attach() (gesture is constructed there).
2227
1992
  isDragging: () => this.gesture?.isDragging ?? false
2228
1993
  },
2229
1994
  resolved.animation
@@ -2328,21 +2093,6 @@ var BottomSheetEngine = class {
2328
2093
  on(event, fn) {
2329
2094
  return this.bus.on(event, fn);
2330
2095
  }
2331
- /**
2332
- * Register a plugin. `install` runs synchronously; if it returns a function,
2333
- * it's captured for teardown during `destroy()`. No-op if already destroyed
2334
- * — installing into a torn-down engine would leak listeners.
2335
- *
2336
- * Throws from `plugin.install` are caught and rethrown via `queueMicrotask`
2337
- * so a buggy plugin can't take the engine down with it: the engine stays
2338
- * usable, sibling plugins still install, and the error surfaces in dev tools
2339
- * on the next tick instead of being swallowed. Mirrors the per-fn isolation
2340
- * model used for teardowns.
2341
- *
2342
- * Plugins that accept the second `scope` argument get transactional cleanup:
2343
- * push partial-install cleanups via `scope.add(fn)` and the engine drains
2344
- * them on throw, OR merges them into destroy-time teardowns on success.
2345
- */
2346
2096
  use(plugin) {
2347
2097
  if (this.destroyed) {
2348
2098
  console.warn(
@@ -2352,9 +2102,6 @@ var BottomSheetEngine = class {
2352
2102
  }
2353
2103
  const scoped = [];
2354
2104
  const scope = {
2355
- // typeof check — not just truthiness — so a JS-only plugin pushing a
2356
- // bogus value is silently rejected instead of queueing a confusing
2357
- // TypeError on destroy() drain.
2358
2105
  add: (fn) => {
2359
2106
  if (typeof fn === "function") scoped.push(fn);
2360
2107
  }
@@ -2383,36 +2130,13 @@ var BottomSheetEngine = class {
2383
2130
  }
2384
2131
  return this;
2385
2132
  }
2386
- /**
2387
- * Replace the linked-sheets list at runtime. Use when peer engines aren't
2388
- * available at construction time (mounted later in the tree). The wiring
2389
- * to the `open` event is independent of this list — it reads `linkedSheets`
2390
- * lazily on emit, so this setter is a simple field swap.
2391
- */
2392
2133
  setLinkedSheets(sheets) {
2393
2134
  if (this.destroyed) return;
2394
2135
  this.linkedSheets = sheets;
2395
2136
  }
2396
- /** Read-only view of the current allow-list, for cross-sheet target negotiation. */
2397
2137
  getAllowedIds() {
2398
2138
  return this.snaps.getAllowedIds().slice();
2399
2139
  }
2400
- /**
2401
- * Animate to a snap point by id. Resolves when the animation completes.
2402
- *
2403
- * Two call shapes:
2404
- * ```ts
2405
- * engine.snapTo("full"); // default velocity
2406
- * engine.snapTo("full", 0.5); // px/ms initial velocity
2407
- * engine.snapTo("full", { velocity: 0.5, signal }); // abortable
2408
- * ```
2409
- *
2410
- * When `opts.signal` is provided, calling `controller.abort()` cancels the
2411
- * in-flight tween/spring and the returned promise resolves without firing
2412
- * `snap`. The captured `AbortSignal` is checked after each await so external
2413
- * aborts ride the same cycle invariant as internal ones (resize, destroy,
2414
- * concurrent snapTo).
2415
- */
2416
2140
  async snapTo(id, velocityOrOpts = 0, _skipBeforeSnap = false) {
2417
2141
  if (this.destroyed) return;
2418
2142
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2471,16 +2195,6 @@ var BottomSheetEngine = class {
2471
2195
  this.handleClose();
2472
2196
  }
2473
2197
  }
2474
- /**
2475
- * Drive the sheet to an arbitrary px size, bypassing snap matching.
2476
- * Useful for preview scrubs, animation chaining, or programmatic gestures.
2477
- * Animates with the configured spring/tween. Resolves when the animation
2478
- * completes. Does NOT update `activeId` — call `snapTo(id)` after to commit.
2479
- *
2480
- * Unlike `snapTo`, this does not fire `before-snap` (it isn't a snap commit)
2481
- * nor `snap` (the sheet isn't settling to a known snap). `progress` events
2482
- * still fire via applySize. Target is clamped to `[0, maxAxisSize]`.
2483
- */
2484
2198
  async dragTo(targetSize, velocityOrOpts = 0) {
2485
2199
  if (this.destroyed) return;
2486
2200
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2518,20 +2232,10 @@ var BottomSheetEngine = class {
2518
2232
  this.snaps.setAllowedIds(ids);
2519
2233
  this.updateAriaSlider();
2520
2234
  if (snap && ids.includes(snap)) {
2521
- void this.snapTo(
2522
- snap,
2523
- 0,
2524
- /* internal */
2525
- true
2526
- );
2235
+ void this.snapTo(snap, 0, true);
2527
2236
  } else if (!ids.includes(this.activeId)) {
2528
2237
  const fallback = ids[0];
2529
- if (fallback) void this.snapTo(
2530
- fallback,
2531
- 0,
2532
- /* internal */
2533
- true
2534
- );
2238
+ if (fallback) void this.snapTo(fallback, 0, true);
2535
2239
  }
2536
2240
  }
2537
2241
  setBackdropRange(range) {
@@ -2549,21 +2253,6 @@ var BottomSheetEngine = class {
2549
2253
  setScrimInteractive(interactive) {
2550
2254
  this.scrim.setScrimInteractive(interactive);
2551
2255
  }
2552
- /**
2553
- * Batch-update multiple scrim fields in one call. Apply order is:
2554
- * 1. `preset` — fills baseline color/blur/range/interactive.
2555
- * 2. `mode` — re-positions the scrim (fixed inset / clear / hide).
2556
- * 3. `enabled` — flips the opacity contribution on/off.
2557
- * 4. `tapToClose` — installs/tears the click listener.
2558
- * 5. `color` / `blur` / `interactive` / `range` — explicit overrides.
2559
- * 6. ONE `applySize` so the per-frame opacity stays consistent.
2560
- *
2561
- * Performance: collecting changes here (vs calling individual setters)
2562
- * avoids repeated `applySize` calls during a multi-field swap. Setters
2563
- * called here suppress their own internal `applySize` either by passing
2564
- * through fast-paths (mode/enabled no-op when value unchanged) or by
2565
- * being visual-only (color/blur/interactive don't influence opacity).
2566
- */
2567
2256
  setScrim(opts) {
2568
2257
  this.scrim.setScrim(opts, () => this.applySize(this.size));
2569
2258
  }
@@ -2579,11 +2268,6 @@ var BottomSheetEngine = class {
2579
2268
  setScrimOverlay(opts) {
2580
2269
  return this.scrim.setScrimOverlay(opts);
2581
2270
  }
2582
- /**
2583
- * Read-only snapshot of scrim state. Use the `setScrim*` setters to mutate.
2584
- * Stable public API — internals (e.g. cached opacity sentinels) are NOT
2585
- * exposed so consumers can't accidentally couple to ScrimController shape.
2586
- */
2587
2271
  getScrimState() {
2588
2272
  return { mode: this.scrim.scrimMode, enabled: this.scrim.scrimEnabled };
2589
2273
  }
@@ -2629,24 +2313,11 @@ var BottomSheetEngine = class {
2629
2313
  emit(event, payload) {
2630
2314
  this.bus.emit(event, payload);
2631
2315
  }
2632
- /**
2633
- * Abort the current cycle's signal and replace the controller. Returns the
2634
- * fresh signal so the caller can capture it locally before any await — when
2635
- * a future newCycle() / destroy aborts our controller, the captured signal
2636
- * flips to aborted and post-await guards short-circuit.
2637
- */
2638
2316
  newCycle() {
2639
2317
  this.currentAbort.abort();
2640
2318
  this.currentAbort = new AbortController();
2641
2319
  return this.currentAbort.signal;
2642
2320
  }
2643
- /**
2644
- * Fire `before-snap` and report whether a listener cancelled it. The
2645
- * `cancel()` callback exposed to listeners is frozen after the synchronous
2646
- * emit phase so async callers can't silently miss — they get a console
2647
- * warning. Returns `true` when cancelled so callers can early-return /
2648
- * spring-back without re-implementing the cancel/frozen pattern.
2649
- */
2650
2321
  emitBeforeSnap(target, previousId) {
2651
2322
  let cancelled = false;
2652
2323
  let frozen = false;
@@ -2680,30 +2351,9 @@ var BottomSheetEngine = class {
2680
2351
  this.snaps.setRaw(this.snapPointsRaw);
2681
2352
  this.scrim.invalidateOpacityCache();
2682
2353
  }
2683
- /**
2684
- * Memoised `allowedRange()` — delegates to SnapResolver which owns the cache.
2685
- * Engine wrapper kept so existing call sites don't all need updating;
2686
- * resolver-level memoization survives across method invocations.
2687
- */
2688
2354
  getAllowedRange() {
2689
2355
  return this.snaps.getAllowedRange();
2690
2356
  }
2691
- /**
2692
- * Write ARIA slider value attributes onto the handle so the engine itself
2693
- * is the source of truth for screen-reader announcements. Called from the
2694
- * constructor and from every code path that mutates `activeId` /
2695
- * `allowedIds`. WAI-ARIA 1.2 requires `aria-valuenow` for `role="slider"` —
2696
- * without it screen readers can't announce drag/snap progress to keyboard
2697
- * + AT users (WCAG 4.1.2 Name, Role, Value).
2698
- *
2699
- * Engine does NOT set `role="slider"` itself — that's the adapter's or
2700
- * consumer markup's responsibility. We only decorate with dynamic values.
2701
- *
2702
- * Empty allowed-list is malformed input (settable via `setAllowed([])`);
2703
- * short-circuit to remove ARIA value attributes so screen readers see a
2704
- * no-value slider rather than a degenerate `min=max=now=0` (WAI-ARIA 1.2
2705
- * requires `valuemax > valuemin` for `role="slider"`).
2706
- */
2707
2357
  updateAriaSlider() {
2708
2358
  this.aria.setValue(this.snaps.getAllowedIds(), this.activeId);
2709
2359
  }
@@ -2728,11 +2378,6 @@ var BottomSheetEngine = class {
2728
2378
  element: this.element,
2729
2379
  mode: this.mode,
2730
2380
  getRoot: () => this.rootEl,
2731
- // Single coalesced reader — narrows the DI surface and removes 3 getter
2732
- // dispatches per onMove frame at 60-120Hz. Returns a SHARED buffer
2733
- // (this.dragContextBuf) — see field JSDoc for the retention contract.
2734
- // Inner `range` object is mutated in place too so callers caching its
2735
- // identity between frames stay valid.
2736
2381
  getDragContext: () => {
2737
2382
  const buf = this.dragContextBuf;
2738
2383
  const r = this.getAllowedRange();
@@ -2898,7 +2543,6 @@ var BottomSheetEngine = class {
2898
2543
  }
2899
2544
  });
2900
2545
  }
2901
- /** Trigger a brief haptic tick (iOS Safari + Android Chrome support). */
2902
2546
  haptic() {
2903
2547
  if (typeof navigator !== "undefined" && "vibrate" in navigator) {
2904
2548
  try {
@@ -2914,6 +2558,9 @@ var BottomSheetEngine = class {
2914
2558
  style.transform = this.transformTemplate(offset);
2915
2559
  if (this.sizeWriteSentinel.shouldWrite(size, SIZE_WRITE_EPSILON)) {
2916
2560
  style.setProperty("--bs-size", `${size}px`);
2561
+ if (this.scrimParent) {
2562
+ this.scrimParent.style.setProperty("--bs-size", `${size}px`);
2563
+ }
2917
2564
  }
2918
2565
  const progress = this.computeProgress(size);
2919
2566
  const progressChanged = this.progressWriteSentinel.shouldWrite(
@@ -2984,7 +2631,7 @@ function useBottomSheet(opts) {
2984
2631
  handle: handleRef.current ?? void 0,
2985
2632
  scrollContainer: contentRef.current ?? void 0,
2986
2633
  backdrop: backdropRef.current ?? void 0,
2987
- screenComponent: screenRef.current ?? void 0
2634
+ scrim: screenRef.current ?? void 0
2988
2635
  });
2989
2636
  engineRef.current = engine;
2990
2637
  cachedSnapshotRef.current = { ...engine.state };
@@ -3118,6 +2765,11 @@ var BottomSheet = react.forwardRef(
3118
2765
  open,
3119
2766
  close,
3120
2767
  setAllowed,
2768
+ setSnapPoints: (points, allowed) => getEngine()?.setSnapPoints(points, allowed),
2769
+ setScrim: (opts) => getEngine()?.setScrim(opts),
2770
+ setScrimOverlay: (opts) => getEngine()?.setScrimOverlay(opts) ?? (() => {
2771
+ }),
2772
+ getEngine,
3121
2773
  get state() {
3122
2774
  return getEngine()?.state ?? state;
3123
2775
  }
@@ -3137,7 +2789,7 @@ var BottomSheet = react.forwardRef(
3137
2789
  onClick: closeOnBackdrop ? () => void close() : void 0
3138
2790
  }
3139
2791
  ),
3140
- screen && /* @__PURE__ */ jsxRuntime.jsx(
2792
+ /* @__PURE__ */ jsxRuntime.jsx(
3141
2793
  "div",
3142
2794
  {
3143
2795
  ref: screenRef,
@@ -3260,8 +2912,6 @@ var OverlayEngine = class {
3260
2912
  __publicField(this, "exitEasing");
3261
2913
  __publicField(this, "backdropOpacity");
3262
2914
  __publicField(this, "backdropFilter");
3263
- // Definite-assignment: written via assignSwipeFromConfig in the
3264
- // constructor — TS can't follow the helper, mark the assertion explicit.
3265
2915
  __publicField(this, "swipeToCloseEnabled");
3266
2916
  __publicField(this, "swipeThreshold");
3267
2917
  __publicField(this, "swipeVelocityThreshold");
@@ -3281,13 +2931,7 @@ var OverlayEngine = class {
3281
2931
  __publicField(this, "isOpen_", false);
3282
2932
  __publicField(this, "isAnimating_", false);
3283
2933
  __publicField(this, "destroyed", false);
3284
- // Tracks whether setOverlayChildren ever populated the panel — lets
3285
- // clearOverlayChildren skip the DOM mutation when nothing was injected
3286
- // (so the field also acts as a zero-allocation feature flag).
3287
2934
  __publicField(this, "hasInjectedChildren", false);
3288
- // Bumped on every open/close. awaitTransition captures it and ignores
3289
- // transitionend/timeout fired by older cycles — prevents cross-cycle
3290
- // listener bleed during rapid toggling.
3291
2935
  __publicField(this, "cycleNonce", 0);
3292
2936
  __publicField(this, "bus", createEventBus());
3293
2937
  __publicField(this, "detachEscape", null);
@@ -3340,9 +2984,6 @@ var OverlayEngine = class {
3340
2984
  closeOnEscape: this.closeOnEscape,
3341
2985
  lockBodyScroll: this.bodyScrollLockEnabled,
3342
2986
  inertSiblings: this.inertSiblingsEnabled,
3343
- // Overlay-specific guard: only paint body siblings inert when the
3344
- // overlay element is actually a body descendant. Detached / shadow-
3345
- // DOM / portal overlays would otherwise lock unrelated trees.
3346
2987
  shouldApplyInertSiblings: () => {
3347
2988
  const bodyChildren = Array.from(document.body.children);
3348
2989
  return bodyChildren.some(
@@ -3496,12 +3137,6 @@ var OverlayEngine = class {
3496
3137
  this.releaseStackEntry = null;
3497
3138
  this.restoreMount();
3498
3139
  }
3499
- /**
3500
- * Runtime visual setter — opacity is applied immediately when open so a
3501
- * live-controls panel can preview the change without re-opening. When
3502
- * closed we only stash the field; next open() picks it up so we don't
3503
- * mutate hidden DOM (avoids paint thrash on detached panels).
3504
- */
3505
3140
  setBackdropOpacity(opacity) {
3506
3141
  if (this.destroyed) return;
3507
3142
  this.backdropOpacity = opacity;
@@ -3509,11 +3144,6 @@ var OverlayEngine = class {
3509
3144
  this.backdrop.style.opacity = String(opacity);
3510
3145
  }
3511
3146
  }
3512
- /**
3513
- * Runtime visual setter for `backdrop-filter`. `undefined` clears the
3514
- * filter (mirrors the close() behaviour). Defers to next open() when the
3515
- * overlay is currently closed.
3516
- */
3517
3147
  setBackdropFilter(filter) {
3518
3148
  if (this.destroyed) return;
3519
3149
  this.backdropFilter = filter;
@@ -3527,12 +3157,6 @@ var OverlayEngine = class {
3527
3157
  style.webkitBackdropFilter = "";
3528
3158
  }
3529
3159
  }
3530
- /**
3531
- * Toggle / reconfigure swipe-to-close at runtime. Re-resolves the flat
3532
- * threshold fields, then installs or tears down the pointer listener so
3533
- * the change applies to the CURRENT open cycle. Idempotent: safe to call
3534
- * with the same config repeatedly.
3535
- */
3536
3160
  setSwipeToClose(config) {
3537
3161
  if (this.destroyed) return;
3538
3162
  const wasEnabled = this.swipeToCloseEnabled;
@@ -3545,7 +3169,6 @@ var OverlayEngine = class {
3545
3169
  this.detachSwipe = null;
3546
3170
  }
3547
3171
  }
3548
- /** Animation setters only stage the field — applied on next open/close. */
3549
3172
  setEnterAnimation(animation) {
3550
3173
  if (this.destroyed) return;
3551
3174
  this.enterAnimation = animation;
@@ -3554,23 +3177,15 @@ var OverlayEngine = class {
3554
3177
  if (this.destroyed) return;
3555
3178
  this.exitAnimation = animation;
3556
3179
  }
3557
- /** Updates the field used at next close() — no immediate side effect. */
3558
3180
  setReturnFocus(target) {
3559
3181
  if (this.destroyed) return;
3560
3182
  this.lifecycle.setReturnFocus(target);
3561
3183
  }
3562
- /**
3563
- * Batch update — preset first, then individual overrides. Applied as a
3564
- * single DOM-write phase when open so live-controls don't trigger
3565
- * cascading reflows. Safe to call when closed (defers to next open).
3566
- */
3567
3184
  setOverlay(opts) {
3568
3185
  if (this.destroyed) return;
3569
3186
  const preset = opts.preset ? OVERLAY_PRESETS[opts.preset] : void 0;
3570
3187
  const merged = preset ? {
3571
3188
  backdropOpacity: preset.backdropOpacity,
3572
- // Preset's `undefined` filter means "no filter" — surface as null
3573
- // so the merged step clears the field instead of inheriting old.
3574
3189
  backdropFilter: preset.backdropFilter === void 0 ? null : preset.backdropFilter,
3575
3190
  swipeToClose: preset.swipeToClose,
3576
3191
  enterAnimation: preset.enterAnimation,
@@ -3629,30 +3244,17 @@ var OverlayEngine = class {
3629
3244
  }
3630
3245
  }
3631
3246
  }
3632
- /**
3633
- * Runtime children injection — replace the panel's contents without
3634
- * unmounting the engine. `replaceChildren()` is a single atomic DOM op,
3635
- * so callers can swap content rapidly without intermediate flicker.
3636
- * Function form is resolved exactly ONCE per call (cheap lazy content).
3637
- */
3638
3247
  setOverlayChildren(children) {
3639
3248
  if (this.destroyed) return;
3640
3249
  const node = typeof children === "function" ? children() : children;
3641
3250
  this.element.replaceChildren(node);
3642
3251
  this.hasInjectedChildren = true;
3643
3252
  }
3644
- /**
3645
- * Clear injected children. No-op when nothing was injected so consumers
3646
- * who never used setOverlayChildren can call this defensively without
3647
- * paying for a DOM mutation.
3648
- */
3649
3253
  clearOverlayChildren() {
3650
3254
  if (this.destroyed || !this.hasInjectedChildren) return;
3651
3255
  this.element.replaceChildren();
3652
3256
  this.hasInjectedChildren = false;
3653
3257
  }
3654
- // Shared swipe-config normalizer — used by constructor, setSwipeToClose,
3655
- // and setOverlay so the union/object shape is decoded in exactly one place.
3656
3258
  assignSwipeFromConfig(swipe) {
3657
3259
  if (typeof swipe === "object" && swipe !== null) {
3658
3260
  this.swipeToCloseEnabled = swipe.enabled ?? true;
@@ -3796,13 +3398,6 @@ var OverlayEngine = class {
3796
3398
  this.detachOutsidePointer = null;
3797
3399
  this.lifecycle.release();
3798
3400
  }
3799
- /**
3800
- * Light pointer-driven dismiss. Tracks finger along the edge axis, follows
3801
- * with translate, dismisses if past `swipeThreshold` (fraction of panel
3802
- * size) or release velocity exceeds `swipeVelocityThreshold` (px/ms).
3803
- * Pure CSS-transform during drag — no rAF or physics required since the
3804
- * panel only moves one direction.
3805
- */
3806
3401
  installSwipeToClose() {
3807
3402
  const el = this.element;
3808
3403
  const edge = this.edge;