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