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