@surdeddd/bottom-sheet 0.1.0 → 0.3.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 +57 -21
- package/dist/BottomSheetEngine-CAymrH-P.d.cts +285 -0
- package/dist/BottomSheetEngine-DcqcuudN.d.ts +285 -0
- package/dist/element.cjs +775 -537
- package/dist/element.d.cts +9 -57
- package/dist/element.d.ts +9 -57
- package/dist/element.iife.global.js +2 -2
- package/dist/element.js +775 -537
- package/dist/index.cjs +760 -600
- package/dist/index.d.cts +18 -85
- package/dist/index.d.ts +18 -85
- package/dist/index.js +758 -601
- 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 +168 -122
- package/dist/overlay.d.cts +14 -137
- package/dist/overlay.d.ts +14 -137
- package/dist/overlay.js +168 -122
- package/dist/preact.d.cts +2 -2
- package/dist/preact.d.ts +2 -2
- package/dist/preact.js +0 -1
- package/dist/qwik.cjs +667 -519
- package/dist/qwik.d.cts +3 -24
- package/dist/qwik.d.ts +3 -24
- package/dist/qwik.js +668 -520
- package/dist/react.cjs +873 -648
- package/dist/react.d.cts +34 -159
- package/dist/react.d.ts +34 -159
- package/dist/react.js +873 -648
- package/dist/solid.cjs +703 -560
- package/dist/solid.d.cts +264 -32
- package/dist/solid.d.ts +264 -32
- package/dist/solid.js +704 -561
- package/dist/solid.source.jsx +3215 -0
- package/dist/styles.css +37 -112
- package/dist/svelte-core.cjs +3149 -0
- package/dist/svelte-core.js +3147 -0
- package/dist/svelte-src/BottomSheet.svelte +210 -0
- package/dist/svelte-src/index.js +2 -0
- package/dist/svelte.cjs +718 -547
- package/dist/svelte.d.cts +100 -0
- package/dist/svelte.d.ts +15 -4
- package/dist/svelte.js +719 -548
- 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 +677 -526
- package/dist/vue.d.cts +78 -0
- package/dist/vue.d.ts +12 -2
- package/dist/vue.js +678 -527
- package/package.json +105 -45
- package/dist/BottomSheetEngine-CvSVbo6c.d.cts +0 -704
- package/dist/BottomSheetEngine-DbO8rW_3.d.ts +0 -704
package/dist/qwik.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { component$, useSignal, useStore, useVisibleTask$, Slot } from '@builder.io/qwik';
|
|
1
|
+
import { component$, useSignal, useStore, useVisibleTask$, noSerialize, useTask$, Slot } from '@builder.io/qwik';
|
|
2
2
|
import { jsxs, jsx } from '@builder.io/qwik/jsx-runtime';
|
|
3
3
|
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -23,6 +23,18 @@ var SheetStack = class {
|
|
|
23
23
|
this.entries.splice(idx, 1);
|
|
24
24
|
this.recompute();
|
|
25
25
|
}
|
|
26
|
+
promote(id) {
|
|
27
|
+
const idx = this.entries.findIndex((e) => e.id === id);
|
|
28
|
+
if (idx === -1) return;
|
|
29
|
+
if (idx !== this.entries.length - 1) {
|
|
30
|
+
const [entry] = this.entries.splice(idx, 1);
|
|
31
|
+
this.entries.push(entry);
|
|
32
|
+
}
|
|
33
|
+
this.recompute();
|
|
34
|
+
}
|
|
35
|
+
update() {
|
|
36
|
+
this.recompute();
|
|
37
|
+
}
|
|
26
38
|
size() {
|
|
27
39
|
return this.entries.length;
|
|
28
40
|
}
|
|
@@ -30,9 +42,24 @@ var SheetStack = class {
|
|
|
30
42
|
this.entries = [];
|
|
31
43
|
}
|
|
32
44
|
recompute() {
|
|
45
|
+
let top = null;
|
|
46
|
+
const open = [];
|
|
47
|
+
for (const entry of this.entries) {
|
|
48
|
+
if (!entry.isOpen || entry.isOpen()) {
|
|
49
|
+
top = entry;
|
|
50
|
+
open.push(entry);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!top && this.entries.length > 0) {
|
|
54
|
+
top = this.entries[this.entries.length - 1];
|
|
55
|
+
}
|
|
33
56
|
this.entries.forEach((entry, i) => {
|
|
34
57
|
entry.setZIndex(BASE_Z + i * STEP);
|
|
35
|
-
entry.setIsTop(
|
|
58
|
+
entry.setIsTop(entry === top);
|
|
59
|
+
if (entry.setDepth) {
|
|
60
|
+
const openIdx = open.indexOf(entry);
|
|
61
|
+
entry.setDepth(openIdx === -1 ? 0 : open.length - 1 - openIdx);
|
|
62
|
+
}
|
|
36
63
|
});
|
|
37
64
|
}
|
|
38
65
|
};
|
|
@@ -231,11 +258,6 @@ var SnapResolver = class {
|
|
|
231
258
|
this.allowedIds = allowed ?? raw.map((p) => p.id);
|
|
232
259
|
this.recompute();
|
|
233
260
|
}
|
|
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
261
|
recompute() {
|
|
240
262
|
this.resolved = resolveSnapList(this.raw, this.mode, this.measureFit);
|
|
241
263
|
this.maxAxisSize = this.resolved.reduce(
|
|
@@ -245,15 +267,10 @@ var SnapResolver = class {
|
|
|
245
267
|
this.rangeCache = null;
|
|
246
268
|
this.onMaxAxisSizeChange?.(this.maxAxisSize);
|
|
247
269
|
}
|
|
248
|
-
/** Replace the raw snap list and recompute. Engine calls on `setSnapPoints`. */
|
|
249
270
|
setRaw(raw) {
|
|
250
271
|
this.raw = raw;
|
|
251
272
|
this.recompute();
|
|
252
273
|
}
|
|
253
|
-
/**
|
|
254
|
-
* Replace the allow-list. Range cache depends on `(resolvedSnaps ×
|
|
255
|
-
* allowedIds)` so flipping ids alone invalidates without a full recompute.
|
|
256
|
-
*/
|
|
257
274
|
setAllowedIds(ids) {
|
|
258
275
|
this.allowedIds = ids;
|
|
259
276
|
this.rangeCache = null;
|
|
@@ -261,11 +278,6 @@ var SnapResolver = class {
|
|
|
261
278
|
findById(id) {
|
|
262
279
|
return findById(id, this.resolved);
|
|
263
280
|
}
|
|
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
281
|
findDragSettleTarget(args) {
|
|
270
282
|
return findDragSettleTarget({
|
|
271
283
|
...args,
|
|
@@ -274,18 +286,9 @@ var SnapResolver = class {
|
|
|
274
286
|
maxAxisSize: this.maxAxisSize
|
|
275
287
|
});
|
|
276
288
|
}
|
|
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
289
|
getAllowedRange() {
|
|
283
290
|
return this.rangeCache ?? (this.rangeCache = allowedRange(this.resolved, this.allowedIds));
|
|
284
291
|
}
|
|
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
292
|
getResolvedSnaps() {
|
|
290
293
|
return this.resolved;
|
|
291
294
|
}
|
|
@@ -295,18 +298,6 @@ var SnapResolver = class {
|
|
|
295
298
|
getMaxAxisSize() {
|
|
296
299
|
return this.maxAxisSize;
|
|
297
300
|
}
|
|
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
301
|
setMaxAxisSize(n) {
|
|
311
302
|
this.maxAxisSize = n;
|
|
312
303
|
this.onMaxAxisSizeChange?.(n);
|
|
@@ -353,14 +344,17 @@ function installAutoCollapse(deps) {
|
|
|
353
344
|
const fire = () => {
|
|
354
345
|
if (deps.isDestroyed()) return;
|
|
355
346
|
if (deps.isDragging()) return;
|
|
356
|
-
const
|
|
357
|
-
|
|
347
|
+
const active = deps.resolveSnap(deps.getActiveId());
|
|
348
|
+
if (!active || active.size === 0) return;
|
|
349
|
+
let target = null;
|
|
350
|
+
for (const id of deps.getAllowedIds()) {
|
|
358
351
|
const snap = deps.resolveSnap(id);
|
|
359
|
-
|
|
360
|
-
|
|
352
|
+
if (!snap || snap.size <= 0) continue;
|
|
353
|
+
if (!target || snap.size < target.size) target = snap;
|
|
354
|
+
}
|
|
361
355
|
if (!target) return;
|
|
362
|
-
if (target ===
|
|
363
|
-
deps.snapTo(target);
|
|
356
|
+
if (target.id === active.id || target.size >= active.size) return;
|
|
357
|
+
deps.snapTo(target.id);
|
|
364
358
|
};
|
|
365
359
|
const reset = () => {
|
|
366
360
|
clearTimer();
|
|
@@ -371,6 +365,7 @@ function installAutoCollapse(deps) {
|
|
|
371
365
|
}, ms);
|
|
372
366
|
};
|
|
373
367
|
const unsubscribe = deps.on("snap", () => reset());
|
|
368
|
+
if ((deps.resolveSnap(deps.getActiveId())?.size ?? 0) > 0) reset();
|
|
374
369
|
return () => {
|
|
375
370
|
clearTimer();
|
|
376
371
|
unsubscribe();
|
|
@@ -383,6 +378,7 @@ function notifyLinkedSheets(sheets, self) {
|
|
|
383
378
|
for (const linked of sheets) {
|
|
384
379
|
if (linked === self) continue;
|
|
385
380
|
try {
|
|
381
|
+
if (linked.state.size === 0) continue;
|
|
386
382
|
const ids = linked.getAllowedIds();
|
|
387
383
|
const target = ids.find((id) => id !== "closed");
|
|
388
384
|
if (target && target !== linked.state.activeId) {
|
|
@@ -429,7 +425,7 @@ function installRoute(deps) {
|
|
|
429
425
|
const onPop = (event) => {
|
|
430
426
|
if (deps.isDestroyed()) return;
|
|
431
427
|
const state = event.state;
|
|
432
|
-
if (state
|
|
428
|
+
if ((!state || state.__bsRouted !== target) && deps.getSize() > 0) {
|
|
433
429
|
routePushed = false;
|
|
434
430
|
void deps.close();
|
|
435
431
|
}
|
|
@@ -583,13 +579,17 @@ function installResizeObserver(deps) {
|
|
|
583
579
|
if (viewportSize2 > 0 && viewportSize2 < max) {
|
|
584
580
|
deps.setMaxAxisSize(viewportSize2);
|
|
585
581
|
}
|
|
582
|
+
const wasAnimating = deps.isAnimating?.() ?? false;
|
|
586
583
|
deps.cancelInFlight();
|
|
587
584
|
deps.newCycle();
|
|
588
585
|
const current = deps.resolveActiveSnap();
|
|
589
586
|
const newMax = deps.getMaxAxisSize();
|
|
590
587
|
const targetSize = current ? Math.min(current.size, newMax) : Math.min(deps.getSize(), newMax);
|
|
591
588
|
deps.setSize(targetSize);
|
|
592
|
-
if (current && !deps.isDragging())
|
|
589
|
+
if (current && !deps.isDragging()) {
|
|
590
|
+
deps.applySize(targetSize);
|
|
591
|
+
if (wasAnimating) deps.resyncAfterCancel?.();
|
|
592
|
+
}
|
|
593
593
|
};
|
|
594
594
|
let resizeObserver = null;
|
|
595
595
|
if (typeof ResizeObserver !== "undefined") {
|
|
@@ -681,73 +681,130 @@ var WriteSentinel = class {
|
|
|
681
681
|
constructor() {
|
|
682
682
|
__publicField(this, "last", -1);
|
|
683
683
|
}
|
|
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
684
|
shouldWrite(next, epsilon) {
|
|
691
685
|
if (Math.abs(next - this.last) <= epsilon) return false;
|
|
692
686
|
this.last = next;
|
|
693
687
|
return true;
|
|
694
688
|
}
|
|
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
689
|
invalidate() {
|
|
702
690
|
this.last = -1;
|
|
703
691
|
}
|
|
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
692
|
setLastWritten(value) {
|
|
711
693
|
this.last = value;
|
|
712
694
|
}
|
|
713
|
-
/** Read-only accessor for diagnostic / dedup-aware peek. */
|
|
714
695
|
get value() {
|
|
715
696
|
return this.last;
|
|
716
697
|
}
|
|
717
698
|
};
|
|
718
699
|
|
|
700
|
+
// src/core/primitives/devWarn.ts
|
|
701
|
+
function devWarn(...args) {
|
|
702
|
+
if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && typeof console !== "undefined") {
|
|
703
|
+
console.warn(...args);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/core/primitives/overlay-position.ts
|
|
708
|
+
function resolveSheetAnchoredStyle(mode, position, inset) {
|
|
709
|
+
const sizeOffset = `calc(var(--bs-size, 0px) + ${inset})`;
|
|
710
|
+
switch (mode) {
|
|
711
|
+
case "bottom":
|
|
712
|
+
if (position === "sheet-top-left") return { bottom: sizeOffset, left: inset };
|
|
713
|
+
if (position === "sheet-top-right") return { bottom: sizeOffset, right: inset };
|
|
714
|
+
return { bottom: sizeOffset, left: "50%", transform: "translateX(-50%)" };
|
|
715
|
+
case "top":
|
|
716
|
+
if (position === "sheet-top-left") return { top: sizeOffset, left: inset };
|
|
717
|
+
if (position === "sheet-top-right") return { top: sizeOffset, right: inset };
|
|
718
|
+
return { top: sizeOffset, left: "50%", transform: "translateX(-50%)" };
|
|
719
|
+
case "left":
|
|
720
|
+
if (position === "sheet-top-left") return { left: sizeOffset, top: inset };
|
|
721
|
+
if (position === "sheet-top-right") return { left: sizeOffset, bottom: inset };
|
|
722
|
+
return { left: sizeOffset, top: "50%", transform: "translateY(-50%)" };
|
|
723
|
+
case "right":
|
|
724
|
+
if (position === "sheet-top-left") return { right: sizeOffset, top: inset };
|
|
725
|
+
if (position === "sheet-top-right") return { right: sizeOffset, bottom: inset };
|
|
726
|
+
return { right: sizeOffset, top: "50%", transform: "translateY(-50%)" };
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function applyOverlayPosition(ws, mode, position, inset) {
|
|
730
|
+
switch (position) {
|
|
731
|
+
case "top-left":
|
|
732
|
+
ws.top = inset;
|
|
733
|
+
ws.left = inset;
|
|
734
|
+
break;
|
|
735
|
+
case "top-center":
|
|
736
|
+
ws.top = inset;
|
|
737
|
+
ws.left = "50%";
|
|
738
|
+
ws.transform = "translateX(-50%)";
|
|
739
|
+
break;
|
|
740
|
+
case "top-right":
|
|
741
|
+
ws.top = inset;
|
|
742
|
+
ws.right = inset;
|
|
743
|
+
break;
|
|
744
|
+
case "center-left":
|
|
745
|
+
ws.top = "50%";
|
|
746
|
+
ws.left = inset;
|
|
747
|
+
ws.transform = "translateY(-50%)";
|
|
748
|
+
break;
|
|
749
|
+
case "center":
|
|
750
|
+
ws.top = "50%";
|
|
751
|
+
ws.left = "50%";
|
|
752
|
+
ws.transform = "translate(-50%, -50%)";
|
|
753
|
+
break;
|
|
754
|
+
case "center-right":
|
|
755
|
+
ws.top = "50%";
|
|
756
|
+
ws.right = inset;
|
|
757
|
+
ws.transform = "translateY(-50%)";
|
|
758
|
+
break;
|
|
759
|
+
case "bottom-left":
|
|
760
|
+
ws.bottom = inset;
|
|
761
|
+
ws.left = inset;
|
|
762
|
+
break;
|
|
763
|
+
case "bottom-center":
|
|
764
|
+
ws.bottom = inset;
|
|
765
|
+
ws.left = "50%";
|
|
766
|
+
ws.transform = "translateX(-50%)";
|
|
767
|
+
break;
|
|
768
|
+
case "bottom-right":
|
|
769
|
+
ws.bottom = inset;
|
|
770
|
+
ws.right = inset;
|
|
771
|
+
break;
|
|
772
|
+
case "sheet-top-left":
|
|
773
|
+
case "sheet-top-center":
|
|
774
|
+
case "sheet-top-right": {
|
|
775
|
+
const style = resolveSheetAnchoredStyle(mode, position, inset);
|
|
776
|
+
if (style.top !== void 0) ws.top = style.top;
|
|
777
|
+
if (style.bottom !== void 0) ws.bottom = style.bottom;
|
|
778
|
+
if (style.left !== void 0) ws.left = style.left;
|
|
779
|
+
if (style.right !== void 0) ws.right = style.right;
|
|
780
|
+
if (style.transform !== void 0) ws.transform = style.transform;
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
719
786
|
// src/core/controllers/scrim-controller.ts
|
|
787
|
+
var VALID_CSS_LENGTH = /^-?\d+(?:\.\d+)?(?:px|em|rem|%)$/;
|
|
720
788
|
var ScrimController = class {
|
|
721
789
|
constructor(deps, opts) {
|
|
722
|
-
// ── DOM refs (mirror engine's; cached for hot path) ─────────────────
|
|
723
790
|
__publicField(this, "screenComponent");
|
|
724
791
|
__publicField(this, "backdrop");
|
|
725
792
|
__publicField(this, "mode");
|
|
726
|
-
// ── Engine callbacks ────────────────────────────────────────────────
|
|
727
793
|
__publicField(this, "isDestroyed");
|
|
728
794
|
__publicField(this, "isTopSheet");
|
|
729
795
|
__publicField(this, "getAllowedIds");
|
|
730
796
|
__publicField(this, "getResolvedSnaps");
|
|
731
797
|
__publicField(this, "snapToFn");
|
|
732
|
-
|
|
798
|
+
__publicField(this, "closeFn");
|
|
799
|
+
__publicField(this, "scrimInteractive", false);
|
|
733
800
|
__publicField(this, "scrimMode");
|
|
734
801
|
__publicField(this, "backdropRange");
|
|
735
802
|
__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
803
|
__publicField(this, "scrimEnabled", true);
|
|
740
804
|
__publicField(this, "savedScrimRanges", null);
|
|
741
|
-
// ── Tap-to-close ────────────────────────────────────────────────────
|
|
742
805
|
__publicField(this, "scrimTapToCloseEnabled", false);
|
|
743
806
|
__publicField(this, "detachScrimTap", null);
|
|
744
|
-
// ── Scrim overlay slot (positioned floating element) ────────────────
|
|
745
807
|
__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
808
|
__publicField(this, "backdropOpacitySentinel", new WriteSentinel());
|
|
752
809
|
__publicField(this, "screenOpacitySentinel", new WriteSentinel());
|
|
753
810
|
__publicField(this, "lastBackdropPointer", "");
|
|
@@ -760,6 +817,7 @@ var ScrimController = class {
|
|
|
760
817
|
this.getAllowedIds = deps.getAllowedIds;
|
|
761
818
|
this.getResolvedSnaps = deps.getResolvedSnaps;
|
|
762
819
|
this.snapToFn = deps.snapTo;
|
|
820
|
+
this.closeFn = deps.close;
|
|
763
821
|
this.scrimMode = opts.scrimMode ?? "full";
|
|
764
822
|
const preset = opts.scrimPreset ? SCRIM_PRESETS[opts.scrimPreset] : null;
|
|
765
823
|
this.backdropRange = opts.backdropRange ?? preset?.range ?? [0, 1];
|
|
@@ -782,41 +840,15 @@ var ScrimController = class {
|
|
|
782
840
|
}
|
|
783
841
|
}
|
|
784
842
|
}
|
|
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
843
|
invalidateOpacityCache() {
|
|
798
844
|
this.backdropOpacitySentinel.invalidate();
|
|
799
845
|
this.screenOpacitySentinel.invalidate();
|
|
800
846
|
}
|
|
801
|
-
/** Engine calls this when its `attach()` runs — installs scrim-tap-click if requested. */
|
|
802
847
|
attach() {
|
|
803
848
|
if (this.scrimTapToCloseEnabled) {
|
|
804
849
|
this.setScrimTapToClose(true);
|
|
805
850
|
}
|
|
806
851
|
}
|
|
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
852
|
applyOpacity(progress, progressChanged) {
|
|
821
853
|
if (!progressChanged && this.backdropOpacitySentinel.value !== -1 && this.screenOpacitySentinel.value !== -1) {
|
|
822
854
|
return;
|
|
@@ -862,80 +894,30 @@ var ScrimController = class {
|
|
|
862
894
|
}
|
|
863
895
|
}
|
|
864
896
|
}
|
|
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
897
|
setBackdropRange(range, applySize) {
|
|
875
898
|
if (this.isDestroyed()) return;
|
|
876
899
|
this.backdropRange = range;
|
|
877
900
|
this.invalidateOpacityCache();
|
|
878
901
|
applySize();
|
|
879
902
|
}
|
|
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
903
|
setScreenRange(range, applySize) {
|
|
889
904
|
if (this.isDestroyed()) return;
|
|
890
905
|
this.screenRange = range;
|
|
891
906
|
this.invalidateOpacityCache();
|
|
892
907
|
applySize();
|
|
893
908
|
}
|
|
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
909
|
setScrimColor(color) {
|
|
903
910
|
if (this.isDestroyed()) return;
|
|
904
911
|
this.applyScrimStyles({ color: color === void 0 ? null : color });
|
|
905
912
|
}
|
|
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
913
|
setScrimBlur(blur) {
|
|
915
914
|
if (this.isDestroyed()) return;
|
|
916
915
|
this.applyScrimStyles({ blur: blur === void 0 ? null : blur });
|
|
917
916
|
}
|
|
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
917
|
setScrimInteractive(interactive) {
|
|
926
918
|
if (this.isDestroyed()) return;
|
|
927
919
|
this.applyScrimStyles({ interactive });
|
|
928
920
|
}
|
|
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
921
|
setScrimMode(mode, applySize) {
|
|
940
922
|
if (this.isDestroyed()) return;
|
|
941
923
|
if (mode === this.scrimMode) return;
|
|
@@ -965,15 +947,6 @@ var ScrimController = class {
|
|
|
965
947
|
}
|
|
966
948
|
applySize();
|
|
967
949
|
}
|
|
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
950
|
setScrimTapToClose(enabled) {
|
|
978
951
|
if (this.isDestroyed()) return;
|
|
979
952
|
if (!this.screenComponent) return;
|
|
@@ -986,11 +959,15 @@ var ScrimController = class {
|
|
|
986
959
|
}
|
|
987
960
|
const onClick = () => {
|
|
988
961
|
if (this.isDestroyed()) return;
|
|
962
|
+
if (this.closeFn) {
|
|
963
|
+
this.closeFn();
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
989
966
|
const allowed = this.getAllowedIds();
|
|
990
967
|
const snaps = this.getResolvedSnaps();
|
|
991
968
|
const fallback = allowed.find((id) => {
|
|
992
969
|
const snap = findById(id, snaps);
|
|
993
|
-
return snap !== null && snap.size
|
|
970
|
+
return snap !== null && snap.size === 0;
|
|
994
971
|
});
|
|
995
972
|
if (fallback) this.snapToFn(fallback);
|
|
996
973
|
};
|
|
@@ -1003,20 +980,11 @@ var ScrimController = class {
|
|
|
1003
980
|
this.scrimTapToCloseEnabled = false;
|
|
1004
981
|
}
|
|
1005
982
|
}
|
|
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
983
|
setScrimEnabled(enabled, applySize) {
|
|
1017
984
|
if (this.isDestroyed()) return;
|
|
1018
985
|
if (enabled === this.scrimEnabled) return;
|
|
1019
986
|
this.scrimEnabled = enabled;
|
|
987
|
+
this.invalidateOpacityCache();
|
|
1020
988
|
if (!enabled) {
|
|
1021
989
|
this.savedScrimRanges = {
|
|
1022
990
|
screen: this.screenRange,
|
|
@@ -1031,20 +999,6 @@ var ScrimController = class {
|
|
|
1031
999
|
}
|
|
1032
1000
|
applySize();
|
|
1033
1001
|
}
|
|
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
1002
|
setScrim(opts, applySize) {
|
|
1049
1003
|
if (this.isDestroyed()) return;
|
|
1050
1004
|
if (opts.preset) {
|
|
@@ -1054,7 +1008,12 @@ var ScrimController = class {
|
|
|
1054
1008
|
blur: cfg.blur ?? null,
|
|
1055
1009
|
interactive: cfg.interactive
|
|
1056
1010
|
});
|
|
1057
|
-
this.
|
|
1011
|
+
if (!this.scrimEnabled && this.savedScrimRanges) {
|
|
1012
|
+
this.savedScrimRanges = { screen: cfg.range, backdrop: cfg.range };
|
|
1013
|
+
} else {
|
|
1014
|
+
this.screenRange = cfg.range;
|
|
1015
|
+
this.backdropRange = cfg.range;
|
|
1016
|
+
}
|
|
1058
1017
|
}
|
|
1059
1018
|
const noop = () => {
|
|
1060
1019
|
};
|
|
@@ -1067,27 +1026,18 @@ var ScrimController = class {
|
|
|
1067
1026
|
interactive: opts.interactive
|
|
1068
1027
|
});
|
|
1069
1028
|
if (opts.range !== void 0) {
|
|
1070
|
-
this.
|
|
1029
|
+
if (!this.scrimEnabled && this.savedScrimRanges) {
|
|
1030
|
+
this.savedScrimRanges = {
|
|
1031
|
+
screen: opts.range,
|
|
1032
|
+
backdrop: this.savedScrimRanges.backdrop
|
|
1033
|
+
};
|
|
1034
|
+
} else {
|
|
1035
|
+
this.screenRange = opts.range;
|
|
1036
|
+
}
|
|
1071
1037
|
}
|
|
1072
1038
|
this.invalidateOpacityCache();
|
|
1073
1039
|
applySize();
|
|
1074
1040
|
}
|
|
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
1041
|
setScrimOverlay(opts) {
|
|
1092
1042
|
if (this.isDestroyed()) return () => {
|
|
1093
1043
|
};
|
|
@@ -1095,61 +1045,38 @@ var ScrimController = class {
|
|
|
1095
1045
|
};
|
|
1096
1046
|
this.scrimOverlayTeardown?.();
|
|
1097
1047
|
const screen = this.screenComponent;
|
|
1098
|
-
const
|
|
1048
|
+
const insetRaw = opts.inset ?? "16px";
|
|
1049
|
+
const inset = VALID_CSS_LENGTH.test(insetRaw) ? insetRaw : "16px";
|
|
1050
|
+
if (inset !== insetRaw) {
|
|
1051
|
+
devWarn(
|
|
1052
|
+
`[bottom-sheet] setScrimOverlay: invalid inset ${JSON.stringify(insetRaw)}; expected a CSS length like "16px" / "1rem" / "5%". Falling back to "16px".`
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
if (opts.children.ownerDocument && opts.children.ownerDocument !== screen.ownerDocument) {
|
|
1056
|
+
devWarn(
|
|
1057
|
+
"[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."
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1099
1060
|
const position = opts.position ?? "top-right";
|
|
1100
1061
|
const interactive = opts.interactive ?? true;
|
|
1101
1062
|
const wrapper = screen.ownerDocument.createElement("div");
|
|
1102
1063
|
wrapper.className = "bs-scrim-overlay";
|
|
1103
1064
|
const ws = wrapper.style;
|
|
1104
1065
|
ws.position = "absolute";
|
|
1105
|
-
|
|
1106
|
-
case "top-left":
|
|
1107
|
-
ws.top = inset;
|
|
1108
|
-
ws.left = inset;
|
|
1109
|
-
break;
|
|
1110
|
-
case "top-center":
|
|
1111
|
-
ws.top = inset;
|
|
1112
|
-
ws.left = "50%";
|
|
1113
|
-
ws.transform = "translateX(-50%)";
|
|
1114
|
-
break;
|
|
1115
|
-
case "top-right":
|
|
1116
|
-
ws.top = inset;
|
|
1117
|
-
ws.right = inset;
|
|
1118
|
-
break;
|
|
1119
|
-
case "center-left":
|
|
1120
|
-
ws.top = "50%";
|
|
1121
|
-
ws.left = inset;
|
|
1122
|
-
ws.transform = "translateY(-50%)";
|
|
1123
|
-
break;
|
|
1124
|
-
case "center":
|
|
1125
|
-
ws.top = "50%";
|
|
1126
|
-
ws.left = "50%";
|
|
1127
|
-
ws.transform = "translate(-50%, -50%)";
|
|
1128
|
-
break;
|
|
1129
|
-
case "center-right":
|
|
1130
|
-
ws.top = "50%";
|
|
1131
|
-
ws.right = inset;
|
|
1132
|
-
ws.transform = "translateY(-50%)";
|
|
1133
|
-
break;
|
|
1134
|
-
case "bottom-left":
|
|
1135
|
-
ws.bottom = inset;
|
|
1136
|
-
ws.left = inset;
|
|
1137
|
-
break;
|
|
1138
|
-
case "bottom-center":
|
|
1139
|
-
ws.bottom = inset;
|
|
1140
|
-
ws.left = "50%";
|
|
1141
|
-
ws.transform = "translateX(-50%)";
|
|
1142
|
-
break;
|
|
1143
|
-
case "bottom-right":
|
|
1144
|
-
ws.bottom = inset;
|
|
1145
|
-
ws.right = inset;
|
|
1146
|
-
break;
|
|
1147
|
-
}
|
|
1066
|
+
applyOverlayPosition(ws, this.mode, position, inset);
|
|
1148
1067
|
if (interactive) {
|
|
1149
1068
|
ws.pointerEvents = "auto";
|
|
1150
1069
|
}
|
|
1151
1070
|
wrapper.appendChild(opts.children);
|
|
1152
|
-
screen.
|
|
1071
|
+
const host = screen.parentElement;
|
|
1072
|
+
if (!host) {
|
|
1073
|
+
devWarn(
|
|
1074
|
+
"[bottom-sheet] setScrimOverlay: scrim element has no parent, skipping overlay injection. Mount the sheet root before calling setScrimOverlay."
|
|
1075
|
+
);
|
|
1076
|
+
return () => {
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
host.appendChild(wrapper);
|
|
1153
1080
|
const teardown = () => {
|
|
1154
1081
|
if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
|
|
1155
1082
|
if (this.scrimOverlayTeardown === teardown) {
|
|
@@ -1159,7 +1086,6 @@ var ScrimController = class {
|
|
|
1159
1086
|
this.scrimOverlayTeardown = teardown;
|
|
1160
1087
|
return teardown;
|
|
1161
1088
|
}
|
|
1162
|
-
/** Tear down toggle-state listeners + remove any injected overlay wrapper. */
|
|
1163
1089
|
destroy() {
|
|
1164
1090
|
this.detachScrimTap?.();
|
|
1165
1091
|
this.detachScrimTap = null;
|
|
@@ -1178,8 +1104,6 @@ var ScrimController = class {
|
|
|
1178
1104
|
this.lastBackdropPointer = "";
|
|
1179
1105
|
this.lastScreenDisplay = "";
|
|
1180
1106
|
}
|
|
1181
|
-
// ── Private helpers ────────────────────────────────────────────────
|
|
1182
|
-
/** Centralized scrim style writer — atomic 3-property write across all setters. */
|
|
1183
1107
|
applyScrimStyles(opts) {
|
|
1184
1108
|
if (!this.screenComponent) return;
|
|
1185
1109
|
const s = this.screenComponent.style;
|
|
@@ -1198,10 +1122,10 @@ var ScrimController = class {
|
|
|
1198
1122
|
}
|
|
1199
1123
|
}
|
|
1200
1124
|
if (opts.interactive !== void 0) {
|
|
1125
|
+
this.scrimInteractive = opts.interactive;
|
|
1201
1126
|
s.pointerEvents = opts.interactive ? "auto" : "none";
|
|
1202
1127
|
}
|
|
1203
1128
|
}
|
|
1204
|
-
/** Pin the screen overlay to the area NOT occupied by the sheet. */
|
|
1205
1129
|
applyAboveSheetInset() {
|
|
1206
1130
|
if (!this.screenComponent) return;
|
|
1207
1131
|
const insetByMode = {
|
|
@@ -1213,7 +1137,7 @@ var ScrimController = class {
|
|
|
1213
1137
|
const s = this.screenComponent.style;
|
|
1214
1138
|
s.position = "fixed";
|
|
1215
1139
|
s.inset = insetByMode[this.mode];
|
|
1216
|
-
s.pointerEvents = "none";
|
|
1140
|
+
s.pointerEvents = this.scrimInteractive || this.scrimTapToCloseEnabled ? "auto" : "none";
|
|
1217
1141
|
}
|
|
1218
1142
|
};
|
|
1219
1143
|
|
|
@@ -1267,6 +1191,10 @@ var tween = ({
|
|
|
1267
1191
|
promise
|
|
1268
1192
|
};
|
|
1269
1193
|
};
|
|
1194
|
+
var prefersReducedMotion = () => {
|
|
1195
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
1196
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1197
|
+
};
|
|
1270
1198
|
|
|
1271
1199
|
// src/core/animation/spring.ts
|
|
1272
1200
|
var DEFAULT_SPRING = {
|
|
@@ -1297,14 +1225,20 @@ var runSpring = ({
|
|
|
1297
1225
|
if (cancelled) return;
|
|
1298
1226
|
const dt = Math.min((now - lastTime) / 1e3, MAX_DT);
|
|
1299
1227
|
lastTime = now;
|
|
1300
|
-
const
|
|
1301
|
-
|
|
1228
|
+
const steps = Math.max(1, Math.ceil(dt * 240));
|
|
1229
|
+
const subDt = dt / steps;
|
|
1230
|
+
let stepsLeft = steps;
|
|
1302
1231
|
while (stepsLeft-- > 0) {
|
|
1303
1232
|
const force = -cfg.stiffness * (x - to) - cfg.damping * v;
|
|
1304
1233
|
const accel = force / cfg.mass;
|
|
1305
1234
|
v += accel * subDt;
|
|
1306
1235
|
x += v * subDt;
|
|
1307
1236
|
}
|
|
1237
|
+
if (!Number.isFinite(x) || !Number.isFinite(v)) {
|
|
1238
|
+
onUpdate(to, 0);
|
|
1239
|
+
resolve();
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1308
1242
|
onUpdate(x, v);
|
|
1309
1243
|
const isAtRest = Math.abs(to - x) < (cfg.restDelta ?? 0.5) && Math.abs(v) < (cfg.restSpeed ?? 0.5);
|
|
1310
1244
|
if (isAtRest) {
|
|
@@ -1368,13 +1302,9 @@ var AnimationRunner = class {
|
|
|
1368
1302
|
__publicField(this, "duration");
|
|
1369
1303
|
__publicField(this, "easing");
|
|
1370
1304
|
__publicField(this, "respectReducedMotion");
|
|
1371
|
-
/** Live-tracked via mq listener so animateTo skips matchMedia per snap. */
|
|
1372
1305
|
__publicField(this, "reducedMotion", false);
|
|
1373
|
-
/** Disposes the matchMedia change listener — drained from destroy(). */
|
|
1374
1306
|
__publicField(this, "detachReducedMotion", null);
|
|
1375
|
-
/** Computed once — both inputs (option + API presence) stable for life. */
|
|
1376
1307
|
__publicField(this, "viewTransitionsAvailable");
|
|
1377
|
-
/** In-flight handles. Public for tests / debugging only — engine drives via cancel(). */
|
|
1378
1308
|
__publicField(this, "currentTween", null);
|
|
1379
1309
|
__publicField(this, "currentSpring", null);
|
|
1380
1310
|
this.element = deps.element;
|
|
@@ -1405,26 +1335,13 @@ var AnimationRunner = class {
|
|
|
1405
1335
|
}
|
|
1406
1336
|
}
|
|
1407
1337
|
}
|
|
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
1338
|
get isAnimating() {
|
|
1416
1339
|
return this.currentTween !== null || this.currentSpring !== null;
|
|
1417
1340
|
}
|
|
1418
|
-
/** Cancel any in-flight tween/spring without touching engine state. */
|
|
1419
1341
|
cancel() {
|
|
1420
1342
|
this.currentTween?.cancel();
|
|
1421
1343
|
this.currentSpring?.cancel();
|
|
1422
1344
|
}
|
|
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
1345
|
async animateTo(target, velocityPxPerMs) {
|
|
1429
1346
|
if (target === this.getSize()) return;
|
|
1430
1347
|
this.currentTween?.cancel();
|
|
@@ -1465,7 +1382,6 @@ var AnimationRunner = class {
|
|
|
1465
1382
|
if (!this.isDragging()) this.element.style.willChange = "auto";
|
|
1466
1383
|
}
|
|
1467
1384
|
}
|
|
1468
|
-
/** Tear down the matchMedia listener. Engine calls from destroy(). */
|
|
1469
1385
|
destroy() {
|
|
1470
1386
|
this.detachReducedMotion?.();
|
|
1471
1387
|
this.detachReducedMotion = null;
|
|
@@ -1497,9 +1413,13 @@ var isVisible = (el) => {
|
|
|
1497
1413
|
var focusables = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter(
|
|
1498
1414
|
(el) => !el.hasAttribute("inert") && isVisible(el) && !el.matches("[aria-hidden='true']")
|
|
1499
1415
|
);
|
|
1416
|
+
var trapStack = [];
|
|
1500
1417
|
var installFocusTrap = (container, options = {}) => {
|
|
1501
1418
|
if (typeof document === "undefined") return () => {
|
|
1502
1419
|
};
|
|
1420
|
+
const token = {};
|
|
1421
|
+
trapStack.push(token);
|
|
1422
|
+
const isActive = () => trapStack[trapStack.length - 1] === token;
|
|
1503
1423
|
const previouslyFocused = document.activeElement;
|
|
1504
1424
|
const focusInitial = () => {
|
|
1505
1425
|
const initial = typeof options.initialFocus === "string" ? container.querySelector(options.initialFocus) : options.initialFocus ?? focusables(container)[0] ?? container;
|
|
@@ -1507,6 +1427,7 @@ var installFocusTrap = (container, options = {}) => {
|
|
|
1507
1427
|
};
|
|
1508
1428
|
focusInitial();
|
|
1509
1429
|
const handleKey = (e) => {
|
|
1430
|
+
if (!isActive()) return;
|
|
1510
1431
|
if (e.key === "Escape" && options.onEscape) {
|
|
1511
1432
|
options.onEscape();
|
|
1512
1433
|
return;
|
|
@@ -1529,6 +1450,7 @@ var installFocusTrap = (container, options = {}) => {
|
|
|
1529
1450
|
}
|
|
1530
1451
|
};
|
|
1531
1452
|
const handleFocusIn = (e) => {
|
|
1453
|
+
if (!isActive()) return;
|
|
1532
1454
|
const target = e.target;
|
|
1533
1455
|
if (target && !container.contains(target)) {
|
|
1534
1456
|
const list = focusables(container);
|
|
@@ -1541,6 +1463,8 @@ var installFocusTrap = (container, options = {}) => {
|
|
|
1541
1463
|
return () => {
|
|
1542
1464
|
if (released) return;
|
|
1543
1465
|
released = true;
|
|
1466
|
+
const idx = trapStack.indexOf(token);
|
|
1467
|
+
if (idx !== -1) trapStack.splice(idx, 1);
|
|
1544
1468
|
document.removeEventListener("keydown", handleKey, true);
|
|
1545
1469
|
document.removeEventListener("focusin", handleFocusIn, true);
|
|
1546
1470
|
if (previouslyFocused && document.contains(previouslyFocused)) {
|
|
@@ -1596,11 +1520,17 @@ function createInertSiblings(rootProvider) {
|
|
|
1596
1520
|
return {
|
|
1597
1521
|
apply() {
|
|
1598
1522
|
if (typeof document === "undefined") return;
|
|
1599
|
-
|
|
1600
|
-
|
|
1523
|
+
let top = rootProvider();
|
|
1524
|
+
while (top.parentElement && top.parentElement !== document.body) {
|
|
1525
|
+
top = top.parentElement;
|
|
1526
|
+
}
|
|
1527
|
+
const parent = top.parentElement ?? document.body;
|
|
1601
1528
|
for (const child of Array.from(parent.children)) {
|
|
1602
|
-
if (child ===
|
|
1529
|
+
if (child === top) continue;
|
|
1603
1530
|
if (child.hasAttribute("inert")) continue;
|
|
1531
|
+
if (child.classList.contains("bs-backdrop") || child.classList.contains("bs-screen") || child.classList.contains("bs-root")) {
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1604
1534
|
child.setAttribute("inert", "");
|
|
1605
1535
|
tracked.push(child);
|
|
1606
1536
|
}
|
|
@@ -1616,7 +1546,6 @@ function createInertSiblings(rootProvider) {
|
|
|
1616
1546
|
var LifecycleController = class {
|
|
1617
1547
|
constructor(deps, opts) {
|
|
1618
1548
|
__publicField(this, "element");
|
|
1619
|
-
__publicField(this, "closeFn");
|
|
1620
1549
|
__publicField(this, "focusTrapEnabled");
|
|
1621
1550
|
__publicField(this, "initialFocus");
|
|
1622
1551
|
__publicField(this, "closeOnEscape");
|
|
@@ -1625,16 +1554,11 @@ var LifecycleController = class {
|
|
|
1625
1554
|
__publicField(this, "inertSiblings");
|
|
1626
1555
|
__publicField(this, "shouldApplyInertSiblings");
|
|
1627
1556
|
__publicField(this, "returnFocus");
|
|
1628
|
-
/** Toggle-state teardowns — fire on close, NOT in destroy-only stack. */
|
|
1629
1557
|
__publicField(this, "releaseFocusTrap", null);
|
|
1630
1558
|
__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
1559
|
__publicField(this, "installed", false);
|
|
1635
1560
|
__publicField(this, "destroyed", false);
|
|
1636
1561
|
this.element = deps.element;
|
|
1637
|
-
this.closeFn = deps.close;
|
|
1638
1562
|
this.focusTrapEnabled = opts.focusTrap ?? false;
|
|
1639
1563
|
this.initialFocus = opts.initialFocus;
|
|
1640
1564
|
this.closeOnEscape = opts.closeOnEscape ?? true;
|
|
@@ -1644,14 +1568,9 @@ var LifecycleController = class {
|
|
|
1644
1568
|
this.returnFocus = opts.returnFocus;
|
|
1645
1569
|
this.inertSiblings = createInertSiblings(() => this.element);
|
|
1646
1570
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
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
|
-
*/
|
|
1571
|
+
get isInstalled() {
|
|
1572
|
+
return this.installed;
|
|
1573
|
+
}
|
|
1655
1574
|
install() {
|
|
1656
1575
|
if (this.destroyed) return;
|
|
1657
1576
|
this.installed = true;
|
|
@@ -1660,30 +1579,17 @@ var LifecycleController = class {
|
|
|
1660
1579
|
}
|
|
1661
1580
|
if (this.focusTrapEnabled && !this.releaseFocusTrap) {
|
|
1662
1581
|
this.releaseFocusTrap = installFocusTrap(this.element, {
|
|
1663
|
-
initialFocus: this.initialFocus
|
|
1664
|
-
onEscape: this.closeOnEscape ? () => void this.closeFn() : void 0
|
|
1582
|
+
initialFocus: this.initialFocus
|
|
1665
1583
|
});
|
|
1666
1584
|
}
|
|
1667
1585
|
if (this.inertSiblingsEnabled && this.shouldApplyInertSiblings()) {
|
|
1668
1586
|
this.inertSiblings.apply();
|
|
1669
1587
|
}
|
|
1670
1588
|
}
|
|
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
1589
|
setReturnFocus(target) {
|
|
1677
1590
|
if (this.destroyed) return;
|
|
1678
1591
|
this.returnFocus = target;
|
|
1679
1592
|
}
|
|
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
1593
|
release() {
|
|
1688
1594
|
if (!this.installed) return;
|
|
1689
1595
|
this.installed = false;
|
|
@@ -1697,12 +1603,6 @@ var LifecycleController = class {
|
|
|
1697
1603
|
target?.focus?.();
|
|
1698
1604
|
}
|
|
1699
1605
|
}
|
|
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
1606
|
destroy() {
|
|
1707
1607
|
this.release();
|
|
1708
1608
|
this.destroyed = true;
|
|
@@ -1712,12 +1612,10 @@ var LifecycleController = class {
|
|
|
1712
1612
|
// src/core/gestures.ts
|
|
1713
1613
|
var isAxisVertical = (mode) => mode === "bottom" || mode === "top";
|
|
1714
1614
|
var SampleRing = class {
|
|
1715
|
-
// current count, ≤ capacity
|
|
1716
1615
|
constructor(capacity) {
|
|
1717
1616
|
__publicField(this, "slots");
|
|
1718
1617
|
__publicField(this, "capacity");
|
|
1719
1618
|
__publicField(this, "head", 0);
|
|
1720
|
-
// next write index
|
|
1721
1619
|
__publicField(this, "size", 0);
|
|
1722
1620
|
this.capacity = capacity;
|
|
1723
1621
|
this.slots = new Array(capacity);
|
|
@@ -1730,12 +1628,6 @@ var SampleRing = class {
|
|
|
1730
1628
|
this.head = (this.head + 1) % this.capacity;
|
|
1731
1629
|
if (this.size < this.capacity) this.size++;
|
|
1732
1630
|
}
|
|
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
1631
|
pruneBefore(cutoff, minKeep) {
|
|
1740
1632
|
while (this.size > minKeep) {
|
|
1741
1633
|
const oldestIdx = (this.head - this.size + this.capacity) % this.capacity;
|
|
@@ -1841,10 +1733,11 @@ var installGestures = (handle, mode, callbacks) => {
|
|
|
1841
1733
|
};
|
|
1842
1734
|
|
|
1843
1735
|
// src/core/primitives/rubber-band.ts
|
|
1844
|
-
var RUBBER_BAND_FACTOR = 0.55;
|
|
1845
1736
|
function rubberBand(overshoot, maxAxisSize) {
|
|
1846
1737
|
const cap = Math.min(maxAxisSize * 0.15, 80);
|
|
1847
|
-
|
|
1738
|
+
if (cap <= 0) return 0;
|
|
1739
|
+
const abs = Math.abs(overshoot);
|
|
1740
|
+
return Math.sign(overshoot) * (cap * abs / (abs + cap));
|
|
1848
1741
|
}
|
|
1849
1742
|
|
|
1850
1743
|
// src/core/features/soft-keyboard.ts
|
|
@@ -1871,28 +1764,14 @@ var GestureController = class {
|
|
|
1871
1764
|
__publicField(this, "detach", null);
|
|
1872
1765
|
__publicField(this, "isDragging_", false);
|
|
1873
1766
|
__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
1767
|
__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
1768
|
__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
1769
|
__publicField(this, "dragPayload", { size: 0, delta: 0 });
|
|
1886
1770
|
this.deps = deps;
|
|
1887
1771
|
}
|
|
1888
|
-
/** True between `dragstart` and `dragend`/`onCancel`. */
|
|
1889
1772
|
get isDragging() {
|
|
1890
1773
|
return this.isDragging_;
|
|
1891
1774
|
}
|
|
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
1775
|
install() {
|
|
1897
1776
|
if (this.detach) return this.detach;
|
|
1898
1777
|
this.detach = installGestures(this.deps.handle, this.deps.mode, {
|
|
@@ -1943,17 +1822,16 @@ var GestureController = class {
|
|
|
1943
1822
|
this.isDragging_ = false;
|
|
1944
1823
|
this.keyboardDismissed = false;
|
|
1945
1824
|
this.deps.getRoot()?.removeAttribute("data-dragging");
|
|
1825
|
+
this.deps.emit("dragend", {
|
|
1826
|
+
size: this.deps.getDragContext().size,
|
|
1827
|
+
velocity: 0
|
|
1828
|
+
});
|
|
1946
1829
|
const restoreTo = this.dragStartSize;
|
|
1947
1830
|
void this.deps.animateTo(restoreTo, 0);
|
|
1948
1831
|
}
|
|
1949
1832
|
});
|
|
1950
1833
|
return this.detach;
|
|
1951
1834
|
}
|
|
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
1835
|
forceClearDragState() {
|
|
1958
1836
|
if (!this.isDragging_) return;
|
|
1959
1837
|
this.isDragging_ = false;
|
|
@@ -1972,11 +1850,6 @@ var AriaSliderWriter = class {
|
|
|
1972
1850
|
mode === "bottom" || mode === "top" ? "vertical" : "horizontal"
|
|
1973
1851
|
);
|
|
1974
1852
|
}
|
|
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
1853
|
setValue(allowedIds, activeId) {
|
|
1981
1854
|
if (allowedIds.length === 0) {
|
|
1982
1855
|
this.handle.removeAttribute("aria-valuemin");
|
|
@@ -2026,6 +1899,287 @@ function createScrollCache(deps) {
|
|
|
2026
1899
|
};
|
|
2027
1900
|
}
|
|
2028
1901
|
|
|
1902
|
+
// src/core/primitives/anchor-animations.ts
|
|
1903
|
+
var DEFAULT_DURATION3 = 200;
|
|
1904
|
+
var DEFAULT_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
|
|
1905
|
+
var PRESET_FRAMES = {
|
|
1906
|
+
fade: {
|
|
1907
|
+
enter: [{ opacity: 0 }, { opacity: 1 }],
|
|
1908
|
+
exit: [{ opacity: 1 }, { opacity: 0 }]
|
|
1909
|
+
},
|
|
1910
|
+
scale: {
|
|
1911
|
+
enter: [
|
|
1912
|
+
{ opacity: 0, transform: "scale(0.85)" },
|
|
1913
|
+
{ opacity: 1, transform: "scale(1)" }
|
|
1914
|
+
],
|
|
1915
|
+
exit: [
|
|
1916
|
+
{ opacity: 1, transform: "scale(1)" },
|
|
1917
|
+
{ opacity: 0, transform: "scale(0.85)" }
|
|
1918
|
+
]
|
|
1919
|
+
},
|
|
1920
|
+
slide: {
|
|
1921
|
+
enter: [
|
|
1922
|
+
{ opacity: 0, transform: "translateY(10px)" },
|
|
1923
|
+
{ opacity: 1, transform: "translateY(0)" }
|
|
1924
|
+
],
|
|
1925
|
+
exit: [
|
|
1926
|
+
{ opacity: 1, transform: "translateY(0)" },
|
|
1927
|
+
{ opacity: 0, transform: "translateY(10px)" }
|
|
1928
|
+
]
|
|
1929
|
+
},
|
|
1930
|
+
pop: {
|
|
1931
|
+
enter: [
|
|
1932
|
+
{ opacity: 0, transform: "scale(0.6)", offset: 0 },
|
|
1933
|
+
{ opacity: 1, transform: "scale(1.06)", offset: 0.7 },
|
|
1934
|
+
{ opacity: 1, transform: "scale(1)", offset: 1 }
|
|
1935
|
+
],
|
|
1936
|
+
exit: [
|
|
1937
|
+
{ opacity: 1, transform: "scale(1)" },
|
|
1938
|
+
{ opacity: 0, transform: "scale(0.6)" }
|
|
1939
|
+
]
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
var INSTANT = {
|
|
1943
|
+
finished: Promise.resolve(),
|
|
1944
|
+
cancel: () => {
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1947
|
+
function resolveTransitionFrames(spec, phase) {
|
|
1948
|
+
const fallback = { frames: null, duration: 0, easing: DEFAULT_EASING };
|
|
1949
|
+
if (spec === "none") return fallback;
|
|
1950
|
+
if (spec === void 0 || typeof spec === "string") {
|
|
1951
|
+
const preset2 = PRESET_FRAMES[spec ?? "fade"];
|
|
1952
|
+
return {
|
|
1953
|
+
frames: preset2[phase],
|
|
1954
|
+
duration: DEFAULT_DURATION3,
|
|
1955
|
+
easing: DEFAULT_EASING
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
const custom = phase === "enter" ? spec.enter : spec.exit;
|
|
1959
|
+
if (custom) {
|
|
1960
|
+
return {
|
|
1961
|
+
frames: custom,
|
|
1962
|
+
duration: spec.duration ?? DEFAULT_DURATION3,
|
|
1963
|
+
easing: spec.easing ?? DEFAULT_EASING
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
if (spec.preset === "none") return fallback;
|
|
1967
|
+
const preset = PRESET_FRAMES[spec.preset ?? "fade"];
|
|
1968
|
+
return {
|
|
1969
|
+
frames: preset[phase],
|
|
1970
|
+
duration: spec.duration ?? DEFAULT_DURATION3,
|
|
1971
|
+
easing: spec.easing ?? DEFAULT_EASING
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
function runAnchorTransition(el, spec, phase) {
|
|
1975
|
+
const respectReduced = typeof spec === "object" ? spec.respectReducedMotion !== false : true;
|
|
1976
|
+
if (respectReduced && prefersReducedMotion()) return INSTANT;
|
|
1977
|
+
const { frames, duration, easing } = resolveTransitionFrames(spec, phase);
|
|
1978
|
+
if (!frames || duration <= 0) return INSTANT;
|
|
1979
|
+
if (typeof el.animate !== "function") return INSTANT;
|
|
1980
|
+
const animation = el.animate(frames, {
|
|
1981
|
+
duration,
|
|
1982
|
+
easing,
|
|
1983
|
+
fill: "both"
|
|
1984
|
+
});
|
|
1985
|
+
const finished = animation.finished.then(() => void 0).catch(() => void 0);
|
|
1986
|
+
return {
|
|
1987
|
+
finished,
|
|
1988
|
+
cancel: () => {
|
|
1989
|
+
try {
|
|
1990
|
+
animation.cancel();
|
|
1991
|
+
} catch {
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// src/core/features/sheet-anchors.ts
|
|
1999
|
+
function attachAnchor(deps, opts) {
|
|
2000
|
+
const doc = deps.host.ownerDocument;
|
|
2001
|
+
const wrapper = doc.createElement("div");
|
|
2002
|
+
wrapper.className = "bs-anchor";
|
|
2003
|
+
const interactive = opts.interactive ?? true;
|
|
2004
|
+
const position = opts.position ?? "sheet-top-right";
|
|
2005
|
+
const isDock = position === "dock-bottom" || position === "dock-top";
|
|
2006
|
+
const ws = wrapper.style;
|
|
2007
|
+
ws.position = "fixed";
|
|
2008
|
+
if (isDock) {
|
|
2009
|
+
ws.left = "0";
|
|
2010
|
+
ws.right = "0";
|
|
2011
|
+
if (position === "dock-bottom") {
|
|
2012
|
+
ws.bottom = "0";
|
|
2013
|
+
} else {
|
|
2014
|
+
ws.top = "0";
|
|
2015
|
+
}
|
|
2016
|
+
} else {
|
|
2017
|
+
applyOverlayPosition(ws, deps.mode, position, opts.inset ?? "16px");
|
|
2018
|
+
}
|
|
2019
|
+
if (opts.fadeRange) {
|
|
2020
|
+
const [r0, r1] = opts.fadeRange;
|
|
2021
|
+
const span = Math.max(r1 - r0, 1e-4);
|
|
2022
|
+
ws.opacity = `clamp(0, calc((var(--bs-progress, 0) - ${r0}) / ${span}), 1)`;
|
|
2023
|
+
}
|
|
2024
|
+
wrapper.appendChild(opts.element);
|
|
2025
|
+
deps.host.appendChild(wrapper);
|
|
2026
|
+
let visible = null;
|
|
2027
|
+
let inFlight = null;
|
|
2028
|
+
const shouldShow = (state) => {
|
|
2029
|
+
if (typeof opts.showOn === "function") return opts.showOn(state);
|
|
2030
|
+
if (Array.isArray(opts.showOn)) return opts.showOn.includes(state.activeId);
|
|
2031
|
+
return isDock || state.size > 0;
|
|
2032
|
+
};
|
|
2033
|
+
const applyVisibility = (next, animate) => {
|
|
2034
|
+
if (next === visible) return;
|
|
2035
|
+
visible = next;
|
|
2036
|
+
inFlight?.cancel();
|
|
2037
|
+
inFlight = null;
|
|
2038
|
+
wrapper.style.pointerEvents = next && interactive ? "auto" : "none";
|
|
2039
|
+
if (next) {
|
|
2040
|
+
wrapper.style.visibility = "";
|
|
2041
|
+
if (animate) {
|
|
2042
|
+
inFlight = runAnchorTransition(opts.element, opts.animation, "enter");
|
|
2043
|
+
}
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
if (!animate) {
|
|
2047
|
+
wrapper.style.visibility = "hidden";
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
const handle = runAnchorTransition(opts.element, opts.animation, "exit");
|
|
2051
|
+
inFlight = handle;
|
|
2052
|
+
void handle.finished.then(() => {
|
|
2053
|
+
if (inFlight === handle && visible === false) {
|
|
2054
|
+
wrapper.style.visibility = "hidden";
|
|
2055
|
+
}
|
|
2056
|
+
});
|
|
2057
|
+
};
|
|
2058
|
+
const evaluate = (animate) => {
|
|
2059
|
+
if (deps.isDestroyed()) return;
|
|
2060
|
+
applyVisibility(shouldShow(deps.getState()), animate);
|
|
2061
|
+
};
|
|
2062
|
+
const offs = [
|
|
2063
|
+
deps.on("snap", () => evaluate(true)),
|
|
2064
|
+
deps.on("open", () => evaluate(true)),
|
|
2065
|
+
deps.on("close", () => evaluate(true))
|
|
2066
|
+
];
|
|
2067
|
+
evaluate(false);
|
|
2068
|
+
return {
|
|
2069
|
+
wrapper,
|
|
2070
|
+
detach: () => {
|
|
2071
|
+
offs.forEach((off) => off());
|
|
2072
|
+
inFlight?.cancel();
|
|
2073
|
+
inFlight = null;
|
|
2074
|
+
if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
|
|
2075
|
+
},
|
|
2076
|
+
syncZ: (z) => {
|
|
2077
|
+
wrapper.style.zIndex = String(z);
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// src/core/features/scrim-stages.ts
|
|
2083
|
+
var matchesId = (def, activeId) => {
|
|
2084
|
+
if (def.for === void 0) return false;
|
|
2085
|
+
return Array.isArray(def.for) ? def.for.includes(activeId) : def.for === activeId;
|
|
2086
|
+
};
|
|
2087
|
+
var matchesRange = (def, progress) => {
|
|
2088
|
+
if (!def.forRange) return false;
|
|
2089
|
+
const [from, to] = def.forRange;
|
|
2090
|
+
return progress >= from && progress <= to;
|
|
2091
|
+
};
|
|
2092
|
+
function installScrimStages(deps, opts) {
|
|
2093
|
+
const doc = deps.host.ownerDocument;
|
|
2094
|
+
const entries = opts.stages.map((def) => {
|
|
2095
|
+
const wrapper = doc.createElement("div");
|
|
2096
|
+
wrapper.className = "bs-scrim-stage";
|
|
2097
|
+
const ws = wrapper.style;
|
|
2098
|
+
ws.position = "absolute";
|
|
2099
|
+
applyOverlayPosition(
|
|
2100
|
+
ws,
|
|
2101
|
+
deps.mode,
|
|
2102
|
+
def.position ?? opts.position ?? "center",
|
|
2103
|
+
def.inset ?? opts.inset ?? "16px"
|
|
2104
|
+
);
|
|
2105
|
+
ws.visibility = "hidden";
|
|
2106
|
+
ws.pointerEvents = "none";
|
|
2107
|
+
wrapper.appendChild(def.element);
|
|
2108
|
+
deps.host.appendChild(wrapper);
|
|
2109
|
+
return { def, wrapper, inFlight: null };
|
|
2110
|
+
});
|
|
2111
|
+
let active = null;
|
|
2112
|
+
const resolveActive = () => {
|
|
2113
|
+
const state = deps.getState();
|
|
2114
|
+
if (state.size === 0) {
|
|
2115
|
+
return entries.find((e) => matchesId(e.def, state.activeId)) ?? null;
|
|
2116
|
+
}
|
|
2117
|
+
return entries.find((e) => matchesId(e.def, state.activeId)) ?? entries.find((e) => matchesRange(e.def, state.progress)) ?? null;
|
|
2118
|
+
};
|
|
2119
|
+
const hide = (entry, animate) => {
|
|
2120
|
+
entry.inFlight?.cancel();
|
|
2121
|
+
entry.wrapper.style.pointerEvents = "none";
|
|
2122
|
+
if (!animate) {
|
|
2123
|
+
entry.inFlight = null;
|
|
2124
|
+
entry.wrapper.style.visibility = "hidden";
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
const handle = runAnchorTransition(
|
|
2128
|
+
entry.def.element,
|
|
2129
|
+
entry.def.animation ?? opts.animation,
|
|
2130
|
+
"exit"
|
|
2131
|
+
);
|
|
2132
|
+
entry.inFlight = handle;
|
|
2133
|
+
void handle.finished.then(() => {
|
|
2134
|
+
if (entry.inFlight === handle && active !== entry) {
|
|
2135
|
+
entry.wrapper.style.visibility = "hidden";
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
};
|
|
2139
|
+
const show = (entry, animate) => {
|
|
2140
|
+
entry.inFlight?.cancel();
|
|
2141
|
+
entry.inFlight = null;
|
|
2142
|
+
entry.wrapper.style.visibility = "";
|
|
2143
|
+
entry.wrapper.style.pointerEvents = entry.def.interactive ?? opts.interactive ?? false ? "auto" : "none";
|
|
2144
|
+
if (animate) {
|
|
2145
|
+
entry.inFlight = runAnchorTransition(
|
|
2146
|
+
entry.def.element,
|
|
2147
|
+
entry.def.animation ?? opts.animation,
|
|
2148
|
+
"enter"
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
};
|
|
2152
|
+
const update = (animate) => {
|
|
2153
|
+
if (deps.isDestroyed()) return;
|
|
2154
|
+
const next = resolveActive();
|
|
2155
|
+
if (next === active) return;
|
|
2156
|
+
const prev = active;
|
|
2157
|
+
active = next;
|
|
2158
|
+
if (prev) hide(prev, animate);
|
|
2159
|
+
if (next) show(next, animate);
|
|
2160
|
+
};
|
|
2161
|
+
const offs = [
|
|
2162
|
+
deps.on("snap", () => update(true)),
|
|
2163
|
+
deps.on("open", () => update(true)),
|
|
2164
|
+
deps.on("close", () => update(true))
|
|
2165
|
+
];
|
|
2166
|
+
if (entries.some((e) => e.def.forRange)) {
|
|
2167
|
+
offs.push(deps.on("progress", () => update(true)));
|
|
2168
|
+
}
|
|
2169
|
+
update(false);
|
|
2170
|
+
return () => {
|
|
2171
|
+
offs.forEach((off) => off());
|
|
2172
|
+
for (const entry of entries) {
|
|
2173
|
+
entry.inFlight?.cancel();
|
|
2174
|
+
entry.inFlight = null;
|
|
2175
|
+
if (entry.wrapper.parentNode) {
|
|
2176
|
+
entry.wrapper.parentNode.removeChild(entry.wrapper);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
active = null;
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2029
2183
|
// src/core/primitives/engine-options.ts
|
|
2030
2184
|
var DEFAULT_FLICK_VELOCITY = 0.65;
|
|
2031
2185
|
var DEFAULT_DRAG_THRESHOLD = 18;
|
|
@@ -2050,9 +2204,6 @@ function resolveEngineOptions(opts, extras) {
|
|
|
2050
2204
|
scrimMode: opts.scrimMode,
|
|
2051
2205
|
scrimColor: opts.scrimColor,
|
|
2052
2206
|
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
2207
|
scrimInteractive: opts.scrimInteractive ?? (opts.scrimTapToClose ? true : void 0),
|
|
2057
2208
|
scrimTapToClose: opts.scrimTapToClose,
|
|
2058
2209
|
scrimPreset: opts.scrimPreset,
|
|
@@ -2073,10 +2224,6 @@ function resolveEngineOptions(opts, extras) {
|
|
|
2073
2224
|
closeOnEscape: opts.closeOnEscape,
|
|
2074
2225
|
lockBodyScroll: opts.lockBodyScroll,
|
|
2075
2226
|
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
2227
|
shouldApplyInertSiblings: extras?.shouldApplyInertSiblings
|
|
2081
2228
|
}
|
|
2082
2229
|
};
|
|
@@ -2111,6 +2258,7 @@ var BottomSheetEngine = class {
|
|
|
2111
2258
|
__publicField(this, "scrollContainer");
|
|
2112
2259
|
__publicField(this, "backdrop");
|
|
2113
2260
|
__publicField(this, "screenComponent");
|
|
2261
|
+
__publicField(this, "scrimParent", null);
|
|
2114
2262
|
__publicField(this, "mode");
|
|
2115
2263
|
__publicField(this, "flickVelocity");
|
|
2116
2264
|
__publicField(this, "dragThreshold");
|
|
@@ -2121,40 +2269,22 @@ var BottomSheetEngine = class {
|
|
|
2121
2269
|
__publicField(this, "lifecycle");
|
|
2122
2270
|
__publicField(this, "closeOnBack");
|
|
2123
2271
|
__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
2272
|
__publicField(this, "snapPointsRaw");
|
|
2127
2273
|
__publicField(this, "snaps");
|
|
2128
2274
|
__publicField(this, "activeId");
|
|
2129
2275
|
__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
2276
|
__publicField(this, "gesture");
|
|
2134
2277
|
__publicField(this, "rootEl", null);
|
|
2135
2278
|
__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
2279
|
__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
2280
|
__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
2281
|
__publicField(this, "sizeWriteSentinel", new WriteSentinel());
|
|
2148
2282
|
__publicField(this, "progressWriteSentinel", new WriteSentinel());
|
|
2149
2283
|
__publicField(this, "isTopSheet", true);
|
|
2150
|
-
// Pooled payload — identity documented in SheetEventMap.progress JSDoc;
|
|
2151
|
-
// consumers must not retain across frames.
|
|
2152
2284
|
__publicField(this, "progressPayload", {
|
|
2153
2285
|
value: 0,
|
|
2154
2286
|
size: 0
|
|
2155
2287
|
});
|
|
2156
|
-
// Pooled buffer for `getDragContext()` — onMove calls this 60-120×/s.
|
|
2157
|
-
// Inner `range` object identity is also stable (mutated in place).
|
|
2158
2288
|
__publicField(this, "dragContextBuf", {
|
|
2159
2289
|
size: 0,
|
|
2160
2290
|
maxAxisSize: 0,
|
|
@@ -2165,13 +2295,13 @@ var BottomSheetEngine = class {
|
|
|
2165
2295
|
__publicField(this, "persistKey");
|
|
2166
2296
|
__publicField(this, "linkedSheets", []);
|
|
2167
2297
|
__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
2298
|
__publicField(this, "teardowns", new TeardownStack());
|
|
2173
|
-
|
|
2174
|
-
|
|
2299
|
+
__publicField(this, "anchors", []);
|
|
2300
|
+
__publicField(this, "anchorHost", null);
|
|
2301
|
+
__publicField(this, "stackZ", 100);
|
|
2302
|
+
__publicField(this, "detachScrimStages", null);
|
|
2303
|
+
__publicField(this, "stackEffectEnabled", false);
|
|
2304
|
+
__publicField(this, "stackEffectPrimed", false);
|
|
2175
2305
|
__publicField(this, "transformTemplate");
|
|
2176
2306
|
const resolved = resolveEngineOptions(opts, {
|
|
2177
2307
|
shouldApplyInertSiblings: () => {
|
|
@@ -2186,7 +2316,8 @@ var BottomSheetEngine = class {
|
|
|
2186
2316
|
this.handle = opts.handle ?? opts.element;
|
|
2187
2317
|
this.scrollContainer = opts.scrollContainer;
|
|
2188
2318
|
this.backdrop = opts.backdrop;
|
|
2189
|
-
this.screenComponent = opts.screenComponent;
|
|
2319
|
+
this.screenComponent = opts.scrim ?? opts.screenComponent;
|
|
2320
|
+
this.scrimParent = this.screenComponent?.parentElement ?? null;
|
|
2190
2321
|
this.mode = resolved.mode;
|
|
2191
2322
|
this.transformTemplate = buildTransformTemplate(this.mode);
|
|
2192
2323
|
this.aria = new AriaSliderWriter(this.handle, this.mode);
|
|
@@ -2204,6 +2335,9 @@ var BottomSheetEngine = class {
|
|
|
2204
2335
|
getResolvedSnaps: () => this.snaps.getResolvedSnaps().slice(),
|
|
2205
2336
|
snapTo: (id) => {
|
|
2206
2337
|
void this.snapTo(id);
|
|
2338
|
+
},
|
|
2339
|
+
close: () => {
|
|
2340
|
+
void this.close();
|
|
2207
2341
|
}
|
|
2208
2342
|
},
|
|
2209
2343
|
resolved.scrim
|
|
@@ -2214,8 +2348,6 @@ var BottomSheetEngine = class {
|
|
|
2214
2348
|
getRootEl: () => this.rootEl,
|
|
2215
2349
|
applySize: (size) => this.applySize(size),
|
|
2216
2350
|
getSize: () => this.size,
|
|
2217
|
-
// Optional chain protects synthetic test paths that drive animateTo
|
|
2218
|
-
// before attach() (gesture is constructed there).
|
|
2219
2351
|
isDragging: () => this.gesture?.isDragging ?? false
|
|
2220
2352
|
},
|
|
2221
2353
|
resolved.animation
|
|
@@ -2229,6 +2361,7 @@ var BottomSheetEngine = class {
|
|
|
2229
2361
|
);
|
|
2230
2362
|
this.closeOnBack = resolved.closeOnBack;
|
|
2231
2363
|
this.routedTo = opts.routedTo;
|
|
2364
|
+
this.stackEffectEnabled = opts.stackEffect ?? false;
|
|
2232
2365
|
this.snapPointsRaw = opts.snapPoints;
|
|
2233
2366
|
this.persistKey = opts.persistKey;
|
|
2234
2367
|
if (opts.linkedSheets) {
|
|
@@ -2241,7 +2374,7 @@ var BottomSheetEngine = class {
|
|
|
2241
2374
|
this.snapPointsRaw,
|
|
2242
2375
|
resolved.initialAllowed,
|
|
2243
2376
|
this.mode,
|
|
2244
|
-
() => this.handle.offsetHeight,
|
|
2377
|
+
() => layoutAxis(this.mode) === "width" ? this.handle.offsetWidth : this.handle.offsetHeight,
|
|
2245
2378
|
(maxAxisSize) => {
|
|
2246
2379
|
this.element.style[layoutAxis(this.mode)] = `${maxAxisSize}px`;
|
|
2247
2380
|
}
|
|
@@ -2320,21 +2453,6 @@ var BottomSheetEngine = class {
|
|
|
2320
2453
|
on(event, fn) {
|
|
2321
2454
|
return this.bus.on(event, fn);
|
|
2322
2455
|
}
|
|
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
2456
|
use(plugin) {
|
|
2339
2457
|
if (this.destroyed) {
|
|
2340
2458
|
console.warn(
|
|
@@ -2344,9 +2462,6 @@ var BottomSheetEngine = class {
|
|
|
2344
2462
|
}
|
|
2345
2463
|
const scoped = [];
|
|
2346
2464
|
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
2465
|
add: (fn) => {
|
|
2351
2466
|
if (typeof fn === "function") scoped.push(fn);
|
|
2352
2467
|
}
|
|
@@ -2375,36 +2490,13 @@ var BottomSheetEngine = class {
|
|
|
2375
2490
|
}
|
|
2376
2491
|
return this;
|
|
2377
2492
|
}
|
|
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
2493
|
setLinkedSheets(sheets) {
|
|
2385
2494
|
if (this.destroyed) return;
|
|
2386
2495
|
this.linkedSheets = sheets;
|
|
2387
2496
|
}
|
|
2388
|
-
/** Read-only view of the current allow-list, for cross-sheet target negotiation. */
|
|
2389
2497
|
getAllowedIds() {
|
|
2390
2498
|
return this.snaps.getAllowedIds().slice();
|
|
2391
2499
|
}
|
|
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
2500
|
async snapTo(id, velocityOrOpts = 0, _skipBeforeSnap = false) {
|
|
2409
2501
|
if (this.destroyed) return;
|
|
2410
2502
|
const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
|
|
@@ -2463,16 +2555,6 @@ var BottomSheetEngine = class {
|
|
|
2463
2555
|
this.handleClose();
|
|
2464
2556
|
}
|
|
2465
2557
|
}
|
|
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
2558
|
async dragTo(targetSize, velocityOrOpts = 0) {
|
|
2477
2559
|
if (this.destroyed) return;
|
|
2478
2560
|
const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
|
|
@@ -2502,28 +2584,13 @@ var BottomSheetEngine = class {
|
|
|
2502
2584
|
}
|
|
2503
2585
|
setAllowed(ids, snap) {
|
|
2504
2586
|
if (this.destroyed) return;
|
|
2505
|
-
const willSnap = snap !== void 0 && ids.includes(snap) || !ids.includes(this.activeId) && ids.length > 0;
|
|
2506
|
-
if (!willSnap) {
|
|
2507
|
-
this.animation.cancel();
|
|
2508
|
-
this.newCycle();
|
|
2509
|
-
}
|
|
2510
2587
|
this.snaps.setAllowedIds(ids);
|
|
2511
2588
|
this.updateAriaSlider();
|
|
2512
2589
|
if (snap && ids.includes(snap)) {
|
|
2513
|
-
void this.snapTo(
|
|
2514
|
-
snap,
|
|
2515
|
-
0,
|
|
2516
|
-
/* internal */
|
|
2517
|
-
true
|
|
2518
|
-
);
|
|
2590
|
+
void this.snapTo(snap, 0, true);
|
|
2519
2591
|
} else if (!ids.includes(this.activeId)) {
|
|
2520
2592
|
const fallback = ids[0];
|
|
2521
|
-
if (fallback) void this.snapTo(
|
|
2522
|
-
fallback,
|
|
2523
|
-
0,
|
|
2524
|
-
/* internal */
|
|
2525
|
-
true
|
|
2526
|
-
);
|
|
2593
|
+
if (fallback) void this.snapTo(fallback, 0, true);
|
|
2527
2594
|
}
|
|
2528
2595
|
}
|
|
2529
2596
|
setBackdropRange(range) {
|
|
@@ -2541,21 +2608,6 @@ var BottomSheetEngine = class {
|
|
|
2541
2608
|
setScrimInteractive(interactive) {
|
|
2542
2609
|
this.scrim.setScrimInteractive(interactive);
|
|
2543
2610
|
}
|
|
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
2611
|
setScrim(opts) {
|
|
2560
2612
|
this.scrim.setScrim(opts, () => this.applySize(this.size));
|
|
2561
2613
|
}
|
|
@@ -2571,11 +2623,79 @@ var BottomSheetEngine = class {
|
|
|
2571
2623
|
setScrimOverlay(opts) {
|
|
2572
2624
|
return this.scrim.setScrimOverlay(opts);
|
|
2573
2625
|
}
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2626
|
+
addAnchor(opts) {
|
|
2627
|
+
if (this.destroyed || typeof document === "undefined") return () => {
|
|
2628
|
+
};
|
|
2629
|
+
if (!this.anchorHost) {
|
|
2630
|
+
this.anchorHost = this.rootEl ?? this.element.parentElement ?? document.body;
|
|
2631
|
+
this.anchorHost.style.setProperty("--bs-size", `${this.size}px`);
|
|
2632
|
+
this.anchorHost.style.setProperty(
|
|
2633
|
+
"--bs-progress",
|
|
2634
|
+
String(this.computeProgress(this.size))
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2637
|
+
const handle = attachAnchor(
|
|
2638
|
+
{
|
|
2639
|
+
mode: this.mode,
|
|
2640
|
+
host: this.anchorHost,
|
|
2641
|
+
getState: () => ({
|
|
2642
|
+
activeId: this.activeId,
|
|
2643
|
+
size: this.size,
|
|
2644
|
+
progress: this.computeProgress(this.size)
|
|
2645
|
+
}),
|
|
2646
|
+
on: (event, fn) => this.on(event, fn),
|
|
2647
|
+
isDestroyed: () => this.destroyed
|
|
2648
|
+
},
|
|
2649
|
+
opts
|
|
2650
|
+
);
|
|
2651
|
+
handle.syncZ(this.stackZ + 1);
|
|
2652
|
+
this.anchors.push(handle);
|
|
2653
|
+
const detach = () => {
|
|
2654
|
+
const idx = this.anchors.indexOf(handle);
|
|
2655
|
+
if (idx === -1) return;
|
|
2656
|
+
this.anchors.splice(idx, 1);
|
|
2657
|
+
handle.detach();
|
|
2658
|
+
};
|
|
2659
|
+
this.teardowns.add(detach);
|
|
2660
|
+
return detach;
|
|
2661
|
+
}
|
|
2662
|
+
setScrimStages(opts) {
|
|
2663
|
+
if (this.destroyed || typeof document === "undefined") return () => {
|
|
2664
|
+
};
|
|
2665
|
+
this.detachScrimStages?.();
|
|
2666
|
+
this.detachScrimStages = null;
|
|
2667
|
+
if (!opts) return () => {
|
|
2668
|
+
};
|
|
2669
|
+
const host = this.screenComponent?.parentElement;
|
|
2670
|
+
if (!host) {
|
|
2671
|
+
console.warn(
|
|
2672
|
+
"[BottomSheet] setScrimStages: no scrim element mounted \u2014 pass `scrim` to the engine first."
|
|
2673
|
+
);
|
|
2674
|
+
return () => {
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
const detachInstalled = installScrimStages(
|
|
2678
|
+
{
|
|
2679
|
+
mode: this.mode,
|
|
2680
|
+
host,
|
|
2681
|
+
getState: () => ({
|
|
2682
|
+
activeId: this.activeId,
|
|
2683
|
+
size: this.size,
|
|
2684
|
+
progress: this.computeProgress(this.size)
|
|
2685
|
+
}),
|
|
2686
|
+
on: (event, fn) => this.on(event, fn),
|
|
2687
|
+
isDestroyed: () => this.destroyed
|
|
2688
|
+
},
|
|
2689
|
+
opts
|
|
2690
|
+
);
|
|
2691
|
+
const detach = () => {
|
|
2692
|
+
if (this.detachScrimStages === detach) this.detachScrimStages = null;
|
|
2693
|
+
detachInstalled();
|
|
2694
|
+
};
|
|
2695
|
+
this.detachScrimStages = detach;
|
|
2696
|
+
this.teardowns.add(() => this.detachScrimStages?.());
|
|
2697
|
+
return detach;
|
|
2698
|
+
}
|
|
2579
2699
|
getScrimState() {
|
|
2580
2700
|
return { mode: this.scrim.scrimMode, enabled: this.scrim.scrimEnabled };
|
|
2581
2701
|
}
|
|
@@ -2621,24 +2741,11 @@ var BottomSheetEngine = class {
|
|
|
2621
2741
|
emit(event, payload) {
|
|
2622
2742
|
this.bus.emit(event, payload);
|
|
2623
2743
|
}
|
|
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
2744
|
newCycle() {
|
|
2631
2745
|
this.currentAbort.abort();
|
|
2632
2746
|
this.currentAbort = new AbortController();
|
|
2633
2747
|
return this.currentAbort.signal;
|
|
2634
2748
|
}
|
|
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
2749
|
emitBeforeSnap(target, previousId) {
|
|
2643
2750
|
let cancelled = false;
|
|
2644
2751
|
let frozen = false;
|
|
@@ -2672,59 +2779,54 @@ var BottomSheetEngine = class {
|
|
|
2672
2779
|
this.snaps.setRaw(this.snapPointsRaw);
|
|
2673
2780
|
this.scrim.invalidateOpacityCache();
|
|
2674
2781
|
}
|
|
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
2782
|
getAllowedRange() {
|
|
2681
2783
|
return this.snaps.getAllowedRange();
|
|
2682
2784
|
}
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
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
|
-
*/
|
|
2785
|
+
allowedIdsBySize() {
|
|
2786
|
+
return this.snaps.getAllowedIds().slice().sort(
|
|
2787
|
+
(a, b) => (this.snaps.findById(a)?.size ?? 0) - (this.snaps.findById(b)?.size ?? 0)
|
|
2788
|
+
);
|
|
2789
|
+
}
|
|
2699
2790
|
updateAriaSlider() {
|
|
2700
|
-
this.aria.setValue(this.
|
|
2791
|
+
this.aria.setValue(this.allowedIdsBySize(), this.activeId);
|
|
2701
2792
|
}
|
|
2702
2793
|
registerInStack() {
|
|
2703
2794
|
this.teardowns.add(sheetStack.push({
|
|
2704
2795
|
id: this.id,
|
|
2705
2796
|
setZIndex: (z) => {
|
|
2797
|
+
this.stackZ = z;
|
|
2706
2798
|
this.element.style.zIndex = String(z);
|
|
2707
2799
|
if (this.backdrop) this.backdrop.style.zIndex = String(z - 1);
|
|
2800
|
+
for (const anchor of this.anchors) anchor.syncZ(z + 1);
|
|
2708
2801
|
},
|
|
2709
2802
|
setIsTop: (isTop) => {
|
|
2710
2803
|
this.isTopSheet = isTop;
|
|
2711
2804
|
if (this.backdrop) {
|
|
2712
2805
|
this.backdrop.style.display = isTop ? "" : "none";
|
|
2713
2806
|
}
|
|
2714
|
-
}
|
|
2807
|
+
},
|
|
2808
|
+
isOpen: () => this.size > 0,
|
|
2809
|
+
setDepth: (depth) => this.applyStackDepth(depth)
|
|
2715
2810
|
}));
|
|
2716
2811
|
}
|
|
2812
|
+
applyStackDepth(depth) {
|
|
2813
|
+
if (!this.stackEffectEnabled) return;
|
|
2814
|
+
this.element.setAttribute("data-stack-depth", String(depth));
|
|
2815
|
+
if (!this.stackEffectPrimed) {
|
|
2816
|
+
this.stackEffectPrimed = true;
|
|
2817
|
+
const isVertical = this.mode === "bottom" || this.mode === "top";
|
|
2818
|
+
this.element.style.transformOrigin = isVertical ? "50% 100%" : "0% 50%";
|
|
2819
|
+
this.element.style.transition = "scale 320ms cubic-bezier(0.29, 1.04, 0.84, 0.99)";
|
|
2820
|
+
}
|
|
2821
|
+
const scale = Math.max(1 - depth * 0.04, 0.86);
|
|
2822
|
+
this.element.style.scale = scale === 1 ? "" : String(scale);
|
|
2823
|
+
}
|
|
2717
2824
|
attach() {
|
|
2718
2825
|
this.gesture = new GestureController({
|
|
2719
2826
|
handle: this.handle,
|
|
2720
2827
|
element: this.element,
|
|
2721
2828
|
mode: this.mode,
|
|
2722
2829
|
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
2830
|
getDragContext: () => {
|
|
2729
2831
|
const buf = this.dragContextBuf;
|
|
2730
2832
|
const r = this.getAllowedRange();
|
|
@@ -2778,7 +2880,7 @@ var BottomSheetEngine = class {
|
|
|
2778
2880
|
container: this.scrollContainer,
|
|
2779
2881
|
isDragging: () => this.gesture?.isDragging ?? false,
|
|
2780
2882
|
isAnimating: () => this.animation.isAnimating,
|
|
2781
|
-
getAllowedIds: () => this.
|
|
2883
|
+
getAllowedIds: () => this.allowedIdsBySize(),
|
|
2782
2884
|
getActiveId: () => this.activeId,
|
|
2783
2885
|
snapTo: (id) => {
|
|
2784
2886
|
void this.snapTo(id);
|
|
@@ -2807,11 +2909,13 @@ var BottomSheetEngine = class {
|
|
|
2807
2909
|
},
|
|
2808
2910
|
newCycle: () => {
|
|
2809
2911
|
this.newCycle();
|
|
2810
|
-
}
|
|
2912
|
+
},
|
|
2913
|
+
isAnimating: () => this.animation.isAnimating,
|
|
2914
|
+
resyncAfterCancel: () => this.resyncAfterResize()
|
|
2811
2915
|
}));
|
|
2812
2916
|
if (this.lifecycle.closeOnEscape) {
|
|
2813
2917
|
const onKey = (e) => {
|
|
2814
|
-
if (this.destroyed) return;
|
|
2918
|
+
if (this.destroyed || e.defaultPrevented) return;
|
|
2815
2919
|
if (e.key === "Escape" && this.isTopSheet && this.size > 0) {
|
|
2816
2920
|
void this.close();
|
|
2817
2921
|
}
|
|
@@ -2843,7 +2947,7 @@ var BottomSheetEngine = class {
|
|
|
2843
2947
|
handle: this.handle,
|
|
2844
2948
|
mode: this.mode,
|
|
2845
2949
|
isDestroyed: () => this.destroyed,
|
|
2846
|
-
getAllowedIds: () => this.
|
|
2950
|
+
getAllowedIds: () => this.allowedIdsBySize(),
|
|
2847
2951
|
getActiveId: () => this.activeId,
|
|
2848
2952
|
snapTo: (id) => {
|
|
2849
2953
|
void this.snapTo(id);
|
|
@@ -2864,6 +2968,7 @@ var BottomSheetEngine = class {
|
|
|
2864
2968
|
});
|
|
2865
2969
|
if (!target) return;
|
|
2866
2970
|
const previousId = this.activeId;
|
|
2971
|
+
if (target.id === previousId && target.size === this.size) return;
|
|
2867
2972
|
if (this.emitBeforeSnap(target, previousId)) {
|
|
2868
2973
|
const restore = this.snaps.findById(previousId);
|
|
2869
2974
|
if (restore) {
|
|
@@ -2872,6 +2977,7 @@ var BottomSheetEngine = class {
|
|
|
2872
2977
|
return;
|
|
2873
2978
|
}
|
|
2874
2979
|
const previousSize = this.size;
|
|
2980
|
+
const previousSnapSize = this.snaps.findById(previousId)?.size;
|
|
2875
2981
|
this.scrollCache.cache(previousId, previousSize, target.size);
|
|
2876
2982
|
this.activeId = target.id;
|
|
2877
2983
|
void this.animation.animateTo(target.size, velocity).then(() => {
|
|
@@ -2880,9 +2986,10 @@ var BottomSheetEngine = class {
|
|
|
2880
2986
|
this.updateAriaSlider();
|
|
2881
2987
|
this.emit("snap", { id: target.id, size: target.size });
|
|
2882
2988
|
this.haptic();
|
|
2883
|
-
if (
|
|
2989
|
+
if (previousSnapSize === 0 && target.size > 0) {
|
|
2884
2990
|
this.emit("open", { id: target.id });
|
|
2885
2991
|
this.handleOpen();
|
|
2992
|
+
notifyLinkedSheets(this.linkedSheets, this);
|
|
2886
2993
|
}
|
|
2887
2994
|
if (target.size === 0) {
|
|
2888
2995
|
this.emit("close", void 0);
|
|
@@ -2890,7 +2997,6 @@ var BottomSheetEngine = class {
|
|
|
2890
2997
|
}
|
|
2891
2998
|
});
|
|
2892
2999
|
}
|
|
2893
|
-
/** Trigger a brief haptic tick (iOS Safari + Android Chrome support). */
|
|
2894
3000
|
haptic() {
|
|
2895
3001
|
if (typeof navigator !== "undefined" && "vibrate" in navigator) {
|
|
2896
3002
|
try {
|
|
@@ -2906,6 +3012,12 @@ var BottomSheetEngine = class {
|
|
|
2906
3012
|
style.transform = this.transformTemplate(offset);
|
|
2907
3013
|
if (this.sizeWriteSentinel.shouldWrite(size, SIZE_WRITE_EPSILON)) {
|
|
2908
3014
|
style.setProperty("--bs-size", `${size}px`);
|
|
3015
|
+
if (this.scrimParent) {
|
|
3016
|
+
this.scrimParent.style.setProperty("--bs-size", `${size}px`);
|
|
3017
|
+
}
|
|
3018
|
+
if (this.anchorHost && this.anchorHost !== this.scrimParent) {
|
|
3019
|
+
this.anchorHost.style.setProperty("--bs-size", `${size}px`);
|
|
3020
|
+
}
|
|
2909
3021
|
}
|
|
2910
3022
|
const progress = this.computeProgress(size);
|
|
2911
3023
|
const progressChanged = this.progressWriteSentinel.shouldWrite(
|
|
@@ -2914,6 +3026,9 @@ var BottomSheetEngine = class {
|
|
|
2914
3026
|
);
|
|
2915
3027
|
if (progressChanged) {
|
|
2916
3028
|
style.setProperty("--bs-progress", String(progress));
|
|
3029
|
+
if (this.anchorHost) {
|
|
3030
|
+
this.anchorHost.style.setProperty("--bs-progress", String(progress));
|
|
3031
|
+
}
|
|
2917
3032
|
}
|
|
2918
3033
|
this.scrim.applyOpacity(progress, progressChanged);
|
|
2919
3034
|
if (progressChanged && this.bus.listenerCount("progress") > 0) {
|
|
@@ -2928,6 +3043,7 @@ var BottomSheetEngine = class {
|
|
|
2928
3043
|
return Math.min(Math.max((size - min) / (max - min), 0), 1);
|
|
2929
3044
|
}
|
|
2930
3045
|
handleOpen() {
|
|
3046
|
+
sheetStack.promote(this.id);
|
|
2931
3047
|
try {
|
|
2932
3048
|
this.lifecycle.install();
|
|
2933
3049
|
} catch (err) {
|
|
@@ -2941,8 +3057,24 @@ var BottomSheetEngine = class {
|
|
|
2941
3057
|
});
|
|
2942
3058
|
}
|
|
2943
3059
|
}
|
|
3060
|
+
resyncAfterResize() {
|
|
3061
|
+
if (this.destroyed) return;
|
|
3062
|
+
const target = this.snaps.findById(this.activeId);
|
|
3063
|
+
if (!target) return;
|
|
3064
|
+
this.updateAriaSlider();
|
|
3065
|
+
this.emit("snap", { id: target.id, size: target.size });
|
|
3066
|
+
if (target.size > 0 && !this.lifecycle.isInstalled) {
|
|
3067
|
+
this.emit("open", { id: target.id });
|
|
3068
|
+
this.handleOpen();
|
|
3069
|
+
notifyLinkedSheets(this.linkedSheets, this);
|
|
3070
|
+
} else if (target.size === 0 && this.lifecycle.isInstalled) {
|
|
3071
|
+
this.emit("close", void 0);
|
|
3072
|
+
this.handleClose();
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
2944
3075
|
handleClose() {
|
|
2945
3076
|
this.lifecycle.release();
|
|
3077
|
+
sheetStack.update();
|
|
2946
3078
|
}
|
|
2947
3079
|
};
|
|
2948
3080
|
var BottomSheet = component$((props) => {
|
|
@@ -2950,6 +3082,7 @@ var BottomSheet = component$((props) => {
|
|
|
2950
3082
|
const handleRef = useSignal();
|
|
2951
3083
|
const contentRef = useSignal();
|
|
2952
3084
|
const backdropRef = useSignal();
|
|
3085
|
+
const scrimRef = useSignal();
|
|
2953
3086
|
const initialActive = props.initial ?? props.snapPoints[0]?.id ?? "default";
|
|
2954
3087
|
const state = useStore({
|
|
2955
3088
|
size: 0,
|
|
@@ -2958,6 +3091,7 @@ var BottomSheet = component$((props) => {
|
|
|
2958
3091
|
isAnimating: false,
|
|
2959
3092
|
progress: 0
|
|
2960
3093
|
});
|
|
3094
|
+
const engineStore = useStore({ engine: void 0 });
|
|
2961
3095
|
useVisibleTask$(({ cleanup }) => {
|
|
2962
3096
|
if (!sheetRef.value) return;
|
|
2963
3097
|
if (!handleRef.value || !contentRef.value) {
|
|
@@ -2970,9 +3104,8 @@ var BottomSheet = component$((props) => {
|
|
|
2970
3104
|
handle: handleRef.value,
|
|
2971
3105
|
scrollContainer: contentRef.value,
|
|
2972
3106
|
backdrop: props.backdrop !== false ? backdropRef.value : void 0,
|
|
3107
|
+
scrim: scrimRef.value,
|
|
2973
3108
|
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
3109
|
snapPoints: props.snapPoints,
|
|
2977
3110
|
allowed: props.allowed,
|
|
2978
3111
|
initial: props.initial,
|
|
@@ -2986,6 +3119,7 @@ var BottomSheet = component$((props) => {
|
|
|
2986
3119
|
backdropRange: props.backdropRange
|
|
2987
3120
|
};
|
|
2988
3121
|
const engine = new BottomSheetEngine(engineOpts);
|
|
3122
|
+
engineStore.engine = noSerialize(engine);
|
|
2989
3123
|
const sync = () => {
|
|
2990
3124
|
const s = engine.state;
|
|
2991
3125
|
state.size = s.size;
|
|
@@ -3002,13 +3136,27 @@ var BottomSheet = component$((props) => {
|
|
|
3002
3136
|
});
|
|
3003
3137
|
engine.on("dragstart", sync);
|
|
3004
3138
|
engine.on("dragend", sync);
|
|
3005
|
-
cleanup(() =>
|
|
3139
|
+
cleanup(() => {
|
|
3140
|
+
engineStore.engine = void 0;
|
|
3141
|
+
engine.destroy();
|
|
3142
|
+
});
|
|
3143
|
+
});
|
|
3144
|
+
useTask$(({ track }) => {
|
|
3145
|
+
const points = track(() => props.snapPoints);
|
|
3146
|
+
const allowed = track(() => props.allowed);
|
|
3147
|
+
const engine = engineStore.engine;
|
|
3148
|
+
if (!engine) return;
|
|
3149
|
+
engine.setSnapPoints(
|
|
3150
|
+
points,
|
|
3151
|
+
allowed
|
|
3152
|
+
);
|
|
3006
3153
|
});
|
|
3007
3154
|
const showBackdrop = props.backdrop !== false;
|
|
3008
3155
|
const mode = props.mode ?? "bottom";
|
|
3009
3156
|
const ariaLabel = props.ariaLabel ?? "Bottom sheet";
|
|
3010
3157
|
return /* @__PURE__ */ jsxs("div", { class: "bs-root", children: [
|
|
3011
3158
|
showBackdrop ? /* @__PURE__ */ jsx("div", { class: "bs-backdrop", ref: backdropRef }) : null,
|
|
3159
|
+
/* @__PURE__ */ jsx("div", { class: "bs-screen", ref: scrimRef, children: /* @__PURE__ */ jsx(Slot, { name: "screen" }) }),
|
|
3012
3160
|
/* @__PURE__ */ jsxs(
|
|
3013
3161
|
"section",
|
|
3014
3162
|
{
|