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