@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/qwik.js CHANGED
@@ -231,11 +231,6 @@ var SnapResolver = class {
231
231
  this.allowedIds = allowed ?? raw.map((p) => p.id);
232
232
  this.recompute();
233
233
  }
234
- /**
235
- * Re-resolve raw → resolved against the current viewport / fit-measure,
236
- * recompute maxAxisSize, invalidate range cache, notify engine. Called on
237
- * resize, orientationchange, visualViewport changes, and snap-list swaps.
238
- */
239
234
  recompute() {
240
235
  this.resolved = resolveSnapList(this.raw, this.mode, this.measureFit);
241
236
  this.maxAxisSize = this.resolved.reduce(
@@ -245,15 +240,10 @@ var SnapResolver = class {
245
240
  this.rangeCache = null;
246
241
  this.onMaxAxisSizeChange?.(this.maxAxisSize);
247
242
  }
248
- /** Replace the raw snap list and recompute. Engine calls on `setSnapPoints`. */
249
243
  setRaw(raw) {
250
244
  this.raw = raw;
251
245
  this.recompute();
252
246
  }
253
- /**
254
- * Replace the allow-list. Range cache depends on `(resolvedSnaps ×
255
- * allowedIds)` so flipping ids alone invalidates without a full recompute.
256
- */
257
247
  setAllowedIds(ids) {
258
248
  this.allowedIds = ids;
259
249
  this.rangeCache = null;
@@ -261,11 +251,6 @@ var SnapResolver = class {
261
251
  findById(id) {
262
252
  return findById(id, this.resolved);
263
253
  }
264
- /**
265
- * Pure drag-settle target resolution. Engine passes the gesture-specific
266
- * fields (delta, velocity, pointerKind, flickVelocity, dragThreshold) plus
267
- * `size`/`activeId` since those are still engine-owned.
268
- */
269
254
  findDragSettleTarget(args) {
270
255
  return findDragSettleTarget({
271
256
  ...args,
@@ -274,18 +259,9 @@ var SnapResolver = class {
274
259
  maxAxisSize: this.maxAxisSize
275
260
  });
276
261
  }
277
- /**
278
- * Memoised allowed-range query. Twice-per-drag-frame (gesture onMove +
279
- * applySize→computeProgress); cache collapses 2× iteration of resolvedSnaps
280
- * to one recompute per geometry/allow-list change.
281
- */
282
262
  getAllowedRange() {
283
263
  return this.rangeCache ?? (this.rangeCache = allowedRange(this.resolved, this.allowedIds));
284
264
  }
285
- /**
286
- * Read-only view of resolved snaps. Returned as `readonly` to discourage
287
- * accidental mutation by feature install sites that close over this list.
288
- */
289
265
  getResolvedSnaps() {
290
266
  return this.resolved;
291
267
  }
@@ -295,18 +271,6 @@ var SnapResolver = class {
295
271
  getMaxAxisSize() {
296
272
  return this.maxAxisSize;
297
273
  }
298
- /**
299
- * External clamp override. The resize-observer feature compares
300
- * `getMaxAxisSize()` against the live viewport and shrinks if the largest
301
- * resolved snap exceeds it (mobile address-bar collapse / soft-keyboard
302
- * window resize). The next `recompute()` overwrites this clamp from the
303
- * resolved-snap reduction.
304
- *
305
- * Does NOT invalidate the range cache — viewport clamping doesn't change
306
- * `(resolvedSnaps × allowedIds) → range`. DOES fire `onMaxAxisSizeChange`
307
- * so the resolver owns the layout-axis style write end-to-end (symmetric
308
- * with `recompute()` — feature call sites no longer duplicate the write).
309
- */
310
274
  setMaxAxisSize(n) {
311
275
  this.maxAxisSize = n;
312
276
  this.onMaxAxisSizeChange?.(n);
@@ -681,73 +645,49 @@ var WriteSentinel = class {
681
645
  constructor() {
682
646
  __publicField(this, "last", -1);
683
647
  }
684
- /**
685
- * Returns `true` (and updates the cached value) if `next` differs from the
686
- * cached `last` by more than `epsilon`. Returns `false` (and leaves the
687
- * cache untouched) when the delta is within epsilon — caller skips the
688
- * CSSOM write.
689
- */
690
648
  shouldWrite(next, epsilon) {
691
649
  if (Math.abs(next - this.last) <= epsilon) return false;
692
650
  this.last = next;
693
651
  return true;
694
652
  }
695
- /**
696
- * Force the next `shouldWrite` to pass. Resets the cache to `-1` so any
697
- * non-negative `next` exceeds the epsilon comparison. Called from setters
698
- * that mutate state under the cache (range/mode/enabled flips that change
699
- * the progress→opacity mapping but don't move `progress` itself).
700
- */
701
653
  invalidate() {
702
654
  this.last = -1;
703
655
  }
704
- /**
705
- * Record `value` as the last-written without performing a write. Used
706
- * when a setter writes a literal value through a different path (e.g.
707
- * `setScrimMode("off")` writes `opacity: "0"` inline) and wants future
708
- * `shouldWrite` calls to short-circuit against that known cached state.
709
- */
710
656
  setLastWritten(value) {
711
657
  this.last = value;
712
658
  }
713
- /** Read-only accessor for diagnostic / dedup-aware peek. */
714
659
  get value() {
715
660
  return this.last;
716
661
  }
717
662
  };
718
663
 
664
+ // src/core/primitives/devWarn.ts
665
+ function devWarn(...args) {
666
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && typeof console !== "undefined") {
667
+ console.warn(...args);
668
+ }
669
+ }
670
+
719
671
  // src/core/controllers/scrim-controller.ts
672
+ var VALID_CSS_LENGTH = /^-?\d+(?:\.\d+)?(?:px|em|rem|%)$/;
720
673
  var ScrimController = class {
721
674
  constructor(deps, opts) {
722
- // ── DOM refs (mirror engine's; cached for hot path) ─────────────────
723
675
  __publicField(this, "screenComponent");
724
676
  __publicField(this, "backdrop");
725
677
  __publicField(this, "mode");
726
- // ── Engine callbacks ────────────────────────────────────────────────
727
678
  __publicField(this, "isDestroyed");
728
679
  __publicField(this, "isTopSheet");
729
680
  __publicField(this, "getAllowedIds");
730
681
  __publicField(this, "getResolvedSnaps");
731
682
  __publicField(this, "snapToFn");
732
- // ── Mode + state ────────────────────────────────────────────────────
733
683
  __publicField(this, "scrimMode");
734
684
  __publicField(this, "backdropRange");
735
685
  __publicField(this, "screenRange");
736
- // setScrimEnabled toggle — gated through `scrimEnabled` rather than nuking
737
- // ranges so re-enable can restore prior values without a remount. Public
738
- // so tests / introspection can read it without an `as any` cast.
739
686
  __publicField(this, "scrimEnabled", true);
740
687
  __publicField(this, "savedScrimRanges", null);
741
- // ── Tap-to-close ────────────────────────────────────────────────────
742
688
  __publicField(this, "scrimTapToCloseEnabled", false);
743
689
  __publicField(this, "detachScrimTap", null);
744
- // ── Scrim overlay slot (positioned floating element) ────────────────
745
690
  __publicField(this, "scrimOverlayTeardown", null);
746
- // ── Hot-path opacity dedup caches (per-frame writes) ────────────────
747
- // Numeric sentinels migrated to WriteSentinel primitive — single
748
- // invalidation surface, shared with the engine's size/progress sentinels.
749
- // The two string-typed sentinels (`Pointer`, `Display`) keep their inline
750
- // pattern: equality on string union, no epsilon, different reset semantics.
751
691
  __publicField(this, "backdropOpacitySentinel", new WriteSentinel());
752
692
  __publicField(this, "screenOpacitySentinel", new WriteSentinel());
753
693
  __publicField(this, "lastBackdropPointer", "");
@@ -782,41 +722,15 @@ var ScrimController = class {
782
722
  }
783
723
  }
784
724
  }
785
- /**
786
- * Invalidate the opacity dedup caches. Called from any setter that mutates
787
- * range/mode/enabled — opacity is a pure function of (progress, range,
788
- * enabled), so a config change makes the cached values stale even if
789
- * `progress` itself didn't move. Next `applyOpacity(progress, false)` call
790
- * sees the -1 sentinel and re-runs the math instead of short-circuiting.
791
- *
792
- * Public for the engine: `setSnapPoints` mutates resolved geometry which
793
- * shifts `progress→opacity` mapping under the cache; engine calls this to
794
- * keep the next render frame correct on a static sheet.
795
- * @internal
796
- */
797
725
  invalidateOpacityCache() {
798
726
  this.backdropOpacitySentinel.invalidate();
799
727
  this.screenOpacitySentinel.invalidate();
800
728
  }
801
- /** Engine calls this when its `attach()` runs — installs scrim-tap-click if requested. */
802
729
  attach() {
803
730
  if (this.scrimTapToCloseEnabled) {
804
731
  this.setScrimTapToClose(true);
805
732
  }
806
733
  }
807
- // ── Hot-path: opacity computation called per-frame from engine.applySize ──
808
- /**
809
- * Per-frame opacity writer. Computes backdrop + screen opacity from the
810
- * sheet's progress (0..1 within allowed range), gated by sub-pixel dedup
811
- * caches so steady-state frames don't trigger style recalc.
812
- *
813
- * `progressChanged` is the engine's already-computed dedup gate — when
814
- * `false`, opacity is by construction a pure function of `progress` so
815
- * cached values are still valid; we short-circuit the Math.min/max +
816
- * division work entirely. Setters that mutate ranges/mode/enabled
817
- * invalidate the cache by writing -1 sentinels so the next call re-runs
818
- * the math.
819
- */
820
734
  applyOpacity(progress, progressChanged) {
821
735
  if (!progressChanged && this.backdropOpacitySentinel.value !== -1 && this.screenOpacitySentinel.value !== -1) {
822
736
  return;
@@ -862,80 +776,30 @@ var ScrimController = class {
862
776
  }
863
777
  }
864
778
  }
865
- // ── Public setters (engine forwards these 1:1) ─────────────────────
866
- /**
867
- * Replace the [start, end] progress window in which the backdrop fades 0→1.
868
- * @param range - Tuple of two progress values in 0..1 (start <= end).
869
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
870
- * @example
871
- * // Backdrop only starts darkening after the sheet crosses 30% open.
872
- * engine.setBackdropRange([0.3, 1]);
873
- */
874
779
  setBackdropRange(range, applySize) {
875
780
  if (this.isDestroyed()) return;
876
781
  this.backdropRange = range;
877
782
  this.invalidateOpacityCache();
878
783
  applySize();
879
784
  }
880
- /**
881
- * Replace the [start, end] progress window in which the screen scrim fades 0→1.
882
- * @param range - Tuple of two progress values in 0..1 (start <= end).
883
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
884
- * @example
885
- * // Scrim is fully opaque only when the sheet is past 50% — softer feel.
886
- * engine.setScreenRange([0, 0.5]);
887
- */
888
785
  setScreenRange(range, applySize) {
889
786
  if (this.isDestroyed()) return;
890
787
  this.screenRange = range;
891
788
  this.invalidateOpacityCache();
892
789
  applySize();
893
790
  }
894
- /**
895
- * Set the scrim background color. Pass `null`/`undefined` to clear the
896
- * inline write and let consumer CSS take over.
897
- * @param color - Any valid CSS color string, or `null`/`undefined` to clear.
898
- * @example
899
- * engine.setScrimColor("rgba(0, 0, 0, 0.6)");
900
- * engine.setScrimColor(null); // hand back to consumer CSS
901
- */
902
791
  setScrimColor(color) {
903
792
  if (this.isDestroyed()) return;
904
793
  this.applyScrimStyles({ color: color === void 0 ? null : color });
905
794
  }
906
- /**
907
- * Set the scrim backdrop-filter blur radius. Pass `null`/`undefined` to clear.
908
- * @param blur - CSS length (e.g. `"8px"`) consumed as `blur(<value>)`, or
909
- * `null`/`undefined` to remove the filter.
910
- * @example
911
- * engine.setScrimBlur("12px"); // frosted-glass effect
912
- * engine.setScrimBlur(null); // disable blur
913
- */
914
795
  setScrimBlur(blur) {
915
796
  if (this.isDestroyed()) return;
916
797
  this.applyScrimStyles({ blur: blur === void 0 ? null : blur });
917
798
  }
918
- /**
919
- * Toggle whether the scrim layer captures pointer events.
920
- * @param interactive - `true` → `pointer-events: auto`; `false` → `none`.
921
- * @example
922
- * engine.setScrimInteractive(true); // scrim swallows clicks
923
- * engine.setScrimInteractive(false); // clicks pass through to content below
924
- */
925
799
  setScrimInteractive(interactive) {
926
800
  if (this.isDestroyed()) return;
927
801
  this.applyScrimStyles({ interactive });
928
802
  }
929
- /**
930
- * Switch scrim positioning at runtime.
931
- * @param mode - `"full"` (consumer CSS), `"above-sheet"` (engine-pinned inset
932
- * so only the area NOT covered by the sheet is dimmed), or
933
- * `"off"` (display:none + opacity:0 — no remount needed).
934
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
935
- * @example
936
- * engine.setScrimMode("above-sheet"); // dim only the area above the sheet
937
- * engine.setScrimMode("off"); // disable scrim layer
938
- */
939
803
  setScrimMode(mode, applySize) {
940
804
  if (this.isDestroyed()) return;
941
805
  if (mode === this.scrimMode) return;
@@ -965,15 +829,6 @@ var ScrimController = class {
965
829
  }
966
830
  applySize();
967
831
  }
968
- /**
969
- * Enable or disable click-on-scrim → snap to first non-zero allowed snap.
970
- * Auto-promotes `pointer-events: auto` when scrim is non-interactive so the
971
- * click listener actually fires.
972
- * @param enabled - `true` installs the click listener; `false` removes it.
973
- * @example
974
- * engine.setScrimTapToClose(true); // tap outside sheet collapses it
975
- * engine.setScrimTapToClose(false); // require explicit close button
976
- */
977
832
  setScrimTapToClose(enabled) {
978
833
  if (this.isDestroyed()) return;
979
834
  if (!this.screenComponent) return;
@@ -1003,16 +858,6 @@ var ScrimController = class {
1003
858
  this.scrimTapToCloseEnabled = false;
1004
859
  }
1005
860
  }
1006
- /**
1007
- * Toggle the scrim layer without remounting. Disabling stashes current
1008
- * `screenRange`/`backdropRange` and degenerates them to `[1, 1]` (opacity
1009
- * formula → 0 across the whole range); re-enabling restores the saved values.
1010
- * @param enabled - `false` hides scrim, `true` restores prior ranges.
1011
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
1012
- * @example
1013
- * engine.setScrimEnabled(false); // hide scrim, ranges remembered
1014
- * engine.setScrimEnabled(true); // restore prior ranges (no remount)
1015
- */
1016
861
  setScrimEnabled(enabled, applySize) {
1017
862
  if (this.isDestroyed()) return;
1018
863
  if (enabled === this.scrimEnabled) return;
@@ -1031,20 +876,6 @@ var ScrimController = class {
1031
876
  }
1032
877
  applySize();
1033
878
  }
1034
- /**
1035
- * Batch-update multiple scrim fields with a single applySize() at the end.
1036
- * If `preset` is set, it seeds color/blur/interactive/range first; explicit
1037
- * fields override.
1038
- * @param opts - Partial scrim config: preset, mode, enabled, tapToClose,
1039
- * color, blur, interactive, range.
1040
- * @param applySize - Engine-supplied recompute hook; runs ONCE per call.
1041
- * @example
1042
- * engine.setScrim({
1043
- * preset: "frosted",
1044
- * mode: "above-sheet",
1045
- * tapToClose: true,
1046
- * });
1047
- */
1048
879
  setScrim(opts, applySize) {
1049
880
  if (this.isDestroyed()) return;
1050
881
  if (opts.preset) {
@@ -1067,27 +898,18 @@ var ScrimController = class {
1067
898
  interactive: opts.interactive
1068
899
  });
1069
900
  if (opts.range !== void 0) {
1070
- this.screenRange = opts.range;
901
+ if (!this.scrimEnabled && this.savedScrimRanges) {
902
+ this.savedScrimRanges = {
903
+ screen: opts.range,
904
+ backdrop: this.savedScrimRanges.backdrop
905
+ };
906
+ } else {
907
+ this.screenRange = opts.range;
908
+ }
1071
909
  }
1072
910
  this.invalidateOpacityCache();
1073
911
  applySize();
1074
912
  }
1075
- /**
1076
- * Inject a positioned floating element into the scrim layer (e.g. a close
1077
- * button anchored top-right). Replaces any prior overlay so repeated calls
1078
- * (reactive effects re-running) don't accumulate wrappers.
1079
- * @param opts - Children element, optional `inset` (default `"16px"`),
1080
- * `position` (9-way grid, default `"top-right"`), and
1081
- * `interactive` flag (default `true`).
1082
- * @returns Teardown function that removes the wrapper from the DOM.
1083
- * @example
1084
- * const teardown = engine.setScrimOverlay({
1085
- * children: closeBtn,
1086
- * position: "top-right",
1087
- * inset: "20px",
1088
- * });
1089
- * // later: teardown(); // remove the floating button
1090
- */
1091
913
  setScrimOverlay(opts) {
1092
914
  if (this.isDestroyed()) return () => {
1093
915
  };
@@ -1095,7 +917,18 @@ var ScrimController = class {
1095
917
  };
1096
918
  this.scrimOverlayTeardown?.();
1097
919
  const screen = this.screenComponent;
1098
- const inset = opts.inset ?? "16px";
920
+ const insetRaw = opts.inset ?? "16px";
921
+ const inset = VALID_CSS_LENGTH.test(insetRaw) ? insetRaw : "16px";
922
+ if (inset !== insetRaw) {
923
+ devWarn(
924
+ `[bottom-sheet] setScrimOverlay: invalid inset ${JSON.stringify(insetRaw)}; expected a CSS length like "16px" / "1rem" / "5%". Falling back to "16px".`
925
+ );
926
+ }
927
+ if (opts.children.ownerDocument && opts.children.ownerDocument !== screen.ownerDocument) {
928
+ devWarn(
929
+ "[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."
930
+ );
931
+ }
1099
932
  const position = opts.position ?? "top-right";
1100
933
  const interactive = opts.interactive ?? true;
1101
934
  const wrapper = screen.ownerDocument.createElement("div");
@@ -1144,12 +977,30 @@ var ScrimController = class {
1144
977
  ws.bottom = inset;
1145
978
  ws.right = inset;
1146
979
  break;
980
+ case "sheet-top-left":
981
+ case "sheet-top-center":
982
+ case "sheet-top-right":
983
+ applySheetAnchoredPosition(
984
+ ws,
985
+ this.mode,
986
+ position,
987
+ inset
988
+ );
989
+ break;
1147
990
  }
1148
991
  if (interactive) {
1149
992
  ws.pointerEvents = "auto";
1150
993
  }
1151
994
  wrapper.appendChild(opts.children);
1152
- screen.appendChild(wrapper);
995
+ const host = screen.parentElement;
996
+ if (!host) {
997
+ devWarn(
998
+ "[bottom-sheet] setScrimOverlay: scrim element has no parent, skipping overlay injection. Mount the sheet root before calling setScrimOverlay."
999
+ );
1000
+ return () => {
1001
+ };
1002
+ }
1003
+ host.appendChild(wrapper);
1153
1004
  const teardown = () => {
1154
1005
  if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
1155
1006
  if (this.scrimOverlayTeardown === teardown) {
@@ -1159,7 +1010,6 @@ var ScrimController = class {
1159
1010
  this.scrimOverlayTeardown = teardown;
1160
1011
  return teardown;
1161
1012
  }
1162
- /** Tear down toggle-state listeners + remove any injected overlay wrapper. */
1163
1013
  destroy() {
1164
1014
  this.detachScrimTap?.();
1165
1015
  this.detachScrimTap = null;
@@ -1178,8 +1028,6 @@ var ScrimController = class {
1178
1028
  this.lastBackdropPointer = "";
1179
1029
  this.lastScreenDisplay = "";
1180
1030
  }
1181
- // ── Private helpers ────────────────────────────────────────────────
1182
- /** Centralized scrim style writer — atomic 3-property write across all setters. */
1183
1031
  applyScrimStyles(opts) {
1184
1032
  if (!this.screenComponent) return;
1185
1033
  const s = this.screenComponent.style;
@@ -1201,7 +1049,6 @@ var ScrimController = class {
1201
1049
  s.pointerEvents = opts.interactive ? "auto" : "none";
1202
1050
  }
1203
1051
  }
1204
- /** Pin the screen overlay to the area NOT occupied by the sheet. */
1205
1052
  applyAboveSheetInset() {
1206
1053
  if (!this.screenComponent) return;
1207
1054
  const insetByMode = {
@@ -1216,6 +1063,35 @@ var ScrimController = class {
1216
1063
  s.pointerEvents = "none";
1217
1064
  }
1218
1065
  };
1066
+ function resolveSheetAnchoredStyle(mode, position, inset) {
1067
+ const sizeOffset = `calc(var(--bs-size, 0px) + ${inset})`;
1068
+ switch (mode) {
1069
+ case "bottom":
1070
+ if (position === "sheet-top-left") return { bottom: sizeOffset, left: inset };
1071
+ if (position === "sheet-top-right") return { bottom: sizeOffset, right: inset };
1072
+ return { bottom: sizeOffset, left: "50%", transform: "translateX(-50%)" };
1073
+ case "top":
1074
+ if (position === "sheet-top-left") return { top: sizeOffset, left: inset };
1075
+ if (position === "sheet-top-right") return { top: sizeOffset, right: inset };
1076
+ return { top: sizeOffset, left: "50%", transform: "translateX(-50%)" };
1077
+ case "left":
1078
+ if (position === "sheet-top-left") return { left: sizeOffset, top: inset };
1079
+ if (position === "sheet-top-right") return { left: sizeOffset, bottom: inset };
1080
+ return { left: sizeOffset, top: "50%", transform: "translateY(-50%)" };
1081
+ case "right":
1082
+ if (position === "sheet-top-left") return { right: sizeOffset, top: inset };
1083
+ if (position === "sheet-top-right") return { right: sizeOffset, bottom: inset };
1084
+ return { right: sizeOffset, top: "50%", transform: "translateY(-50%)" };
1085
+ }
1086
+ }
1087
+ function applySheetAnchoredPosition(ws, mode, position, inset) {
1088
+ const style = resolveSheetAnchoredStyle(mode, position, inset);
1089
+ if (style.top !== void 0) ws.top = style.top;
1090
+ if (style.bottom !== void 0) ws.bottom = style.bottom;
1091
+ if (style.left !== void 0) ws.left = style.left;
1092
+ if (style.right !== void 0) ws.right = style.right;
1093
+ if (style.transform !== void 0) ws.transform = style.transform;
1094
+ }
1219
1095
 
1220
1096
  // src/core/animation/animation.ts
1221
1097
  var easeOutBack = (t) => {
@@ -1368,13 +1244,9 @@ var AnimationRunner = class {
1368
1244
  __publicField(this, "duration");
1369
1245
  __publicField(this, "easing");
1370
1246
  __publicField(this, "respectReducedMotion");
1371
- /** Live-tracked via mq listener so animateTo skips matchMedia per snap. */
1372
1247
  __publicField(this, "reducedMotion", false);
1373
- /** Disposes the matchMedia change listener — drained from destroy(). */
1374
1248
  __publicField(this, "detachReducedMotion", null);
1375
- /** Computed once — both inputs (option + API presence) stable for life. */
1376
1249
  __publicField(this, "viewTransitionsAvailable");
1377
- /** In-flight handles. Public for tests / debugging only — engine drives via cancel(). */
1378
1250
  __publicField(this, "currentTween", null);
1379
1251
  __publicField(this, "currentSpring", null);
1380
1252
  this.element = deps.element;
@@ -1405,26 +1277,13 @@ var AnimationRunner = class {
1405
1277
  }
1406
1278
  }
1407
1279
  }
1408
- /**
1409
- * Live derived flag — `true` whenever a spring/tween handle is in flight.
1410
- * Reading the canonical handles (instead of mirroring into a separate
1411
- * boolean) eliminates the reduced-motion early-return latent bug where
1412
- * the flag stayed true forever, and removes the cross-controller
1413
- * setIsAnimating callback.
1414
- */
1415
1280
  get isAnimating() {
1416
1281
  return this.currentTween !== null || this.currentSpring !== null;
1417
1282
  }
1418
- /** Cancel any in-flight tween/spring without touching engine state. */
1419
1283
  cancel() {
1420
1284
  this.currentTween?.cancel();
1421
1285
  this.currentSpring?.cancel();
1422
1286
  }
1423
- /**
1424
- * Animate from current size to `target`. Resolves when the animation
1425
- * completes; resolves early if cancelled mid-flight (caller checks an
1426
- * abort signal post-await for cycle-invariant correctness).
1427
- */
1428
1287
  async animateTo(target, velocityPxPerMs) {
1429
1288
  if (target === this.getSize()) return;
1430
1289
  this.currentTween?.cancel();
@@ -1465,7 +1324,6 @@ var AnimationRunner = class {
1465
1324
  if (!this.isDragging()) this.element.style.willChange = "auto";
1466
1325
  }
1467
1326
  }
1468
- /** Tear down the matchMedia listener. Engine calls from destroy(). */
1469
1327
  destroy() {
1470
1328
  this.detachReducedMotion?.();
1471
1329
  this.detachReducedMotion = null;
@@ -1625,12 +1483,8 @@ var LifecycleController = class {
1625
1483
  __publicField(this, "inertSiblings");
1626
1484
  __publicField(this, "shouldApplyInertSiblings");
1627
1485
  __publicField(this, "returnFocus");
1628
- /** Toggle-state teardowns — fire on close, NOT in destroy-only stack. */
1629
1486
  __publicField(this, "releaseFocusTrap", null);
1630
1487
  __publicField(this, "releaseScrollLock", null);
1631
- /** True between install() and release(). Guards release()/destroy() from
1632
- * re-running side effects (notably `returnFocus.focus()`) on already-
1633
- * released or never-installed instances. */
1634
1488
  __publicField(this, "installed", false);
1635
1489
  __publicField(this, "destroyed", false);
1636
1490
  this.element = deps.element;
@@ -1644,14 +1498,6 @@ var LifecycleController = class {
1644
1498
  this.returnFocus = opts.returnFocus;
1645
1499
  this.inertSiblings = createInertSiblings(() => this.element);
1646
1500
  }
1647
- /**
1648
- * Run the open-time install steps. Idempotent: each step checks its
1649
- * existing teardown ref so a re-open during a half-cancelled close
1650
- * doesn't double-install. Sets `installed=true` BEFORE the first side
1651
- * effect so a partial throw (detached / shadow-DOM target) leaves the
1652
- * caller's `release()` rollback path armed to undo whatever did succeed.
1653
- * No-op when destroyed.
1654
- */
1655
1501
  install() {
1656
1502
  if (this.destroyed) return;
1657
1503
  this.installed = true;
@@ -1668,22 +1514,10 @@ var LifecycleController = class {
1668
1514
  this.inertSiblings.apply();
1669
1515
  }
1670
1516
  }
1671
- /**
1672
- * Update the post-release focus target. Overlay re-binds this when the
1673
- * consumer changes `returnFocus` after construction; the new value applies
1674
- * on the next `release()`. No-op when destroyed.
1675
- */
1676
1517
  setReturnFocus(target) {
1677
1518
  if (this.destroyed) return;
1678
1519
  this.returnFocus = target;
1679
1520
  }
1680
- /**
1681
- * Release all install-time teardowns. Short-circuits when not installed,
1682
- * so `close() → destroy()` (each routing through here) only restores
1683
- * focus once. The `installed` guard lets `inertSiblings.remove()` and
1684
- * the focus-trap/scroll-lock teardown nullings stay simple — they were
1685
- * already idempotent, but `returnFocus.focus()` was firing on every call.
1686
- */
1687
1521
  release() {
1688
1522
  if (!this.installed) return;
1689
1523
  this.installed = false;
@@ -1697,12 +1531,6 @@ var LifecycleController = class {
1697
1531
  target?.focus?.();
1698
1532
  }
1699
1533
  }
1700
- /**
1701
- * Final teardown — releases any still-installed listeners and seals the
1702
- * controller. After destroy(), install() is a no-op and setReturnFocus()
1703
- * is rejected, so post-destroy mutations can't queue up a stale focus
1704
- * restore.
1705
- */
1706
1534
  destroy() {
1707
1535
  this.release();
1708
1536
  this.destroyed = true;
@@ -1712,12 +1540,10 @@ var LifecycleController = class {
1712
1540
  // src/core/gestures.ts
1713
1541
  var isAxisVertical = (mode) => mode === "bottom" || mode === "top";
1714
1542
  var SampleRing = class {
1715
- // current count, ≤ capacity
1716
1543
  constructor(capacity) {
1717
1544
  __publicField(this, "slots");
1718
1545
  __publicField(this, "capacity");
1719
1546
  __publicField(this, "head", 0);
1720
- // next write index
1721
1547
  __publicField(this, "size", 0);
1722
1548
  this.capacity = capacity;
1723
1549
  this.slots = new Array(capacity);
@@ -1730,12 +1556,6 @@ var SampleRing = class {
1730
1556
  this.head = (this.head + 1) % this.capacity;
1731
1557
  if (this.size < this.capacity) this.size++;
1732
1558
  }
1733
- /**
1734
- * Drop oldest entries whose timestamp is below `cutoff`, but never shrink
1735
- * below `minKeep`. Mirrors the original
1736
- * `while (samples.length > minKeep && samples[0].t < cutoff) samples.shift()`
1737
- * but without copying — just advances the logical tail.
1738
- */
1739
1559
  pruneBefore(cutoff, minKeep) {
1740
1560
  while (this.size > minKeep) {
1741
1561
  const oldestIdx = (this.head - this.size + this.capacity) % this.capacity;
@@ -1871,28 +1691,14 @@ var GestureController = class {
1871
1691
  __publicField(this, "detach", null);
1872
1692
  __publicField(this, "isDragging_", false);
1873
1693
  __publicField(this, "dragStartSize", 0);
1874
- // Only "touch" gestures are eligible for soft-keyboard dismissal —
1875
- // mouse/trackpad users don't have a keyboard to dismiss.
1876
1694
  __publicField(this, "currentPointerKind", "touch");
1877
- // One blur per drag — flipping focus every move event races the keyboard's
1878
- // own animations on iOS and makes the page jitter.
1879
1695
  __publicField(this, "keyboardDismissed", false);
1880
- // Reused payload object for the "drag" event — mutated each onMove instead
1881
- // of allocating a fresh `{size, delta}` per frame. At 60-120Hz a long drag
1882
- // would otherwise churn tens of KB through the GC. Mirrors browser
1883
- // PointerEvent semantics: consumers must NOT retain the reference across
1884
- // frames (clone if needed). See SheetEventMap.drag JSDoc.
1885
1696
  __publicField(this, "dragPayload", { size: 0, delta: 0 });
1886
1697
  this.deps = deps;
1887
1698
  }
1888
- /** True between `dragstart` and `dragend`/`onCancel`. */
1889
1699
  get isDragging() {
1890
1700
  return this.isDragging_;
1891
1701
  }
1892
- /**
1893
- * Bind pointer listeners on the handle and return a teardown. Engine pushes
1894
- * the teardown into its TeardownStack so destroy() drains it once.
1895
- */
1896
1702
  install() {
1897
1703
  if (this.detach) return this.detach;
1898
1704
  this.detach = installGestures(this.deps.handle, this.deps.mode, {
@@ -1949,11 +1755,6 @@ var GestureController = class {
1949
1755
  });
1950
1756
  return this.detach;
1951
1757
  }
1952
- /**
1953
- * Force-clear drag visual state. Engine calls from destroy() when destroyed
1954
- * mid-drag — gesture detach won't synthesize an onCancel, so we have to
1955
- * roll back the data-attribute + will-change ourselves.
1956
- */
1957
1758
  forceClearDragState() {
1958
1759
  if (!this.isDragging_) return;
1959
1760
  this.isDragging_ = false;
@@ -1972,11 +1773,6 @@ var AriaSliderWriter = class {
1972
1773
  mode === "bottom" || mode === "top" ? "vertical" : "horizontal"
1973
1774
  );
1974
1775
  }
1975
- /**
1976
- * Update slider position. `allowedIds` length determines `aria-valuemax`;
1977
- * `activeId` indexes into it for `aria-valuenow` + `aria-valuetext`.
1978
- * Empty list → strip every dynamic attr so AT skips the handle entirely.
1979
- */
1980
1776
  setValue(allowedIds, activeId) {
1981
1777
  if (allowedIds.length === 0) {
1982
1778
  this.handle.removeAttribute("aria-valuemin");
@@ -2050,9 +1846,6 @@ function resolveEngineOptions(opts, extras) {
2050
1846
  scrimMode: opts.scrimMode,
2051
1847
  scrimColor: opts.scrimColor,
2052
1848
  scrimBlur: opts.scrimBlur,
2053
- // Auto-promote pointer-events when tap-to-close is on so the click
2054
- // listener actually receives events. Caller can still force `false`
2055
- // explicitly to opt out — `??` only kicks in for `undefined`.
2056
1849
  scrimInteractive: opts.scrimInteractive ?? (opts.scrimTapToClose ? true : void 0),
2057
1850
  scrimTapToClose: opts.scrimTapToClose,
2058
1851
  scrimPreset: opts.scrimPreset,
@@ -2073,10 +1866,6 @@ function resolveEngineOptions(opts, extras) {
2073
1866
  closeOnEscape: opts.closeOnEscape,
2074
1867
  lockBodyScroll: opts.lockBodyScroll,
2075
1868
  inertSiblings: opts.inertSiblings,
2076
- // Folded in from `extras` so the lifecycle bag is fully assembled at
2077
- // the call site — engine no longer needs a spread+override at
2078
- // controller construction. When `extras` is omitted, the field stays
2079
- // undefined and the controller uses its own default predicate.
2080
1869
  shouldApplyInertSiblings: extras?.shouldApplyInertSiblings
2081
1870
  }
2082
1871
  };
@@ -2111,6 +1900,7 @@ var BottomSheetEngine = class {
2111
1900
  __publicField(this, "scrollContainer");
2112
1901
  __publicField(this, "backdrop");
2113
1902
  __publicField(this, "screenComponent");
1903
+ __publicField(this, "scrimParent", null);
2114
1904
  __publicField(this, "mode");
2115
1905
  __publicField(this, "flickVelocity");
2116
1906
  __publicField(this, "dragThreshold");
@@ -2121,40 +1911,22 @@ var BottomSheetEngine = class {
2121
1911
  __publicField(this, "lifecycle");
2122
1912
  __publicField(this, "closeOnBack");
2123
1913
  __publicField(this, "routedTo");
2124
- // Engine retains `snapPointsRaw` for the "find a closed-id" query in
2125
- // close() (raw input, not resolved geometry). Resolver owns geometry.
2126
1914
  __publicField(this, "snapPointsRaw");
2127
1915
  __publicField(this, "snaps");
2128
1916
  __publicField(this, "activeId");
2129
1917
  __publicField(this, "size", 0);
2130
- // Constructed in attach(); a plugin's install() could read `state` before
2131
- // attach completes if the constructor throws partway through. State getter
2132
- // null-guards via `this.gesture?.isDragging ?? false`.
2133
1918
  __publicField(this, "gesture");
2134
1919
  __publicField(this, "rootEl", null);
2135
1920
  __publicField(this, "destroyed", false);
2136
- // In-flight ViewTransition handle. A second snapTo while a VT is in flight
2137
- // would otherwise let Chromium reject the second's promise and flicker —
2138
- // newCycle() calls skipTransition() to abort cleanly.
2139
1921
  __publicField(this, "currentViewTransition", null);
2140
- // Each cycle captures the signal at entry; when a newer cycle aborts the
2141
- // controller, the captured signal flips to `aborted=true` and post-await
2142
- // guards short-circuit (prevents stale snap/open/close emits).
2143
1922
  __publicField(this, "currentAbort", new AbortController());
2144
- // Sub-pixel `--bs-size` deltas and <1% `--bs-progress` deltas are not
2145
- // visually distinguishable; gating CSSOM writes drops 30-50% of writes
2146
- // during the spring's settle tail.
2147
1923
  __publicField(this, "sizeWriteSentinel", new WriteSentinel());
2148
1924
  __publicField(this, "progressWriteSentinel", new WriteSentinel());
2149
1925
  __publicField(this, "isTopSheet", true);
2150
- // Pooled payload — identity documented in SheetEventMap.progress JSDoc;
2151
- // consumers must not retain across frames.
2152
1926
  __publicField(this, "progressPayload", {
2153
1927
  value: 0,
2154
1928
  size: 0
2155
1929
  });
2156
- // Pooled buffer for `getDragContext()` — onMove calls this 60-120×/s.
2157
- // Inner `range` object identity is also stable (mutated in place).
2158
1930
  __publicField(this, "dragContextBuf", {
2159
1931
  size: 0,
2160
1932
  maxAxisSize: 0,
@@ -2165,13 +1937,7 @@ var BottomSheetEngine = class {
2165
1937
  __publicField(this, "persistKey");
2166
1938
  __publicField(this, "linkedSheets", []);
2167
1939
  __publicField(this, "scrollCache");
2168
- // Single LIFO stack for ALL destroy-only teardowns. Plugins (added last)
2169
- // tear down first before the infrastructure they depend on. Toggle-semantic
2170
- // listeners (focus-trap / scroll-lock / scrim tap-to-close) stay as named
2171
- // fields on their controllers.
2172
1940
  __publicField(this, "teardowns", new TeardownStack());
2173
- // Pre-computed transform template — built once per engine since `mode` is
2174
- // fixed for its lifetime. Avoids the 4-arm switch at 60-120 Hz.
2175
1941
  __publicField(this, "transformTemplate");
2176
1942
  const resolved = resolveEngineOptions(opts, {
2177
1943
  shouldApplyInertSiblings: () => {
@@ -2186,7 +1952,8 @@ var BottomSheetEngine = class {
2186
1952
  this.handle = opts.handle ?? opts.element;
2187
1953
  this.scrollContainer = opts.scrollContainer;
2188
1954
  this.backdrop = opts.backdrop;
2189
- this.screenComponent = opts.screenComponent;
1955
+ this.screenComponent = opts.scrim ?? opts.screenComponent;
1956
+ this.scrimParent = this.screenComponent?.parentElement ?? null;
2190
1957
  this.mode = resolved.mode;
2191
1958
  this.transformTemplate = buildTransformTemplate(this.mode);
2192
1959
  this.aria = new AriaSliderWriter(this.handle, this.mode);
@@ -2214,8 +1981,6 @@ var BottomSheetEngine = class {
2214
1981
  getRootEl: () => this.rootEl,
2215
1982
  applySize: (size) => this.applySize(size),
2216
1983
  getSize: () => this.size,
2217
- // Optional chain protects synthetic test paths that drive animateTo
2218
- // before attach() (gesture is constructed there).
2219
1984
  isDragging: () => this.gesture?.isDragging ?? false
2220
1985
  },
2221
1986
  resolved.animation
@@ -2320,21 +2085,6 @@ var BottomSheetEngine = class {
2320
2085
  on(event, fn) {
2321
2086
  return this.bus.on(event, fn);
2322
2087
  }
2323
- /**
2324
- * Register a plugin. `install` runs synchronously; if it returns a function,
2325
- * it's captured for teardown during `destroy()`. No-op if already destroyed
2326
- * — installing into a torn-down engine would leak listeners.
2327
- *
2328
- * Throws from `plugin.install` are caught and rethrown via `queueMicrotask`
2329
- * so a buggy plugin can't take the engine down with it: the engine stays
2330
- * usable, sibling plugins still install, and the error surfaces in dev tools
2331
- * on the next tick instead of being swallowed. Mirrors the per-fn isolation
2332
- * model used for teardowns.
2333
- *
2334
- * Plugins that accept the second `scope` argument get transactional cleanup:
2335
- * push partial-install cleanups via `scope.add(fn)` and the engine drains
2336
- * them on throw, OR merges them into destroy-time teardowns on success.
2337
- */
2338
2088
  use(plugin) {
2339
2089
  if (this.destroyed) {
2340
2090
  console.warn(
@@ -2344,9 +2094,6 @@ var BottomSheetEngine = class {
2344
2094
  }
2345
2095
  const scoped = [];
2346
2096
  const scope = {
2347
- // typeof check — not just truthiness — so a JS-only plugin pushing a
2348
- // bogus value is silently rejected instead of queueing a confusing
2349
- // TypeError on destroy() drain.
2350
2097
  add: (fn) => {
2351
2098
  if (typeof fn === "function") scoped.push(fn);
2352
2099
  }
@@ -2375,36 +2122,13 @@ var BottomSheetEngine = class {
2375
2122
  }
2376
2123
  return this;
2377
2124
  }
2378
- /**
2379
- * Replace the linked-sheets list at runtime. Use when peer engines aren't
2380
- * available at construction time (mounted later in the tree). The wiring
2381
- * to the `open` event is independent of this list — it reads `linkedSheets`
2382
- * lazily on emit, so this setter is a simple field swap.
2383
- */
2384
2125
  setLinkedSheets(sheets) {
2385
2126
  if (this.destroyed) return;
2386
2127
  this.linkedSheets = sheets;
2387
2128
  }
2388
- /** Read-only view of the current allow-list, for cross-sheet target negotiation. */
2389
2129
  getAllowedIds() {
2390
2130
  return this.snaps.getAllowedIds().slice();
2391
2131
  }
2392
- /**
2393
- * Animate to a snap point by id. Resolves when the animation completes.
2394
- *
2395
- * Two call shapes:
2396
- * ```ts
2397
- * engine.snapTo("full"); // default velocity
2398
- * engine.snapTo("full", 0.5); // px/ms initial velocity
2399
- * engine.snapTo("full", { velocity: 0.5, signal }); // abortable
2400
- * ```
2401
- *
2402
- * When `opts.signal` is provided, calling `controller.abort()` cancels the
2403
- * in-flight tween/spring and the returned promise resolves without firing
2404
- * `snap`. The captured `AbortSignal` is checked after each await so external
2405
- * aborts ride the same cycle invariant as internal ones (resize, destroy,
2406
- * concurrent snapTo).
2407
- */
2408
2132
  async snapTo(id, velocityOrOpts = 0, _skipBeforeSnap = false) {
2409
2133
  if (this.destroyed) return;
2410
2134
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2463,16 +2187,6 @@ var BottomSheetEngine = class {
2463
2187
  this.handleClose();
2464
2188
  }
2465
2189
  }
2466
- /**
2467
- * Drive the sheet to an arbitrary px size, bypassing snap matching.
2468
- * Useful for preview scrubs, animation chaining, or programmatic gestures.
2469
- * Animates with the configured spring/tween. Resolves when the animation
2470
- * completes. Does NOT update `activeId` — call `snapTo(id)` after to commit.
2471
- *
2472
- * Unlike `snapTo`, this does not fire `before-snap` (it isn't a snap commit)
2473
- * nor `snap` (the sheet isn't settling to a known snap). `progress` events
2474
- * still fire via applySize. Target is clamped to `[0, maxAxisSize]`.
2475
- */
2476
2190
  async dragTo(targetSize, velocityOrOpts = 0) {
2477
2191
  if (this.destroyed) return;
2478
2192
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2510,20 +2224,10 @@ var BottomSheetEngine = class {
2510
2224
  this.snaps.setAllowedIds(ids);
2511
2225
  this.updateAriaSlider();
2512
2226
  if (snap && ids.includes(snap)) {
2513
- void this.snapTo(
2514
- snap,
2515
- 0,
2516
- /* internal */
2517
- true
2518
- );
2227
+ void this.snapTo(snap, 0, true);
2519
2228
  } else if (!ids.includes(this.activeId)) {
2520
2229
  const fallback = ids[0];
2521
- if (fallback) void this.snapTo(
2522
- fallback,
2523
- 0,
2524
- /* internal */
2525
- true
2526
- );
2230
+ if (fallback) void this.snapTo(fallback, 0, true);
2527
2231
  }
2528
2232
  }
2529
2233
  setBackdropRange(range) {
@@ -2541,21 +2245,6 @@ var BottomSheetEngine = class {
2541
2245
  setScrimInteractive(interactive) {
2542
2246
  this.scrim.setScrimInteractive(interactive);
2543
2247
  }
2544
- /**
2545
- * Batch-update multiple scrim fields in one call. Apply order is:
2546
- * 1. `preset` — fills baseline color/blur/range/interactive.
2547
- * 2. `mode` — re-positions the scrim (fixed inset / clear / hide).
2548
- * 3. `enabled` — flips the opacity contribution on/off.
2549
- * 4. `tapToClose` — installs/tears the click listener.
2550
- * 5. `color` / `blur` / `interactive` / `range` — explicit overrides.
2551
- * 6. ONE `applySize` so the per-frame opacity stays consistent.
2552
- *
2553
- * Performance: collecting changes here (vs calling individual setters)
2554
- * avoids repeated `applySize` calls during a multi-field swap. Setters
2555
- * called here suppress their own internal `applySize` either by passing
2556
- * through fast-paths (mode/enabled no-op when value unchanged) or by
2557
- * being visual-only (color/blur/interactive don't influence opacity).
2558
- */
2559
2248
  setScrim(opts) {
2560
2249
  this.scrim.setScrim(opts, () => this.applySize(this.size));
2561
2250
  }
@@ -2571,11 +2260,6 @@ var BottomSheetEngine = class {
2571
2260
  setScrimOverlay(opts) {
2572
2261
  return this.scrim.setScrimOverlay(opts);
2573
2262
  }
2574
- /**
2575
- * Read-only snapshot of scrim state. Use the `setScrim*` setters to mutate.
2576
- * Stable public API — internals (e.g. cached opacity sentinels) are NOT
2577
- * exposed so consumers can't accidentally couple to ScrimController shape.
2578
- */
2579
2263
  getScrimState() {
2580
2264
  return { mode: this.scrim.scrimMode, enabled: this.scrim.scrimEnabled };
2581
2265
  }
@@ -2621,24 +2305,11 @@ var BottomSheetEngine = class {
2621
2305
  emit(event, payload) {
2622
2306
  this.bus.emit(event, payload);
2623
2307
  }
2624
- /**
2625
- * Abort the current cycle's signal and replace the controller. Returns the
2626
- * fresh signal so the caller can capture it locally before any await — when
2627
- * a future newCycle() / destroy aborts our controller, the captured signal
2628
- * flips to aborted and post-await guards short-circuit.
2629
- */
2630
2308
  newCycle() {
2631
2309
  this.currentAbort.abort();
2632
2310
  this.currentAbort = new AbortController();
2633
2311
  return this.currentAbort.signal;
2634
2312
  }
2635
- /**
2636
- * Fire `before-snap` and report whether a listener cancelled it. The
2637
- * `cancel()` callback exposed to listeners is frozen after the synchronous
2638
- * emit phase so async callers can't silently miss — they get a console
2639
- * warning. Returns `true` when cancelled so callers can early-return /
2640
- * spring-back without re-implementing the cancel/frozen pattern.
2641
- */
2642
2313
  emitBeforeSnap(target, previousId) {
2643
2314
  let cancelled = false;
2644
2315
  let frozen = false;
@@ -2672,30 +2343,9 @@ var BottomSheetEngine = class {
2672
2343
  this.snaps.setRaw(this.snapPointsRaw);
2673
2344
  this.scrim.invalidateOpacityCache();
2674
2345
  }
2675
- /**
2676
- * Memoised `allowedRange()` — delegates to SnapResolver which owns the cache.
2677
- * Engine wrapper kept so existing call sites don't all need updating;
2678
- * resolver-level memoization survives across method invocations.
2679
- */
2680
2346
  getAllowedRange() {
2681
2347
  return this.snaps.getAllowedRange();
2682
2348
  }
2683
- /**
2684
- * Write ARIA slider value attributes onto the handle so the engine itself
2685
- * is the source of truth for screen-reader announcements. Called from the
2686
- * constructor and from every code path that mutates `activeId` /
2687
- * `allowedIds`. WAI-ARIA 1.2 requires `aria-valuenow` for `role="slider"` —
2688
- * without it screen readers can't announce drag/snap progress to keyboard
2689
- * + AT users (WCAG 4.1.2 Name, Role, Value).
2690
- *
2691
- * Engine does NOT set `role="slider"` itself — that's the adapter's or
2692
- * consumer markup's responsibility. We only decorate with dynamic values.
2693
- *
2694
- * Empty allowed-list is malformed input (settable via `setAllowed([])`);
2695
- * short-circuit to remove ARIA value attributes so screen readers see a
2696
- * no-value slider rather than a degenerate `min=max=now=0` (WAI-ARIA 1.2
2697
- * requires `valuemax > valuemin` for `role="slider"`).
2698
- */
2699
2349
  updateAriaSlider() {
2700
2350
  this.aria.setValue(this.snaps.getAllowedIds(), this.activeId);
2701
2351
  }
@@ -2720,11 +2370,6 @@ var BottomSheetEngine = class {
2720
2370
  element: this.element,
2721
2371
  mode: this.mode,
2722
2372
  getRoot: () => this.rootEl,
2723
- // Single coalesced reader — narrows the DI surface and removes 3 getter
2724
- // dispatches per onMove frame at 60-120Hz. Returns a SHARED buffer
2725
- // (this.dragContextBuf) — see field JSDoc for the retention contract.
2726
- // Inner `range` object is mutated in place too so callers caching its
2727
- // identity between frames stay valid.
2728
2373
  getDragContext: () => {
2729
2374
  const buf = this.dragContextBuf;
2730
2375
  const r = this.getAllowedRange();
@@ -2890,7 +2535,6 @@ var BottomSheetEngine = class {
2890
2535
  }
2891
2536
  });
2892
2537
  }
2893
- /** Trigger a brief haptic tick (iOS Safari + Android Chrome support). */
2894
2538
  haptic() {
2895
2539
  if (typeof navigator !== "undefined" && "vibrate" in navigator) {
2896
2540
  try {
@@ -2906,6 +2550,9 @@ var BottomSheetEngine = class {
2906
2550
  style.transform = this.transformTemplate(offset);
2907
2551
  if (this.sizeWriteSentinel.shouldWrite(size, SIZE_WRITE_EPSILON)) {
2908
2552
  style.setProperty("--bs-size", `${size}px`);
2553
+ if (this.scrimParent) {
2554
+ this.scrimParent.style.setProperty("--bs-size", `${size}px`);
2555
+ }
2909
2556
  }
2910
2557
  const progress = this.computeProgress(size);
2911
2558
  const progressChanged = this.progressWriteSentinel.shouldWrite(
@@ -2950,6 +2597,7 @@ var BottomSheet = component$((props) => {
2950
2597
  const handleRef = useSignal();
2951
2598
  const contentRef = useSignal();
2952
2599
  const backdropRef = useSignal();
2600
+ const scrimRef = useSignal();
2953
2601
  const initialActive = props.initial ?? props.snapPoints[0]?.id ?? "default";
2954
2602
  const state = useStore({
2955
2603
  size: 0,
@@ -2970,9 +2618,8 @@ var BottomSheet = component$((props) => {
2970
2618
  handle: handleRef.value,
2971
2619
  scrollContainer: contentRef.value,
2972
2620
  backdrop: props.backdrop !== false ? backdropRef.value : void 0,
2621
+ scrim: scrimRef.value,
2973
2622
  mode: props.mode ?? "bottom",
2974
- // Widen literal-id back to string at the engine boundary — the engine
2975
- // is string-typed at runtime, the TId narrowing is a TS-only veneer.
2976
2623
  snapPoints: props.snapPoints,
2977
2624
  allowed: props.allowed,
2978
2625
  initial: props.initial,
@@ -3009,6 +2656,7 @@ var BottomSheet = component$((props) => {
3009
2656
  const ariaLabel = props.ariaLabel ?? "Bottom sheet";
3010
2657
  return /* @__PURE__ */ jsxs("div", { class: "bs-root", children: [
3011
2658
  showBackdrop ? /* @__PURE__ */ jsx("div", { class: "bs-backdrop", ref: backdropRef }) : null,
2659
+ /* @__PURE__ */ jsx("div", { class: "bs-screen", ref: scrimRef, children: /* @__PURE__ */ jsx(Slot, { name: "screen" }) }),
3012
2660
  /* @__PURE__ */ jsxs(
3013
2661
  "section",
3014
2662
  {