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