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