@patternmode/stacksheet 1.3.6 → 1.3.8
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/dist/Drag/drag-geometry.d.ts.map +1 -1
- package/dist/Drag/drag-velocity.d.ts +24 -0
- package/dist/Drag/drag-velocity.d.ts.map +1 -0
- package/dist/Drag/use-drag.d.ts.map +1 -1
- package/dist/SheetPanel/default-header.d.ts +1 -1
- package/dist/SheetPanel/default-header.d.ts.map +1 -1
- package/dist/SheetPanel/sheet-panel-content.d.ts +1 -1
- package/dist/SheetPanel/sheet-panel-content.d.ts.map +1 -1
- package/dist/SheetPanel/sheet-panel-focus.d.ts +1 -1
- package/dist/SheetPanel/sheet-panel-focus.d.ts.map +1 -1
- package/dist/SheetPanel/sheet-panel-handles.d.ts +2 -2
- package/dist/SheetPanel/sheet-panel-handles.d.ts.map +1 -1
- package/dist/SheetPanel/sheet-panel-layout.d.ts.map +1 -1
- package/dist/SheetPanel/sheet-panel.d.ts +1 -1
- package/dist/SheetPanel/sheet-panel.d.ts.map +1 -1
- package/dist/SheetParts/sheet-back.d.ts +1 -1
- package/dist/SheetParts/sheet-back.d.ts.map +1 -1
- package/dist/SheetParts/sheet-body.d.ts +1 -1
- package/dist/SheetParts/sheet-body.d.ts.map +1 -1
- package/dist/SheetParts/sheet-close.d.ts +1 -1
- package/dist/SheetParts/sheet-close.d.ts.map +1 -1
- package/dist/SheetParts/sheet-description.d.ts +1 -1
- package/dist/SheetParts/sheet-description.d.ts.map +1 -1
- package/dist/SheetParts/sheet-footer.d.ts +1 -1
- package/dist/SheetParts/sheet-footer.d.ts.map +1 -1
- package/dist/SheetParts/sheet-handle.d.ts +1 -1
- package/dist/SheetParts/sheet-handle.d.ts.map +1 -1
- package/dist/SheetParts/sheet-header.d.ts +1 -1
- package/dist/SheetParts/sheet-header.d.ts.map +1 -1
- package/dist/SheetParts/sheet-parts.d.ts +8 -8
- package/dist/SheetParts/sheet-title.d.ts +1 -1
- package/dist/SheetParts/sheet-title.d.ts.map +1 -1
- package/dist/Store/create-sheet-store.d.ts.map +1 -1
- package/dist/Store/store-args.d.ts +17 -0
- package/dist/Store/store-args.d.ts.map +1 -1
- package/dist/create.d.ts.map +1 -1
- package/dist/icons.d.ts +2 -2
- package/dist/icons.d.ts.map +1 -1
- package/dist/index.mjs +710 -436
- package/dist/index.mjs.map +1 -1
- package/dist/media.d.ts.map +1 -1
- package/dist/panel-context.d.ts +4 -0
- package/dist/panel-context.d.ts.map +1 -1
- package/dist/renderer-effects.d.ts.map +1 -1
- package/dist/renderer-helpers.d.ts +9 -1
- package/dist/renderer-helpers.d.ts.map +1 -1
- package/dist/renderer.d.ts +2 -2
- package/dist/renderer.d.ts.map +1 -1
- package/dist/snap-points.d.ts.map +1 -1
- package/dist/springs.d.ts +11 -17
- package/dist/springs.d.ts.map +1 -1
- package/dist/stacking.d.ts +2 -2
- package/dist/stacking.d.ts.map +1 -1
- package/dist/types.d.ts +31 -33
- package/dist/types.d.ts.map +1 -1
- package/package.json +13 -12
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Portal } from "@radix-ui/react-portal";
|
|
3
|
-
import { createContext, use, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { createContext, use, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { createStore, useStore } from "zustand";
|
|
5
5
|
import { useShallow } from "zustand/react/shallow";
|
|
6
|
+
import { springs as springs$1 } from "@howells/motion";
|
|
6
7
|
import { AnimatePresence, LazyMotion, domMax, m, useReducedMotion } from "motion/react";
|
|
7
8
|
import { RemoveScroll } from "react-remove-scroll";
|
|
8
9
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -11,29 +12,28 @@ import { joinClassNames } from "@patternmode/system";
|
|
|
11
12
|
import { Slot } from "@radix-ui/react-slot";
|
|
12
13
|
import { Root, Scrollbar, Thumb, Viewport } from "@radix-ui/react-scroll-area";
|
|
13
14
|
//#region src/springs.ts
|
|
15
|
+
/** Strips the shared token's `type: "spring"` down to Stacksheet's numeric SpringConfig. */
|
|
16
|
+
const toSpringConfig = (spring) => ({
|
|
17
|
+
damping: spring.damping,
|
|
18
|
+
mass: spring.mass,
|
|
19
|
+
stiffness: spring.stiffness
|
|
20
|
+
});
|
|
14
21
|
/**
|
|
15
22
|
* Spring presets inspired by iOS animation feel.
|
|
16
23
|
*
|
|
17
|
-
* - `subtle` — Barely noticeable bounce, professional.
|
|
18
|
-
* - `snappy` — Quick, responsive for interactions.
|
|
19
|
-
* - `stiff` — Very quick, controlled. Panels, drawers. **(default)**
|
|
24
|
+
* - `subtle` — Barely noticeable bounce, professional. (shared @howells/motion token)
|
|
25
|
+
* - `snappy` — Quick, responsive for interactions. (shared @howells/motion token)
|
|
26
|
+
* - `stiff` — Very quick, controlled. Panels, drawers. **(default)** Intentional
|
|
27
|
+
* fork: damping 40 (tighter than the shared `stiff`) for drawer control.
|
|
20
28
|
*/
|
|
21
29
|
const springs = {
|
|
22
|
-
snappy:
|
|
23
|
-
damping: 28,
|
|
24
|
-
mass: .8,
|
|
25
|
-
stiffness: 400
|
|
26
|
-
},
|
|
30
|
+
snappy: toSpringConfig(springs$1.snappy),
|
|
27
31
|
stiff: {
|
|
28
32
|
damping: 40,
|
|
29
33
|
mass: 1,
|
|
30
34
|
stiffness: 400
|
|
31
35
|
},
|
|
32
|
-
subtle:
|
|
33
|
-
damping: 30,
|
|
34
|
-
mass: 1,
|
|
35
|
-
stiffness: 300
|
|
36
|
-
}
|
|
36
|
+
subtle: toSpringConfig(springs$1.subtle)
|
|
37
37
|
};
|
|
38
38
|
//#endregion
|
|
39
39
|
//#region src/config.ts
|
|
@@ -114,9 +114,13 @@ const useIsMobile = (breakpoint) => {
|
|
|
114
114
|
useEffect(() => {
|
|
115
115
|
const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
|
|
116
116
|
setIsMobile(mql.matches);
|
|
117
|
-
const handler = (e) =>
|
|
117
|
+
const handler = (e) => {
|
|
118
|
+
setIsMobile(e.matches);
|
|
119
|
+
};
|
|
118
120
|
mql.addEventListener("change", handler);
|
|
119
|
-
return () =>
|
|
121
|
+
return () => {
|
|
122
|
+
mql.removeEventListener("change", handler);
|
|
123
|
+
};
|
|
120
124
|
}, [breakpoint]);
|
|
121
125
|
return isMobile;
|
|
122
126
|
};
|
|
@@ -130,13 +134,17 @@ const usePanelHeight = (panelRef, hasSnapPoints) => {
|
|
|
130
134
|
const [height, setHeight] = useState(0);
|
|
131
135
|
useEffect(() => {
|
|
132
136
|
const el = panelRef.current;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
let observer;
|
|
138
|
+
if (el !== null && hasSnapPoints) {
|
|
139
|
+
setHeight(el.offsetHeight);
|
|
140
|
+
observer = new ResizeObserver(([entry]) => {
|
|
141
|
+
if (entry) setHeight(entry.contentRect.height);
|
|
142
|
+
});
|
|
143
|
+
observer.observe(el);
|
|
144
|
+
}
|
|
145
|
+
return () => {
|
|
146
|
+
observer?.disconnect();
|
|
147
|
+
};
|
|
140
148
|
}, [panelRef, hasSnapPoints]);
|
|
141
149
|
return height;
|
|
142
150
|
};
|
|
@@ -144,40 +152,68 @@ const getViewportHeight = () => typeof window === "undefined" ? 0 : window.visua
|
|
|
144
152
|
const useViewportHeight = (active) => {
|
|
145
153
|
const [height, setHeight] = useState(() => typeof window === "undefined" ? void 0 : getViewportHeight());
|
|
146
154
|
useEffect(() => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
window
|
|
155
|
+
const update = () => {
|
|
156
|
+
setHeight(getViewportHeight());
|
|
157
|
+
};
|
|
158
|
+
const canListen = typeof window !== "undefined";
|
|
159
|
+
if (canListen) {
|
|
160
|
+
window.addEventListener("resize", update);
|
|
161
|
+
window.visualViewport?.addEventListener("resize", update);
|
|
162
|
+
}
|
|
151
163
|
return () => {
|
|
152
|
-
|
|
153
|
-
|
|
164
|
+
if (canListen) {
|
|
165
|
+
window.removeEventListener("resize", update);
|
|
166
|
+
window.visualViewport?.removeEventListener("resize", update);
|
|
167
|
+
}
|
|
154
168
|
};
|
|
155
169
|
}, []);
|
|
156
170
|
return active ? height ?? 0 : 0;
|
|
157
171
|
};
|
|
172
|
+
const BODY_SCALE_TRANSITION = "transform 500ms cubic-bezier(0.32, 0.72, 0, 1), border-radius 500ms cubic-bezier(0.32, 0.72, 0, 1)";
|
|
173
|
+
/** Fallback delay (transition duration + margin) if `transitionend` never fires. */
|
|
174
|
+
const BODY_SCALE_RESET_FALLBACK_MS = 600;
|
|
175
|
+
/** Cancels a pending un-scale reset when the sheet reopens mid-animation. */
|
|
176
|
+
let cancelPendingBodyScaleReset;
|
|
158
177
|
const useBodyScale = (config, isOpen, prefersReducedMotion) => {
|
|
159
178
|
useEffect(() => {
|
|
160
|
-
if (!config.shouldScaleBackground || prefersReducedMotion) return;
|
|
161
179
|
const wrapper = document.querySelector("[data-stacksheet-wrapper]");
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return;
|
|
180
|
+
const scalable = config.shouldScaleBackground && !prefersReducedMotion && wrapper instanceof HTMLElement && isOpen ? wrapper : null;
|
|
181
|
+
if (scalable !== null) {
|
|
182
|
+
cancelPendingBodyScaleReset?.();
|
|
183
|
+
scalable.style.transition = BODY_SCALE_TRANSITION;
|
|
184
|
+
scalable.style.transform = `scale(${config.scaleBackgroundAmount})`;
|
|
185
|
+
scalable.style.borderRadius = "8px";
|
|
186
|
+
scalable.style.overflow = "hidden";
|
|
187
|
+
scalable.style.transformOrigin = "center top";
|
|
171
188
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
189
|
+
return () => {
|
|
190
|
+
if (scalable === null) return;
|
|
191
|
+
scalable.style.transition = BODY_SCALE_TRANSITION;
|
|
192
|
+
scalable.style.transform = "";
|
|
193
|
+
scalable.style.borderRadius = "";
|
|
194
|
+
const controller = new AbortController();
|
|
195
|
+
const { signal } = controller;
|
|
196
|
+
const finish = () => {
|
|
197
|
+
controller.abort();
|
|
198
|
+
cancelPendingBodyScaleReset = void 0;
|
|
199
|
+
scalable.style.transition = "";
|
|
200
|
+
scalable.style.overflow = "";
|
|
201
|
+
scalable.style.transformOrigin = "";
|
|
202
|
+
};
|
|
203
|
+
scalable.addEventListener("transitionend", (event) => {
|
|
204
|
+
if (event.target === scalable && event.propertyName === "transform") finish();
|
|
205
|
+
}, { signal });
|
|
206
|
+
const timeoutId = setTimeout(() => {
|
|
207
|
+
if (!signal.aborted) finish();
|
|
208
|
+
}, BODY_SCALE_RESET_FALLBACK_MS);
|
|
209
|
+
signal.addEventListener("abort", () => {
|
|
210
|
+
clearTimeout(timeoutId);
|
|
211
|
+
});
|
|
212
|
+
cancelPendingBodyScaleReset = () => {
|
|
213
|
+
controller.abort();
|
|
214
|
+
cancelPendingBodyScaleReset = void 0;
|
|
215
|
+
};
|
|
178
216
|
};
|
|
179
|
-
wrapper.addEventListener("transitionend", handleEnd, { once: true });
|
|
180
|
-
return () => wrapper.removeEventListener("transitionend", handleEnd);
|
|
181
217
|
}, [
|
|
182
218
|
isOpen,
|
|
183
219
|
config.shouldScaleBackground,
|
|
@@ -187,7 +223,14 @@ const useBodyScale = (config, isOpen, prefersReducedMotion) => {
|
|
|
187
223
|
};
|
|
188
224
|
//#endregion
|
|
189
225
|
//#region src/snap-points.ts
|
|
190
|
-
const SNAP_POINT_RE = /^(
|
|
226
|
+
const SNAP_POINT_RE = /^(?<value>\d+(?:\.\d+)?)(?<unit>px|rem|em|vh|%)$/u;
|
|
227
|
+
const getRootFontSize = () => typeof document === "undefined" ? 16 : Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
228
|
+
const resolveUnitPx = (value, unit, viewportHeight) => {
|
|
229
|
+
if (unit === "px") return value;
|
|
230
|
+
if (unit === "rem" || unit === "em") return value * getRootFontSize();
|
|
231
|
+
if (unit === "vh" || unit === "%") return value / 100 * viewportHeight;
|
|
232
|
+
return 0;
|
|
233
|
+
};
|
|
191
234
|
/**
|
|
192
235
|
* Resolve a snap point value to pixels given the viewport height.
|
|
193
236
|
* - number 0-1: fraction of viewport (0.5 → 50% of vh)
|
|
@@ -197,18 +240,11 @@ const SNAP_POINT_RE = /^(\d+(?:\.\d+)?)(px|rem|em|vh|%)$/u;
|
|
|
197
240
|
const resolveSnapPointPx = (point, viewportHeight) => {
|
|
198
241
|
if (typeof point === "number") return point <= 1 ? point * viewportHeight : point;
|
|
199
242
|
if (typeof point === "string") {
|
|
200
|
-
const match =
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
case "px": return value;
|
|
206
|
-
case "rem":
|
|
207
|
-
case "em": return value * (typeof document === "undefined" ? 16 : Number.parseFloat(getComputedStyle(document.documentElement).fontSize));
|
|
208
|
-
case "vh":
|
|
209
|
-
case "%": return value / 100 * viewportHeight;
|
|
210
|
-
default: return 0;
|
|
211
|
-
}
|
|
243
|
+
const match = SNAP_POINT_RE.exec(point);
|
|
244
|
+
const rawValue = match?.groups?.value;
|
|
245
|
+
const unit = match?.groups?.unit;
|
|
246
|
+
if (rawValue === void 0 || unit === void 0) return 0;
|
|
247
|
+
return resolveUnitPx(Number.parseFloat(rawValue), unit, viewportHeight);
|
|
212
248
|
}
|
|
213
249
|
return 0;
|
|
214
250
|
};
|
|
@@ -294,12 +330,13 @@ const resolveClassNames = (cn) => {
|
|
|
294
330
|
panel: cn.panel ?? ""
|
|
295
331
|
};
|
|
296
332
|
};
|
|
297
|
-
const buildAriaProps = (
|
|
333
|
+
const buildAriaProps = ({ ariaLabel, hasDescription, hasTitle, isComposable, isModal, isTop, panelId }) => {
|
|
298
334
|
if (!isTop) return {};
|
|
299
335
|
const props = { role: "dialog" };
|
|
300
336
|
if (isModal) props["aria-modal"] = "true";
|
|
301
337
|
if (isComposable) {
|
|
302
|
-
props["aria-labelledby"] = `${panelId}-title`;
|
|
338
|
+
if (hasTitle) props["aria-labelledby"] = `${panelId}-title`;
|
|
339
|
+
else props["aria-label"] = ariaLabel;
|
|
303
340
|
if (hasDescription) props["aria-describedby"] = `${panelId}-desc`;
|
|
304
341
|
} else props["aria-label"] = ariaLabel;
|
|
305
342
|
return props;
|
|
@@ -397,7 +434,7 @@ const RUBBER_BAND_FACTOR = .6;
|
|
|
397
434
|
//#region src/Drag/drag-geometry.ts
|
|
398
435
|
const isInteractiveElement = (el) => {
|
|
399
436
|
if (INTERACTIVE_TAGS.has(el.tagName)) return true;
|
|
400
|
-
if (el.isContentEditable) return true;
|
|
437
|
+
if (el instanceof HTMLElement && el.isContentEditable) return true;
|
|
401
438
|
if (el.closest("button, a, input, textarea, select, [contenteditable]")) return true;
|
|
402
439
|
if (el.closest("[data-stacksheet-no-drag]")) return true;
|
|
403
440
|
return false;
|
|
@@ -473,95 +510,108 @@ const classifyGesture = (dx, dy, axis, sign) => {
|
|
|
473
510
|
/** Decide whether a pointer gesture commits as a drag or should be ignored. */
|
|
474
511
|
const commitGesture = (dx, dy, axis, sign, scrollEl) => {
|
|
475
512
|
if (classifyGesture(dx, dy, axis, sign) === "none") return "none";
|
|
476
|
-
if (scrollEl && !isAtScrollEdge(scrollEl, axis, sign)) return "none";
|
|
513
|
+
if (scrollEl !== null && !isAtScrollEdge(scrollEl, axis, sign)) return "none";
|
|
477
514
|
return "drag";
|
|
478
515
|
};
|
|
479
516
|
const getPanelDimension = (panel, axis) => {
|
|
480
517
|
if (!panel) return 300;
|
|
481
518
|
return axis === "x" ? panel.offsetWidth : panel.offsetHeight;
|
|
482
519
|
};
|
|
483
|
-
|
|
484
|
-
|
|
520
|
+
/** Upper bound on retained samples — plenty for a 100ms window at 120Hz. */
|
|
521
|
+
const MAX_SAMPLES = 20;
|
|
485
522
|
/**
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
*
|
|
523
|
+
* Append a pointer sample, pruning entries that fall outside the sliding
|
|
524
|
+
* window (plus a hard cap as a memory guard). Mutates and returns `samples`.
|
|
525
|
+
*/
|
|
526
|
+
const appendVelocitySample = (samples, sample) => {
|
|
527
|
+
samples.push(sample);
|
|
528
|
+
const cutoff = sample.time - 100;
|
|
529
|
+
while (samples.length > MAX_SAMPLES || samples.length > 2 && (samples[0]?.time ?? 0) < cutoff) samples.shift();
|
|
530
|
+
return samples;
|
|
531
|
+
};
|
|
532
|
+
/**
|
|
533
|
+
* Compute release velocity (px/ms, positive = dismiss direction) from the
|
|
534
|
+
* samples recorded within the sliding window before `releaseTime`.
|
|
497
535
|
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
536
|
+
* Using only recent samples means a pause followed by a flick reports the
|
|
537
|
+
* flick's velocity — not the whole-gesture average, which would dilute it
|
|
538
|
+
* to near zero.
|
|
500
539
|
*/
|
|
501
|
-
const
|
|
502
|
-
const
|
|
503
|
-
const
|
|
504
|
-
const
|
|
505
|
-
const
|
|
506
|
-
|
|
540
|
+
const getReleaseVelocity = (samples, releaseTime) => {
|
|
541
|
+
const cutoff = releaseTime - 100;
|
|
542
|
+
const recent = samples.filter((sample) => sample.time >= cutoff);
|
|
543
|
+
const [first] = recent;
|
|
544
|
+
const last = recent.at(-1);
|
|
545
|
+
if (first === void 0 || last === void 0 || last.time <= first.time) return 0;
|
|
546
|
+
return (last.offset - first.offset) / (last.time - first.time);
|
|
547
|
+
};
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region src/Drag/use-drag.ts
|
|
550
|
+
const resetDragRefs = ({ committedRef, offsetRef, samplesRef, scrollTargetRef, startRef }) => {
|
|
551
|
+
startRef.current = null;
|
|
552
|
+
committedRef.current = null;
|
|
553
|
+
offsetRef.current = 0;
|
|
554
|
+
samplesRef.current = [];
|
|
555
|
+
scrollTargetRef.current = null;
|
|
556
|
+
};
|
|
557
|
+
const createDragHandlers = ({ axis, config, onDragUpdate, panelRef, refs, sign }) => {
|
|
558
|
+
const dismiss = () => {
|
|
559
|
+
if (config.isNested) config.onPop();
|
|
560
|
+
else config.onClose();
|
|
561
|
+
};
|
|
507
562
|
const handlePointerDown = (e) => {
|
|
508
|
-
if (!config.enabled) return;
|
|
509
|
-
|
|
510
|
-
const
|
|
511
|
-
if (!target) return;
|
|
512
|
-
const isHandle = !!target.closest("[data-stacksheet-handle]");
|
|
563
|
+
if (!config.enabled || e.button !== 0 || !(e.target instanceof Element)) return;
|
|
564
|
+
const { target } = e;
|
|
565
|
+
const isHandle = target.closest("[data-stacksheet-handle]") !== null;
|
|
513
566
|
if (!isHandle && isInteractiveElement(target)) return;
|
|
514
|
-
scrollTargetRef.current = isHandle ? null : findScrollableAncestor(target, axis);
|
|
515
|
-
startRef.current = {
|
|
516
|
-
time: Date.now(),
|
|
567
|
+
refs.scrollTargetRef.current = isHandle ? null : findScrollableAncestor(target, axis);
|
|
568
|
+
refs.startRef.current = {
|
|
517
569
|
x: e.clientX,
|
|
518
570
|
y: e.clientY
|
|
519
571
|
};
|
|
520
|
-
committedRef.current = null;
|
|
521
|
-
offsetRef.current = 0;
|
|
522
|
-
|
|
572
|
+
refs.committedRef.current = null;
|
|
573
|
+
refs.offsetRef.current = 0;
|
|
574
|
+
refs.samplesRef.current = [{
|
|
575
|
+
offset: 0,
|
|
576
|
+
time: Date.now()
|
|
577
|
+
}];
|
|
523
578
|
};
|
|
524
579
|
const handlePointerMove = (e) => {
|
|
525
|
-
if (
|
|
526
|
-
const dx = e.clientX - startRef.current.x;
|
|
527
|
-
const dy = e.clientY - startRef.current.y;
|
|
580
|
+
if (refs.startRef.current === null) return;
|
|
581
|
+
const dx = e.clientX - refs.startRef.current.x;
|
|
582
|
+
const dy = e.clientY - refs.startRef.current.y;
|
|
528
583
|
const dist = Math.hypot(dx, dy);
|
|
529
|
-
if (committedRef.current === null && dist < 10) return;
|
|
530
|
-
if (committedRef.current === null) {
|
|
531
|
-
committedRef.current = commitGesture(dx, dy, axis, sign, scrollTargetRef.current);
|
|
532
|
-
if (committedRef.current !== "drag") {
|
|
533
|
-
startRef.current = null;
|
|
584
|
+
if (refs.committedRef.current === null && dist < 10) return;
|
|
585
|
+
if (refs.committedRef.current === null) {
|
|
586
|
+
refs.committedRef.current = commitGesture(dx, dy, axis, sign, refs.scrollTargetRef.current);
|
|
587
|
+
if (refs.committedRef.current !== "drag") {
|
|
588
|
+
refs.startRef.current = null;
|
|
534
589
|
return;
|
|
535
590
|
}
|
|
591
|
+
if (e.currentTarget instanceof HTMLElement) e.currentTarget.setPointerCapture(e.pointerId);
|
|
536
592
|
}
|
|
537
|
-
if (committedRef.current !== "drag") return;
|
|
593
|
+
if (refs.committedRef.current !== "drag") return;
|
|
538
594
|
const directional = (axis === "x" ? dx : dy) * sign;
|
|
539
595
|
const clampedOffset = directional >= 0 ? directional : -Math.sqrt(Math.abs(directional)) * RUBBER_BAND_FACTOR;
|
|
540
|
-
offsetRef.current = clampedOffset;
|
|
596
|
+
refs.offsetRef.current = clampedOffset;
|
|
597
|
+
appendVelocitySample(refs.samplesRef.current, {
|
|
598
|
+
offset: clampedOffset,
|
|
599
|
+
time: Date.now()
|
|
600
|
+
});
|
|
541
601
|
onDragUpdate({
|
|
542
602
|
isDragging: true,
|
|
543
603
|
offset: clampedOffset
|
|
544
604
|
});
|
|
545
605
|
e.preventDefault();
|
|
546
606
|
};
|
|
547
|
-
const
|
|
548
|
-
if (
|
|
549
|
-
|
|
550
|
-
};
|
|
551
|
-
const handlePointerUp = (_e) => {
|
|
552
|
-
if (!startRef.current || committedRef.current !== "drag") {
|
|
553
|
-
startRef.current = null;
|
|
554
|
-
committedRef.current = null;
|
|
555
|
-
scrollTargetRef.current = null;
|
|
607
|
+
const handlePointerUp = () => {
|
|
608
|
+
if (refs.startRef.current === null || refs.committedRef.current !== "drag") {
|
|
609
|
+
resetDragRefs(refs);
|
|
556
610
|
return;
|
|
557
611
|
}
|
|
558
|
-
const offset = Math.max(0, offsetRef.current);
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
startRef.current = null;
|
|
562
|
-
committedRef.current = null;
|
|
563
|
-
offsetRef.current = 0;
|
|
564
|
-
scrollTargetRef.current = null;
|
|
612
|
+
const offset = Math.max(0, refs.offsetRef.current);
|
|
613
|
+
const velocity = getReleaseVelocity(refs.samplesRef.current, Date.now());
|
|
614
|
+
resetDragRefs(refs);
|
|
565
615
|
const panelSize = getPanelDimension(panelRef.current, axis);
|
|
566
616
|
if (config.snapHeights.length > 0) {
|
|
567
617
|
const targetIndex = findSnapTarget(offset, panelSize, config.snapHeights, velocity, config.activeSnapIndex, config.sequential);
|
|
@@ -584,15 +634,56 @@ const useDrag = (panelRef, config, onDragUpdate) => {
|
|
|
584
634
|
});
|
|
585
635
|
};
|
|
586
636
|
const handlePointerCancel = () => {
|
|
587
|
-
|
|
588
|
-
committedRef.current = null;
|
|
589
|
-
offsetRef.current = 0;
|
|
590
|
-
scrollTargetRef.current = null;
|
|
637
|
+
resetDragRefs(refs);
|
|
591
638
|
onDragUpdate({
|
|
592
639
|
isDragging: false,
|
|
593
640
|
offset: 0
|
|
594
641
|
});
|
|
595
642
|
};
|
|
643
|
+
return {
|
|
644
|
+
handlePointerCancel,
|
|
645
|
+
handlePointerDown,
|
|
646
|
+
handlePointerMove,
|
|
647
|
+
handlePointerUp
|
|
648
|
+
};
|
|
649
|
+
};
|
|
650
|
+
/**
|
|
651
|
+
* Hook that manages drag gestures that can dismiss a sheet panel.
|
|
652
|
+
*
|
|
653
|
+
* Gesture pipeline:
|
|
654
|
+
* 1. Dead zone (10px) — ignores micro-movements
|
|
655
|
+
* 2. Angle check (35°) — must be roughly aligned with dismiss axis
|
|
656
|
+
* 3. Scroll conflict — yields to scrollable containers not at edge
|
|
657
|
+
* 4. Commit — drag is active, applies offset via `onDragUpdate`
|
|
658
|
+
* 5. Release — velocity + threshold determine close/snap/bounce-back
|
|
659
|
+
*
|
|
660
|
+
* Opposite-direction drag uses √(offset) damping for elastic
|
|
661
|
+
* rubber-band resistance (same physics as iOS over-scroll).
|
|
662
|
+
*
|
|
663
|
+
* When `snapHeights` is provided, release targeting uses
|
|
664
|
+
* `findSnapTarget()` instead of the simple threshold check.
|
|
665
|
+
*/
|
|
666
|
+
const useDrag = (panelRef, config, onDragUpdate) => {
|
|
667
|
+
const startRef = useRef(null);
|
|
668
|
+
const committedRef = useRef(null);
|
|
669
|
+
const offsetRef = useRef(0);
|
|
670
|
+
const samplesRef = useRef([]);
|
|
671
|
+
const scrollTargetRef = useRef(null);
|
|
672
|
+
const { axis, sign } = getDismissAxis(config.side);
|
|
673
|
+
const { handlePointerCancel, handlePointerDown, handlePointerMove, handlePointerUp } = createDragHandlers({
|
|
674
|
+
axis,
|
|
675
|
+
config,
|
|
676
|
+
onDragUpdate,
|
|
677
|
+
panelRef,
|
|
678
|
+
refs: {
|
|
679
|
+
committedRef,
|
|
680
|
+
offsetRef,
|
|
681
|
+
samplesRef,
|
|
682
|
+
scrollTargetRef,
|
|
683
|
+
startRef
|
|
684
|
+
},
|
|
685
|
+
sign
|
|
686
|
+
});
|
|
596
687
|
const handlersRef = useRef({
|
|
597
688
|
handlePointerCancel,
|
|
598
689
|
handlePointerDown,
|
|
@@ -607,20 +698,32 @@ const useDrag = (panelRef, config, onDragUpdate) => {
|
|
|
607
698
|
};
|
|
608
699
|
useEffect(() => {
|
|
609
700
|
const el = panelRef.current;
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
701
|
+
const onPointerDown = (event) => {
|
|
702
|
+
handlersRef.current.handlePointerDown(event);
|
|
703
|
+
};
|
|
704
|
+
const onPointerMove = (event) => {
|
|
705
|
+
handlersRef.current.handlePointerMove(event);
|
|
706
|
+
};
|
|
707
|
+
const onPointerUp = () => {
|
|
708
|
+
handlersRef.current.handlePointerUp();
|
|
709
|
+
};
|
|
710
|
+
const onPointerCancel = () => {
|
|
711
|
+
handlersRef.current.handlePointerCancel();
|
|
712
|
+
};
|
|
713
|
+
const canListen = el !== null && config.enabled;
|
|
714
|
+
if (canListen) {
|
|
715
|
+
el.addEventListener("pointerdown", onPointerDown);
|
|
716
|
+
el.addEventListener("pointermove", onPointerMove);
|
|
717
|
+
el.addEventListener("pointerup", onPointerUp);
|
|
718
|
+
el.addEventListener("pointercancel", onPointerCancel);
|
|
719
|
+
}
|
|
619
720
|
return () => {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
721
|
+
if (canListen) {
|
|
722
|
+
el.removeEventListener("pointerdown", onPointerDown);
|
|
723
|
+
el.removeEventListener("pointermove", onPointerMove);
|
|
724
|
+
el.removeEventListener("pointerup", onPointerUp);
|
|
725
|
+
el.removeEventListener("pointercancel", onPointerCancel);
|
|
726
|
+
}
|
|
624
727
|
};
|
|
625
728
|
}, [panelRef, config.enabled]);
|
|
626
729
|
};
|
|
@@ -795,11 +898,12 @@ const DefaultHeader = ({ isNested, onBack, onClose, className }) => /* @__PURE__
|
|
|
795
898
|
//#endregion
|
|
796
899
|
//#region src/SheetPanel/sheet-panel-content.tsx
|
|
797
900
|
const PanelInnerContent = ({ isComposable, shouldRender, Content, data, renderHeader, headerProps, headerClassName }) => {
|
|
798
|
-
if (isComposable) return shouldRender && Content ? /* @__PURE__ */ jsx(Content, { ...data }) : null;
|
|
799
|
-
|
|
901
|
+
if (isComposable) return shouldRender && Content !== void 0 ? /* @__PURE__ */ jsx(Content, { ...data }) : null;
|
|
902
|
+
const customHeader = renderHeader !== void 0 && renderHeader !== false ? renderHeader : void 0;
|
|
903
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [customHeader === void 0 ? /* @__PURE__ */ jsx(DefaultHeader, {
|
|
800
904
|
...headerProps,
|
|
801
905
|
className: headerClassName
|
|
802
|
-
}), shouldRender && Content && /* @__PURE__ */ jsx("div", {
|
|
906
|
+
}) : customHeader(headerProps), shouldRender && Content !== void 0 && /* @__PURE__ */ jsx("div", {
|
|
803
907
|
className: "min-h-0 flex-1 overflow-y-auto overscroll-contain",
|
|
804
908
|
"data-stacksheet-no-drag": "",
|
|
805
909
|
children: /* @__PURE__ */ jsx(Content, { ...data })
|
|
@@ -816,7 +920,9 @@ const LAYERED_MODAL_SELECTORS = [
|
|
|
816
920
|
].join(", ");
|
|
817
921
|
const subscribeToFocusTarget = (onStoreChange) => {
|
|
818
922
|
document.addEventListener("focusin", onStoreChange, true);
|
|
819
|
-
return () =>
|
|
923
|
+
return () => {
|
|
924
|
+
document.removeEventListener("focusin", onStoreChange, true);
|
|
925
|
+
};
|
|
820
926
|
};
|
|
821
927
|
const getServerLayeredModalFocused = () => false;
|
|
822
928
|
const getLayeredModalFocused = () => {
|
|
@@ -837,10 +943,10 @@ const ModalFocusTrap = ({ enabled, active, fallbackRef, children }) => {
|
|
|
837
943
|
allowOutsideClick: true,
|
|
838
944
|
escapeDeactivates: false,
|
|
839
945
|
fallbackFocus: () => {
|
|
840
|
-
if (fallbackRef.current) return fallbackRef.current;
|
|
946
|
+
if (fallbackRef.current !== null) return fallbackRef.current;
|
|
841
947
|
return document.body;
|
|
842
948
|
},
|
|
843
|
-
initialFocus:
|
|
949
|
+
initialFocus: () => fallbackRef.current ?? void 0,
|
|
844
950
|
returnFocusOnDeactivate: true
|
|
845
951
|
},
|
|
846
952
|
paused,
|
|
@@ -890,10 +996,18 @@ const resolvePanelLayout = (layout, renderHeader) => {
|
|
|
890
996
|
//#region src/SheetPanel/sheet-panel.tsx
|
|
891
997
|
const getPanelAriaLabel = (item, fallbackLabel) => item.ariaLabel ?? (typeof item.data?.__ariaLabel === "string" ? item.data.__ariaLabel : void 0) ?? fallbackLabel;
|
|
892
998
|
const getPanelHoverProps = (enabled, setIsHovered) => enabled ? {
|
|
893
|
-
onBlur: () =>
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
999
|
+
onBlur: () => {
|
|
1000
|
+
setIsHovered(false);
|
|
1001
|
+
},
|
|
1002
|
+
onFocus: () => {
|
|
1003
|
+
setIsHovered(true);
|
|
1004
|
+
},
|
|
1005
|
+
onMouseEnter: () => {
|
|
1006
|
+
setIsHovered(true);
|
|
1007
|
+
},
|
|
1008
|
+
onMouseLeave: () => {
|
|
1009
|
+
setIsHovered(false);
|
|
1010
|
+
}
|
|
897
1011
|
} : {};
|
|
898
1012
|
const getInactivePanelProps = (isTop) => isTop ? {} : {
|
|
899
1013
|
"aria-hidden": "true",
|
|
@@ -905,14 +1019,16 @@ const getHeaderProps = ({ close, isNested, pop, side }) => ({
|
|
|
905
1019
|
onClose: close,
|
|
906
1020
|
side
|
|
907
1021
|
});
|
|
908
|
-
const getPanelContext = ({ close, hasDescription, isNested, isTop, panelId, pop, registerDescription, side }) => ({
|
|
1022
|
+
const getPanelContext = ({ close, hasDescription, hasTitle, isNested, isTop, panelId, pop, registerDescription, registerTitle, side }) => ({
|
|
909
1023
|
back: pop,
|
|
910
1024
|
close,
|
|
911
1025
|
hasDescription,
|
|
1026
|
+
hasTitle,
|
|
912
1027
|
isNested,
|
|
913
1028
|
isTop,
|
|
914
1029
|
panelId,
|
|
915
1030
|
registerDescription,
|
|
1031
|
+
registerTitle,
|
|
916
1032
|
side
|
|
917
1033
|
});
|
|
918
1034
|
const getOptionalSideHandle = ({ isHovered, onDismiss, show, side }) => show ? /* @__PURE__ */ jsx(SideHandle, {
|
|
@@ -927,25 +1043,22 @@ const completeOpeningAnimation = (hasEnteredRef, isTop, onOpenCompleteRef) => {
|
|
|
927
1043
|
onOpenCompleteRef.current?.();
|
|
928
1044
|
}
|
|
929
1045
|
};
|
|
930
|
-
const
|
|
931
|
-
const panelRef = useRef(null);
|
|
1046
|
+
const useOpeningCompletion = (isTop, onOpenComplete) => {
|
|
932
1047
|
const hasEnteredRef = useRef(false);
|
|
933
|
-
const onOpenCompleteRef = useRef(
|
|
934
|
-
const [dragState, setDragState] = useState({
|
|
935
|
-
isDragging: false,
|
|
936
|
-
offset: 0
|
|
937
|
-
});
|
|
938
|
-
const [isHovered, setIsHovered] = useState(false);
|
|
939
|
-
const measuredHeight = usePanelHeight(panelRef, snapHeights.length > 0);
|
|
940
|
-
const transform = getStackTransform(depth, config.stacking);
|
|
941
|
-
const panelStyles = getPanelStyles(side, config, index);
|
|
1048
|
+
const onOpenCompleteRef = useRef(onOpenComplete);
|
|
942
1049
|
if (!isTop && hasEnteredRef.current) hasEnteredRef.current = false;
|
|
943
1050
|
useEffect(() => {
|
|
944
|
-
onOpenCompleteRef.current =
|
|
945
|
-
}, [
|
|
946
|
-
|
|
1051
|
+
onOpenCompleteRef.current = onOpenComplete;
|
|
1052
|
+
}, [onOpenComplete]);
|
|
1053
|
+
return () => {
|
|
947
1054
|
completeOpeningAnimation(hasEnteredRef, isTop, onOpenCompleteRef);
|
|
948
1055
|
};
|
|
1056
|
+
};
|
|
1057
|
+
const useSheetPanelDrag = ({ activeSnapIndex, config, isNested, isTop, onSnap, prefersReducedMotion, side, snapHeights, swipeClose, swipePop }, panelRef) => {
|
|
1058
|
+
const [dragState, setDragState] = useState({
|
|
1059
|
+
isDragging: false,
|
|
1060
|
+
offset: 0
|
|
1061
|
+
});
|
|
949
1062
|
useDrag(panelRef, {
|
|
950
1063
|
activeSnapIndex,
|
|
951
1064
|
closeThreshold: config.closeThreshold,
|
|
@@ -959,23 +1072,63 @@ const SheetPanel = ({ item, index, depth, isTop, isNested, side, config, classNa
|
|
|
959
1072
|
snapHeights,
|
|
960
1073
|
velocityThreshold: config.velocityThreshold
|
|
961
1074
|
}, setDragState);
|
|
962
|
-
|
|
963
|
-
|
|
1075
|
+
return dragState;
|
|
1076
|
+
};
|
|
1077
|
+
const useSheetPanelContext = ({ close, isNested, isTop, pop, side }, panelId) => {
|
|
964
1078
|
const [hasDescription, setHasDescription] = useState(false);
|
|
965
|
-
const
|
|
1079
|
+
const [hasTitle, setHasTitle] = useState(false);
|
|
1080
|
+
const registerDescription = useCallback(() => {
|
|
966
1081
|
setHasDescription(true);
|
|
967
|
-
return () =>
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1082
|
+
return () => {
|
|
1083
|
+
setHasDescription(false);
|
|
1084
|
+
};
|
|
1085
|
+
}, []);
|
|
1086
|
+
const registerTitle = useCallback(() => {
|
|
1087
|
+
setHasTitle(true);
|
|
1088
|
+
return () => {
|
|
1089
|
+
setHasTitle(false);
|
|
1090
|
+
};
|
|
1091
|
+
}, []);
|
|
1092
|
+
return {
|
|
971
1093
|
hasDescription,
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1094
|
+
hasTitle,
|
|
1095
|
+
panelContext: useMemo(() => getPanelContext({
|
|
1096
|
+
close,
|
|
1097
|
+
hasDescription,
|
|
1098
|
+
hasTitle,
|
|
1099
|
+
isNested,
|
|
1100
|
+
isTop,
|
|
1101
|
+
panelId,
|
|
1102
|
+
pop,
|
|
1103
|
+
registerDescription,
|
|
1104
|
+
registerTitle,
|
|
1105
|
+
side
|
|
1106
|
+
}), [
|
|
1107
|
+
close,
|
|
1108
|
+
hasDescription,
|
|
1109
|
+
hasTitle,
|
|
1110
|
+
isNested,
|
|
1111
|
+
isTop,
|
|
1112
|
+
panelId,
|
|
1113
|
+
pop,
|
|
1114
|
+
registerDescription,
|
|
1115
|
+
registerTitle,
|
|
1116
|
+
side
|
|
1117
|
+
])
|
|
1118
|
+
};
|
|
1119
|
+
};
|
|
1120
|
+
const useSheetPanelModel = (props) => {
|
|
1121
|
+
const { item, index, depth, isTop, isNested, side, config, classNames, pop, close, snapHeights, activeSnapIndex, layout, renderHeader, slideFrom, slideTarget, spring, stackSpring } = props;
|
|
1122
|
+
const panelRef = useRef(null);
|
|
1123
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
1124
|
+
const measuredHeight = usePanelHeight(panelRef, snapHeights.length > 0);
|
|
1125
|
+
const transform = getStackTransform(depth, config.stacking);
|
|
1126
|
+
const panelStyles = getPanelStyles(side, config, index);
|
|
1127
|
+
const handleAnimationComplete = useOpeningCompletion(isTop, config.onOpenComplete);
|
|
1128
|
+
const dragState = useSheetPanelDrag(props, panelRef);
|
|
1129
|
+
const ariaLabel = getPanelAriaLabel(item, config.ariaLabel);
|
|
1130
|
+
const panelId = `stacksheet-${item.id}`;
|
|
1131
|
+
const { hasDescription, hasTitle, panelContext } = useSheetPanelContext(props, panelId);
|
|
979
1132
|
const isComposable = resolvePanelLayout(layout, renderHeader) === "composable";
|
|
980
1133
|
const hasPanelClass = classNames.panel !== "";
|
|
981
1134
|
const dragOffset = getDragTransform(side, dragState.offset);
|
|
@@ -986,7 +1139,15 @@ const SheetPanel = ({ item, index, depth, isTop, isNested, side, config, classNa
|
|
|
986
1139
|
pop,
|
|
987
1140
|
side
|
|
988
1141
|
});
|
|
989
|
-
const ariaProps = buildAriaProps(
|
|
1142
|
+
const ariaProps = buildAriaProps({
|
|
1143
|
+
ariaLabel,
|
|
1144
|
+
hasDescription,
|
|
1145
|
+
hasTitle,
|
|
1146
|
+
isComposable,
|
|
1147
|
+
isModal: config.modal,
|
|
1148
|
+
isTop,
|
|
1149
|
+
panelId
|
|
1150
|
+
});
|
|
990
1151
|
const transition = buildPanelTransition(dragState.isDragging, isTop, spring, stackSpring);
|
|
991
1152
|
const animatedRadius = getAnimatedBorderRadius(side, depth, config.stacking);
|
|
992
1153
|
const snapYOffset = computeSnapYOffset(side, snapHeights, activeSnapIndex, measuredHeight);
|
|
@@ -1002,9 +1163,26 @@ const SheetPanel = ({ item, index, depth, isTop, isNested, side, config, classNa
|
|
|
1002
1163
|
show: showSideHandle,
|
|
1003
1164
|
side
|
|
1004
1165
|
});
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1166
|
+
return {
|
|
1167
|
+
animateTarget,
|
|
1168
|
+
ariaProps,
|
|
1169
|
+
bottomHandle: getOptionalBottomHandle(showBottomHandle, dismiss),
|
|
1170
|
+
handleAnimationComplete,
|
|
1171
|
+
headerProps,
|
|
1172
|
+
hoverProps: getPanelHoverProps(showSideHandle, setIsHovered),
|
|
1173
|
+
inactivePanelProps: getInactivePanelProps(isTop),
|
|
1174
|
+
initialRadius,
|
|
1175
|
+
isComposable,
|
|
1176
|
+
panelContext,
|
|
1177
|
+
panelRef,
|
|
1178
|
+
panelStyle,
|
|
1179
|
+
resolvedSlideFrom,
|
|
1180
|
+
sideHandle
|
|
1181
|
+
};
|
|
1182
|
+
};
|
|
1183
|
+
const SheetPanel = (props) => {
|
|
1184
|
+
const { item, isTop, config, classNames, Content, shouldRender, renderHeader, prefersReducedMotion } = props;
|
|
1185
|
+
const { animateTarget, ariaProps, bottomHandle, handleAnimationComplete, headerProps, hoverProps, inactivePanelProps, initialRadius, isComposable, panelContext, panelRef, panelStyle, resolvedSlideFrom, sideHandle } = useSheetPanelModel(props);
|
|
1008
1186
|
const panelContent = /* @__PURE__ */ jsxs(m.div, {
|
|
1009
1187
|
animate: animateTarget,
|
|
1010
1188
|
className: classNames.panel || void 0,
|
|
@@ -1079,21 +1257,27 @@ const getBackdropStyle = (config, hasBackdropClass) => ({
|
|
|
1079
1257
|
zIndex: config.zIndex,
|
|
1080
1258
|
...hasBackdropClass ? {} : { background: "var(--overlay, rgba(0, 0, 0, 0.15))" }
|
|
1081
1259
|
});
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classNamesProp, layout, renderHeader }) => {
|
|
1260
|
+
const isContentComponent = (content) => typeof content === "function";
|
|
1261
|
+
const getStaticContent = (sheets, type) => {
|
|
1262
|
+
if (sheets === void 0) return;
|
|
1263
|
+
const content = Object.entries(sheets).find(([sheetType]) => sheetType === type)?.[1];
|
|
1264
|
+
return isContentComponent(content) ? content : void 0;
|
|
1265
|
+
};
|
|
1266
|
+
const useRendererStore = (store) => {
|
|
1090
1267
|
const isOpen = useStore(store, (s) => s.isOpen);
|
|
1091
1268
|
const stack = useStore(store, (s) => s.stack);
|
|
1092
|
-
const rawClose = useStore(store, (s) =>
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1269
|
+
const { rawClose, rawPop } = useStore(store, useShallow((s) => ({
|
|
1270
|
+
rawClose: s.close,
|
|
1271
|
+
rawPop: s.pop
|
|
1272
|
+
})));
|
|
1273
|
+
return {
|
|
1274
|
+
isOpen,
|
|
1275
|
+
rawClose,
|
|
1276
|
+
rawPop,
|
|
1277
|
+
stack
|
|
1278
|
+
};
|
|
1279
|
+
};
|
|
1280
|
+
const useSnapState = (config, isOpen, side, stack) => {
|
|
1097
1281
|
const viewportHeight = useViewportHeight(isOpen && side === "bottom" && config.snapPoints.length > 0);
|
|
1098
1282
|
const snapHeights = getResolvedSnapHeights(side, config.snapPoints, viewportHeight);
|
|
1099
1283
|
const snapContext = isOpen ? stack.map((item) => item.id).join("\0") : "";
|
|
@@ -1118,6 +1302,13 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
|
|
|
1118
1302
|
});
|
|
1119
1303
|
config.onSnapPointChange?.(index);
|
|
1120
1304
|
};
|
|
1305
|
+
return {
|
|
1306
|
+
activeSnapIndex,
|
|
1307
|
+
handleSnap,
|
|
1308
|
+
snapHeights
|
|
1309
|
+
};
|
|
1310
|
+
};
|
|
1311
|
+
const useCloseControls = (rawClose, rawPop) => {
|
|
1121
1312
|
const closeReasonRef = useRef("programmatic");
|
|
1122
1313
|
const closeWith = (reason) => {
|
|
1123
1314
|
closeReasonRef.current = reason;
|
|
@@ -1127,9 +1318,19 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
|
|
|
1127
1318
|
closeReasonRef.current = reason;
|
|
1128
1319
|
rawPop();
|
|
1129
1320
|
};
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1321
|
+
return {
|
|
1322
|
+
close: () => {
|
|
1323
|
+
closeWith("programmatic");
|
|
1324
|
+
},
|
|
1325
|
+
closeReasonRef,
|
|
1326
|
+
closeWith,
|
|
1327
|
+
pop: () => {
|
|
1328
|
+
popWith("programmatic");
|
|
1329
|
+
},
|
|
1330
|
+
popWith
|
|
1331
|
+
};
|
|
1332
|
+
};
|
|
1333
|
+
const useFocusRestore = (isOpen) => {
|
|
1133
1334
|
const triggerRef = useRef(null);
|
|
1134
1335
|
const wasOpenRef = useRef(false);
|
|
1135
1336
|
useEffect(() => {
|
|
@@ -1141,56 +1342,96 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
|
|
|
1141
1342
|
}
|
|
1142
1343
|
wasOpenRef.current = isOpen;
|
|
1143
1344
|
}, [isOpen]);
|
|
1144
|
-
|
|
1345
|
+
};
|
|
1346
|
+
const dismissFromEscape = ({ closeReasonRef, rawClose, rawPop, stackLengthRef }) => {
|
|
1347
|
+
closeReasonRef.current = "escape";
|
|
1348
|
+
if (stackLengthRef.current > 1) rawPop();
|
|
1349
|
+
else rawClose();
|
|
1350
|
+
};
|
|
1351
|
+
const useDismissalEffects = ({ closeReasonRef, config, isOpen, rawClose, rawPop, stackLength }) => {
|
|
1352
|
+
const stackLengthRef = useRef(stackLength);
|
|
1145
1353
|
useEffect(() => {
|
|
1146
|
-
stackLengthRef.current =
|
|
1147
|
-
}, [
|
|
1354
|
+
stackLengthRef.current = stackLength;
|
|
1355
|
+
}, [stackLength]);
|
|
1148
1356
|
useEffect(() => {
|
|
1149
|
-
|
|
1357
|
+
const shouldListen = isOpen && config.closeOnEscape && config.dismissible;
|
|
1150
1358
|
const handleKeyDown = (e) => {
|
|
1151
|
-
if (e.key
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1359
|
+
if (e.key !== "Escape" || e.defaultPrevented) return;
|
|
1360
|
+
e.preventDefault();
|
|
1361
|
+
dismissFromEscape({
|
|
1362
|
+
closeReasonRef,
|
|
1363
|
+
rawClose,
|
|
1364
|
+
rawPop,
|
|
1365
|
+
stackLengthRef
|
|
1366
|
+
});
|
|
1367
|
+
};
|
|
1368
|
+
if (shouldListen) document.addEventListener("keydown", handleKeyDown);
|
|
1369
|
+
return () => {
|
|
1370
|
+
if (shouldListen) document.removeEventListener("keydown", handleKeyDown);
|
|
1161
1371
|
};
|
|
1162
|
-
document.addEventListener("keydown", handleKeyDown);
|
|
1163
|
-
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
1164
1372
|
}, [
|
|
1165
1373
|
isOpen,
|
|
1166
1374
|
config.closeOnEscape,
|
|
1167
1375
|
config.dismissible,
|
|
1168
1376
|
rawPop,
|
|
1169
|
-
rawClose
|
|
1377
|
+
rawClose,
|
|
1378
|
+
closeReasonRef
|
|
1170
1379
|
]);
|
|
1171
1380
|
useEffect(() => {
|
|
1172
|
-
|
|
1173
|
-
const
|
|
1381
|
+
const CloseWatcherConstructor = globalThis.CloseWatcher;
|
|
1382
|
+
const shouldListen = isOpen && config.closeOnEscape && config.dismissible && CloseWatcherConstructor !== void 0;
|
|
1383
|
+
let watcher;
|
|
1174
1384
|
const handleClose = () => {
|
|
1175
|
-
|
|
1176
|
-
closeReasonRef
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
}
|
|
1385
|
+
dismissFromEscape({
|
|
1386
|
+
closeReasonRef,
|
|
1387
|
+
rawClose,
|
|
1388
|
+
rawPop,
|
|
1389
|
+
stackLengthRef
|
|
1390
|
+
});
|
|
1182
1391
|
};
|
|
1183
|
-
|
|
1392
|
+
if (shouldListen) {
|
|
1393
|
+
watcher = new CloseWatcherConstructor();
|
|
1394
|
+
watcher.addEventListener("close", handleClose);
|
|
1395
|
+
}
|
|
1184
1396
|
return () => {
|
|
1185
|
-
watcher
|
|
1186
|
-
|
|
1397
|
+
if (watcher !== void 0) {
|
|
1398
|
+
watcher.removeEventListener("close", handleClose);
|
|
1399
|
+
watcher.destroy();
|
|
1400
|
+
}
|
|
1187
1401
|
};
|
|
1188
1402
|
}, [
|
|
1189
1403
|
isOpen,
|
|
1404
|
+
config.closeOnEscape,
|
|
1190
1405
|
config.dismissible,
|
|
1191
1406
|
rawPop,
|
|
1192
|
-
rawClose
|
|
1407
|
+
rawClose,
|
|
1408
|
+
closeReasonRef
|
|
1193
1409
|
]);
|
|
1410
|
+
};
|
|
1411
|
+
/**
|
|
1412
|
+
* Root renderer component — manages the backdrop, scroll lock, snap points,
|
|
1413
|
+
* close reasons, focus restoration, keyboard/CloseWatcher dismissal, and
|
|
1414
|
+
* delegates per-panel rendering to `SheetPanel`.
|
|
1415
|
+
*
|
|
1416
|
+
* Mounted inside a Portal by `StacksheetProvider`.
|
|
1417
|
+
*/
|
|
1418
|
+
const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classNamesProp, layout, renderHeader }) => {
|
|
1419
|
+
const { isOpen, rawClose, rawPop, stack } = useRendererStore(store);
|
|
1420
|
+
const side = useResolvedSide(config);
|
|
1421
|
+
const prefersReducedMotion = useReducedMotion() ?? false;
|
|
1422
|
+
const classNames = resolveClassNames(classNamesProp);
|
|
1423
|
+
const { activeSnapIndex, handleSnap, snapHeights } = useSnapState(config, isOpen, side, stack);
|
|
1424
|
+
const { close, closeReasonRef, closeWith, pop, popWith } = useCloseControls(rawClose, rawPop);
|
|
1425
|
+
useBodyScale(config, isOpen, prefersReducedMotion);
|
|
1426
|
+
useFocusRestore(isOpen);
|
|
1427
|
+
useDismissalEffects({
|
|
1428
|
+
closeReasonRef,
|
|
1429
|
+
config,
|
|
1430
|
+
isOpen,
|
|
1431
|
+
rawClose,
|
|
1432
|
+
rawPop,
|
|
1433
|
+
stackLength: stack.length
|
|
1434
|
+
});
|
|
1194
1435
|
const slideFrom = getSlideFrom(side);
|
|
1195
1436
|
const slideTarget = getSlideTarget();
|
|
1196
1437
|
const spring = getMotionSpring(prefersReducedMotion, config);
|
|
@@ -1201,8 +1442,12 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
|
|
|
1201
1442
|
const handleExitComplete = () => {
|
|
1202
1443
|
if (stack.length === 0) config.onCloseComplete?.(closeReasonRef.current);
|
|
1203
1444
|
};
|
|
1204
|
-
const swipeClose = () =>
|
|
1205
|
-
|
|
1445
|
+
const swipeClose = () => {
|
|
1446
|
+
closeWith("swipe");
|
|
1447
|
+
};
|
|
1448
|
+
const swipePop = () => {
|
|
1449
|
+
popWith("swipe");
|
|
1450
|
+
};
|
|
1206
1451
|
const shouldLockScroll = isOpen && isModal && config.lockScroll;
|
|
1207
1452
|
return /* @__PURE__ */ jsxs(LazyMotion, {
|
|
1208
1453
|
features: domMax,
|
|
@@ -1213,7 +1458,9 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
|
|
|
1213
1458
|
className: `fixed inset-0 ${classNames.backdrop || ""}`,
|
|
1214
1459
|
exit: { opacity: 0 },
|
|
1215
1460
|
initial: { opacity: 0 },
|
|
1216
|
-
onClick: config.closeOnBackdrop && config.dismissible ? () =>
|
|
1461
|
+
onClick: config.closeOnBackdrop && config.dismissible ? () => {
|
|
1462
|
+
closeWith("backdrop");
|
|
1463
|
+
} : void 0,
|
|
1217
1464
|
style: backdropStyle,
|
|
1218
1465
|
transition: spring
|
|
1219
1466
|
}, "stacksheet-backdrop")
|
|
@@ -1232,7 +1479,7 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
|
|
|
1232
1479
|
const shouldRender = depth <= config.stacking.renderThreshold;
|
|
1233
1480
|
return /* @__PURE__ */ jsx(SheetPanel, {
|
|
1234
1481
|
activeSnapIndex,
|
|
1235
|
-
Content: componentMap.get(item.type) ?? sheets
|
|
1482
|
+
Content: componentMap.get(item.type) ?? getStaticContent(sheets, item.type),
|
|
1236
1483
|
classNames,
|
|
1237
1484
|
close,
|
|
1238
1485
|
config,
|
|
@@ -1264,11 +1511,32 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
|
|
|
1264
1511
|
};
|
|
1265
1512
|
//#endregion
|
|
1266
1513
|
//#region src/Store/store-args.ts
|
|
1514
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
1515
|
+
const toRecord = (value) => isRecord(value) ? value : {};
|
|
1516
|
+
const isComponent = (value) => typeof value === "function";
|
|
1517
|
+
const getComponentName = (component) => {
|
|
1518
|
+
const name = component.displayName ?? component.name;
|
|
1519
|
+
return name === "" ? void 0 : name;
|
|
1520
|
+
};
|
|
1521
|
+
const getNodeEnv = () => globalThis.process?.env?.NODE_ENV;
|
|
1522
|
+
/**
|
|
1523
|
+
* Generate a unique sheet id. `crypto.randomUUID` is only available in
|
|
1524
|
+
* secure contexts, so fall back to a timestamp + random suffix on
|
|
1525
|
+
* non-secure origins (e.g. plain-HTTP LAN dev servers).
|
|
1526
|
+
*/
|
|
1527
|
+
const generateSheetId = () => {
|
|
1528
|
+
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
1529
|
+
return `sheet-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1530
|
+
};
|
|
1531
|
+
const getStringArg = (value, name) => {
|
|
1532
|
+
if (typeof value !== "string") throw new TypeError(`Expected ${name} to be a string.`);
|
|
1533
|
+
return value;
|
|
1534
|
+
};
|
|
1267
1535
|
const resolvePresentationOptions = (value) => {
|
|
1268
|
-
if (!(value
|
|
1269
|
-
const
|
|
1270
|
-
if (
|
|
1271
|
-
return
|
|
1536
|
+
if (!isRecord(value)) return;
|
|
1537
|
+
const { ariaLabel } = value;
|
|
1538
|
+
if (ariaLabel !== void 0 && typeof ariaLabel !== "string") return;
|
|
1539
|
+
return ariaLabel === void 0 ? {} : { ariaLabel };
|
|
1272
1540
|
};
|
|
1273
1541
|
/**
|
|
1274
1542
|
* Dev-mode warning: detect likely inline arrow functions passed as ad-hoc components.
|
|
@@ -1276,52 +1544,202 @@ const resolvePresentationOptions = (value) => {
|
|
|
1276
1544
|
* it's almost always an inline arrow being re-created every render.
|
|
1277
1545
|
*/
|
|
1278
1546
|
const warnInlineComponent = (component, componentRegistry, warnedNames) => {
|
|
1279
|
-
if (
|
|
1280
|
-
const name = component
|
|
1281
|
-
if (
|
|
1547
|
+
if (getNodeEnv() === "production") return;
|
|
1548
|
+
const name = getComponentName(component);
|
|
1549
|
+
if (name === void 0) return;
|
|
1282
1550
|
if (warnedNames.has(name)) return;
|
|
1283
|
-
for (const [existing, key] of componentRegistry) if ((existing
|
|
1551
|
+
for (const [existing, key] of componentRegistry) if (getComponentName(existing) === name) {
|
|
1284
1552
|
warnedNames.add(name);
|
|
1285
1553
|
console.warn(`[stacksheet] A new component reference with name "${name}" was registered (key: ${key}), but a different reference with the same name already exists. This usually means you're passing an inline arrow function (e.g. open(() => <MySheet />)). Define the component outside of render to avoid memory leaks and broken navigate() same-type detection.`);
|
|
1286
1554
|
return;
|
|
1287
1555
|
}
|
|
1288
1556
|
};
|
|
1557
|
+
const registerComponent = (component, componentRegistry, componentMap, getNextKey, warnedNames) => {
|
|
1558
|
+
const existingKey = componentRegistry.get(component);
|
|
1559
|
+
if (existingKey !== void 0) return existingKey;
|
|
1560
|
+
warnInlineComponent(component, componentRegistry, warnedNames);
|
|
1561
|
+
const nextKey = getNextKey();
|
|
1562
|
+
componentRegistry.set(component, nextKey);
|
|
1563
|
+
componentMap.set(nextKey, component);
|
|
1564
|
+
return nextKey;
|
|
1565
|
+
};
|
|
1289
1566
|
/**
|
|
1290
1567
|
* If `first` is a function (component), register it and return { type, id, data }.
|
|
1291
1568
|
* Otherwise, pass through the string-based (type, id, data) args unchanged.
|
|
1292
1569
|
*/
|
|
1293
1570
|
const resolveArgs = (componentRegistry, componentMap, getNextKey, warnedNames, first, second, third, fourth) => {
|
|
1294
|
-
if (
|
|
1295
|
-
const
|
|
1296
|
-
let typeKey = componentRegistry.get(component);
|
|
1297
|
-
if (!typeKey) {
|
|
1298
|
-
warnInlineComponent(component, componentRegistry, warnedNames);
|
|
1299
|
-
typeKey = getNextKey();
|
|
1300
|
-
componentRegistry.set(component, typeKey);
|
|
1301
|
-
componentMap.set(typeKey, component);
|
|
1302
|
-
}
|
|
1571
|
+
if (isComponent(first)) {
|
|
1572
|
+
const typeKey = registerComponent(first, componentRegistry, componentMap, getNextKey, warnedNames);
|
|
1303
1573
|
if (typeof second === "string") return {
|
|
1304
1574
|
ariaLabel: resolvePresentationOptions(fourth)?.ariaLabel,
|
|
1305
|
-
data: third
|
|
1575
|
+
data: toRecord(third),
|
|
1306
1576
|
id: second,
|
|
1307
1577
|
type: typeKey
|
|
1308
1578
|
};
|
|
1309
1579
|
return {
|
|
1310
1580
|
ariaLabel: resolvePresentationOptions(third)?.ariaLabel,
|
|
1311
|
-
data: second
|
|
1312
|
-
id:
|
|
1581
|
+
data: toRecord(second),
|
|
1582
|
+
id: generateSheetId(),
|
|
1313
1583
|
type: typeKey
|
|
1314
1584
|
};
|
|
1315
1585
|
}
|
|
1316
1586
|
return {
|
|
1317
1587
|
ariaLabel: resolvePresentationOptions(fourth)?.ariaLabel,
|
|
1318
|
-
data: third
|
|
1319
|
-
id: second,
|
|
1320
|
-
type: first
|
|
1588
|
+
data: toRecord(third),
|
|
1589
|
+
id: getStringArg(second, "sheet id"),
|
|
1590
|
+
type: getStringArg(first, "sheet type")
|
|
1321
1591
|
};
|
|
1322
1592
|
};
|
|
1323
1593
|
//#endregion
|
|
1324
1594
|
//#region src/Store/create-sheet-store.ts
|
|
1595
|
+
const createItem = ({ type, id, data, ariaLabel }) => ({
|
|
1596
|
+
ariaLabel,
|
|
1597
|
+
data,
|
|
1598
|
+
id,
|
|
1599
|
+
type
|
|
1600
|
+
});
|
|
1601
|
+
const createResolvedWriters = (set, config) => ({
|
|
1602
|
+
openResolved: (resolved) => {
|
|
1603
|
+
set({
|
|
1604
|
+
isOpen: true,
|
|
1605
|
+
stack: [createItem(resolved)]
|
|
1606
|
+
});
|
|
1607
|
+
},
|
|
1608
|
+
pushResolved: (resolved) => {
|
|
1609
|
+
set((state) => {
|
|
1610
|
+
const item = createItem(resolved);
|
|
1611
|
+
if (Number.isFinite(config.maxDepth) && state.stack.length >= config.maxDepth) return {
|
|
1612
|
+
isOpen: true,
|
|
1613
|
+
stack: [...state.stack.slice(0, -1), item]
|
|
1614
|
+
};
|
|
1615
|
+
return {
|
|
1616
|
+
isOpen: true,
|
|
1617
|
+
stack: [...state.stack, item]
|
|
1618
|
+
};
|
|
1619
|
+
});
|
|
1620
|
+
},
|
|
1621
|
+
replaceResolved: (resolved) => {
|
|
1622
|
+
set((state) => {
|
|
1623
|
+
const item = createItem(resolved);
|
|
1624
|
+
if (state.stack.length === 0) return {
|
|
1625
|
+
isOpen: true,
|
|
1626
|
+
stack: [item]
|
|
1627
|
+
};
|
|
1628
|
+
return {
|
|
1629
|
+
isOpen: true,
|
|
1630
|
+
stack: [...state.stack.slice(0, -1), item]
|
|
1631
|
+
};
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
const createNavigate = (componentMap, get, resolve, writers) => (first, second, third, fourth) => {
|
|
1636
|
+
const resolved = resolve(first, second, third, fourth);
|
|
1637
|
+
const { stack } = get();
|
|
1638
|
+
const top = stack.at(-1);
|
|
1639
|
+
if (stack.length === 0) {
|
|
1640
|
+
writers.openResolved(resolved);
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
let isSameType = top?.type === resolved.type;
|
|
1644
|
+
if (!isSameType && isComponent(first)) isSameType = componentMap.get(top?.type ?? "") === first;
|
|
1645
|
+
if (isSameType) {
|
|
1646
|
+
writers.replaceResolved(resolved);
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
writers.pushResolved(resolved);
|
|
1650
|
+
};
|
|
1651
|
+
const createPop = (set, pruneRegistry) => () => {
|
|
1652
|
+
set((state) => {
|
|
1653
|
+
if (state.stack.length <= 1) {
|
|
1654
|
+
pruneRegistry([]);
|
|
1655
|
+
return {
|
|
1656
|
+
isOpen: false,
|
|
1657
|
+
stack: []
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
const next = state.stack.slice(0, -1);
|
|
1661
|
+
pruneRegistry(next);
|
|
1662
|
+
return {
|
|
1663
|
+
isOpen: true,
|
|
1664
|
+
stack: next
|
|
1665
|
+
};
|
|
1666
|
+
});
|
|
1667
|
+
};
|
|
1668
|
+
const createRemove = (set, pruneRegistry) => (id) => {
|
|
1669
|
+
set((state) => {
|
|
1670
|
+
const next = state.stack.filter((item) => item.id !== id);
|
|
1671
|
+
if (next.length === state.stack.length) return state;
|
|
1672
|
+
pruneRegistry(next);
|
|
1673
|
+
return next.length === 0 ? {
|
|
1674
|
+
isOpen: false,
|
|
1675
|
+
stack: []
|
|
1676
|
+
} : { stack: next };
|
|
1677
|
+
});
|
|
1678
|
+
};
|
|
1679
|
+
const createSetData = (set, resolve) => (first, second, third) => {
|
|
1680
|
+
const { id, data } = resolve(first, second, third);
|
|
1681
|
+
set((state) => {
|
|
1682
|
+
const index = state.stack.findIndex((item) => item.id === id);
|
|
1683
|
+
const existing = state.stack[index];
|
|
1684
|
+
if (existing === void 0) return state;
|
|
1685
|
+
const updated = [...state.stack];
|
|
1686
|
+
updated[index] = {
|
|
1687
|
+
...existing,
|
|
1688
|
+
data
|
|
1689
|
+
};
|
|
1690
|
+
return { stack: updated };
|
|
1691
|
+
});
|
|
1692
|
+
};
|
|
1693
|
+
const createSwap = (set, internals) => (first, second, third) => {
|
|
1694
|
+
const type = isComponent(first) ? registerComponent(first, internals.componentRegistry, internals.componentMap, internals.getNextKey, internals.warnedNames) : getStringArg(first, "sheet type");
|
|
1695
|
+
const data = toRecord(second);
|
|
1696
|
+
const ariaLabel = resolvePresentationOptions(third)?.ariaLabel;
|
|
1697
|
+
set((state) => {
|
|
1698
|
+
const top = state.stack.at(-1);
|
|
1699
|
+
if (top === void 0) return state;
|
|
1700
|
+
const newStack = [...state.stack];
|
|
1701
|
+
newStack[newStack.length - 1] = {
|
|
1702
|
+
ariaLabel: ariaLabel ?? top.ariaLabel,
|
|
1703
|
+
data,
|
|
1704
|
+
id: top.id,
|
|
1705
|
+
type
|
|
1706
|
+
};
|
|
1707
|
+
return { stack: newStack };
|
|
1708
|
+
});
|
|
1709
|
+
};
|
|
1710
|
+
const createStoreState = ({ componentMap, componentRegistry, config, getNextKey, pruneRegistry, resolve, warnedNames }) => (set, get) => {
|
|
1711
|
+
const writers = createResolvedWriters(set, config);
|
|
1712
|
+
return {
|
|
1713
|
+
close: () => {
|
|
1714
|
+
pruneRegistry([]);
|
|
1715
|
+
set({
|
|
1716
|
+
isOpen: false,
|
|
1717
|
+
stack: []
|
|
1718
|
+
});
|
|
1719
|
+
},
|
|
1720
|
+
isOpen: false,
|
|
1721
|
+
navigate: createNavigate(componentMap, get, resolve, writers),
|
|
1722
|
+
open: (first, second, third, fourth) => {
|
|
1723
|
+
writers.openResolved(resolve(first, second, third, fourth));
|
|
1724
|
+
},
|
|
1725
|
+
pop: createPop(set, pruneRegistry),
|
|
1726
|
+
push: (first, second, third, fourth) => {
|
|
1727
|
+
writers.pushResolved(resolve(first, second, third, fourth));
|
|
1728
|
+
},
|
|
1729
|
+
remove: createRemove(set, pruneRegistry),
|
|
1730
|
+
replace: (first, second, third, fourth) => {
|
|
1731
|
+
writers.replaceResolved(resolve(first, second, third, fourth));
|
|
1732
|
+
},
|
|
1733
|
+
setData: createSetData(set, resolve),
|
|
1734
|
+
stack: [],
|
|
1735
|
+
swap: createSwap(set, {
|
|
1736
|
+
componentMap,
|
|
1737
|
+
componentRegistry,
|
|
1738
|
+
getNextKey,
|
|
1739
|
+
warnedNames
|
|
1740
|
+
})
|
|
1741
|
+
};
|
|
1742
|
+
};
|
|
1325
1743
|
/**
|
|
1326
1744
|
* Create an isolated Zustand store for a sheet stack instance.
|
|
1327
1745
|
*
|
|
@@ -1355,165 +1773,15 @@ const createSheetStore = (config) => {
|
|
|
1355
1773
|
return {
|
|
1356
1774
|
componentMap,
|
|
1357
1775
|
componentRegistry,
|
|
1358
|
-
store: createStore()((
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
}]
|
|
1368
|
-
});
|
|
1369
|
-
};
|
|
1370
|
-
const _pushResolved = ({ type, id, data, ariaLabel }) => {
|
|
1371
|
-
set((state) => {
|
|
1372
|
-
const item = {
|
|
1373
|
-
ariaLabel,
|
|
1374
|
-
data,
|
|
1375
|
-
id,
|
|
1376
|
-
type
|
|
1377
|
-
};
|
|
1378
|
-
if (Number.isFinite(config.maxDepth) && state.stack.length >= config.maxDepth) return {
|
|
1379
|
-
isOpen: true,
|
|
1380
|
-
stack: [...state.stack.slice(0, -1), item]
|
|
1381
|
-
};
|
|
1382
|
-
return {
|
|
1383
|
-
isOpen: true,
|
|
1384
|
-
stack: [...state.stack, item]
|
|
1385
|
-
};
|
|
1386
|
-
});
|
|
1387
|
-
};
|
|
1388
|
-
const _replaceResolved = ({ type, id, data, ariaLabel }) => {
|
|
1389
|
-
set((state) => {
|
|
1390
|
-
const item = {
|
|
1391
|
-
ariaLabel,
|
|
1392
|
-
data,
|
|
1393
|
-
id,
|
|
1394
|
-
type
|
|
1395
|
-
};
|
|
1396
|
-
if (state.stack.length === 0) return {
|
|
1397
|
-
isOpen: true,
|
|
1398
|
-
stack: [item]
|
|
1399
|
-
};
|
|
1400
|
-
return {
|
|
1401
|
-
isOpen: true,
|
|
1402
|
-
stack: [...state.stack.slice(0, -1), item]
|
|
1403
|
-
};
|
|
1404
|
-
});
|
|
1405
|
-
};
|
|
1406
|
-
return {
|
|
1407
|
-
close: () => {
|
|
1408
|
-
pruneRegistry([]);
|
|
1409
|
-
set({
|
|
1410
|
-
isOpen: false,
|
|
1411
|
-
stack: []
|
|
1412
|
-
});
|
|
1413
|
-
},
|
|
1414
|
-
isOpen: false,
|
|
1415
|
-
navigate: (first, second, third, fourth) => {
|
|
1416
|
-
const resolved = resolve(first, second, third, fourth);
|
|
1417
|
-
const { stack } = get();
|
|
1418
|
-
const top = stack.at(-1);
|
|
1419
|
-
if (stack.length === 0) {
|
|
1420
|
-
_openResolved(resolved);
|
|
1421
|
-
return;
|
|
1422
|
-
}
|
|
1423
|
-
let isSameType = top?.type === resolved.type;
|
|
1424
|
-
if (!isSameType && typeof first === "function") isSameType = componentMap.get(top?.type ?? "") === first;
|
|
1425
|
-
if (isSameType) {
|
|
1426
|
-
_replaceResolved(resolved);
|
|
1427
|
-
return;
|
|
1428
|
-
}
|
|
1429
|
-
_pushResolved(resolved);
|
|
1430
|
-
},
|
|
1431
|
-
open: (first, second, third, fourth) => {
|
|
1432
|
-
_openResolved(resolve(first, second, third, fourth));
|
|
1433
|
-
},
|
|
1434
|
-
pop: () => {
|
|
1435
|
-
set((state) => {
|
|
1436
|
-
if (state.stack.length <= 1) {
|
|
1437
|
-
pruneRegistry([]);
|
|
1438
|
-
return {
|
|
1439
|
-
isOpen: false,
|
|
1440
|
-
stack: []
|
|
1441
|
-
};
|
|
1442
|
-
}
|
|
1443
|
-
const next = state.stack.slice(0, -1);
|
|
1444
|
-
pruneRegistry(next);
|
|
1445
|
-
return {
|
|
1446
|
-
isOpen: true,
|
|
1447
|
-
stack: next
|
|
1448
|
-
};
|
|
1449
|
-
});
|
|
1450
|
-
},
|
|
1451
|
-
push: (first, second, third, fourth) => {
|
|
1452
|
-
_pushResolved(resolve(first, second, third, fourth));
|
|
1453
|
-
},
|
|
1454
|
-
remove: (id) => {
|
|
1455
|
-
set((state) => {
|
|
1456
|
-
const next = state.stack.filter((item) => item.id !== id);
|
|
1457
|
-
if (next.length === state.stack.length) return state;
|
|
1458
|
-
pruneRegistry(next);
|
|
1459
|
-
if (next.length === 0) return {
|
|
1460
|
-
isOpen: false,
|
|
1461
|
-
stack: []
|
|
1462
|
-
};
|
|
1463
|
-
return { stack: next };
|
|
1464
|
-
});
|
|
1465
|
-
},
|
|
1466
|
-
replace: (first, second, third, fourth) => {
|
|
1467
|
-
_replaceResolved(resolve(first, second, third, fourth));
|
|
1468
|
-
},
|
|
1469
|
-
setData: (first, second, third) => {
|
|
1470
|
-
const { id, data } = resolve(first, second, third);
|
|
1471
|
-
set((state) => {
|
|
1472
|
-
const idx = state.stack.findIndex((item) => item.id === id);
|
|
1473
|
-
if (idx === -1) return state;
|
|
1474
|
-
const updated = [...state.stack];
|
|
1475
|
-
updated[idx] = {
|
|
1476
|
-
...updated[idx],
|
|
1477
|
-
data
|
|
1478
|
-
};
|
|
1479
|
-
return { stack: updated };
|
|
1480
|
-
});
|
|
1481
|
-
},
|
|
1482
|
-
stack: [],
|
|
1483
|
-
swap: (first, second, third) => {
|
|
1484
|
-
let type;
|
|
1485
|
-
let data;
|
|
1486
|
-
const ariaLabel = resolvePresentationOptions(third)?.ariaLabel;
|
|
1487
|
-
if (typeof first === "function") {
|
|
1488
|
-
const component = first;
|
|
1489
|
-
let typeKey = componentRegistry.get(component);
|
|
1490
|
-
if (!typeKey) {
|
|
1491
|
-
warnInlineComponent(component, componentRegistry, warnedNames);
|
|
1492
|
-
typeKey = getNextKey();
|
|
1493
|
-
componentRegistry.set(component, typeKey);
|
|
1494
|
-
componentMap.set(typeKey, component);
|
|
1495
|
-
}
|
|
1496
|
-
type = typeKey;
|
|
1497
|
-
data = second ?? {};
|
|
1498
|
-
} else {
|
|
1499
|
-
type = first;
|
|
1500
|
-
data = second ?? {};
|
|
1501
|
-
}
|
|
1502
|
-
set((state) => {
|
|
1503
|
-
const top = state.stack.at(-1);
|
|
1504
|
-
if (!top) return state;
|
|
1505
|
-
const newStack = [...state.stack];
|
|
1506
|
-
newStack[newStack.length - 1] = {
|
|
1507
|
-
ariaLabel: ariaLabel ?? top.ariaLabel,
|
|
1508
|
-
data,
|
|
1509
|
-
id: top.id,
|
|
1510
|
-
type
|
|
1511
|
-
};
|
|
1512
|
-
return { stack: newStack };
|
|
1513
|
-
});
|
|
1514
|
-
}
|
|
1515
|
-
};
|
|
1516
|
-
})
|
|
1776
|
+
store: createStore()(createStoreState({
|
|
1777
|
+
componentMap,
|
|
1778
|
+
componentRegistry,
|
|
1779
|
+
config,
|
|
1780
|
+
getNextKey,
|
|
1781
|
+
pruneRegistry,
|
|
1782
|
+
resolve,
|
|
1783
|
+
warnedNames
|
|
1784
|
+
}))
|
|
1517
1785
|
};
|
|
1518
1786
|
};
|
|
1519
1787
|
//#endregion
|
|
@@ -1557,12 +1825,11 @@ const createStacksheet = (config) => {
|
|
|
1557
1825
|
if (!ctx) throw new Error("useSheet/useStacksheetState must be used within <StacksheetProvider>");
|
|
1558
1826
|
return ctx;
|
|
1559
1827
|
};
|
|
1560
|
-
const EMPTY_SHEETS = {};
|
|
1561
1828
|
const providerValue = {
|
|
1562
1829
|
config: resolved,
|
|
1563
1830
|
store
|
|
1564
1831
|
};
|
|
1565
|
-
const StacksheetProvider = ({ sheets
|
|
1832
|
+
const StacksheetProvider = ({ sheets, children, classNames, layout, renderHeader }) => /* @__PURE__ */ jsxs(StoreContext.Provider, {
|
|
1566
1833
|
value: providerValue,
|
|
1567
1834
|
children: [children, /* @__PURE__ */ jsx(Portal, {
|
|
1568
1835
|
asChild: false,
|
|
@@ -1611,19 +1878,20 @@ const createStacksheet = (config) => {
|
|
|
1611
1878
|
const SheetBack = ({ asChild, className, style, children }) => {
|
|
1612
1879
|
const { back, isNested } = useSheetPanel();
|
|
1613
1880
|
if (!isNested) return null;
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1881
|
+
const isAsChild = asChild === true;
|
|
1882
|
+
return /* @__PURE__ */ jsx(isAsChild ? Slot : "button", {
|
|
1883
|
+
"aria-label": children === void 0 || children === null ? "Back" : void 0,
|
|
1884
|
+
className: joinClassNames(isAsChild ? void 0 : "flex min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded-md border-none bg-transparent p-0 text-inherit opacity-60 transition-opacity duration-150 hover:opacity-100", className),
|
|
1617
1885
|
onClick: back,
|
|
1618
1886
|
style,
|
|
1619
|
-
type:
|
|
1887
|
+
type: isAsChild ? void 0 : "button",
|
|
1620
1888
|
children: children ?? /* @__PURE__ */ jsx(ArrowLeftIcon, {})
|
|
1621
1889
|
});
|
|
1622
1890
|
};
|
|
1623
1891
|
//#endregion
|
|
1624
1892
|
//#region src/SheetParts/sheet-body.tsx
|
|
1625
1893
|
const SheetBody = ({ asChild, className, style, children }) => {
|
|
1626
|
-
if (asChild) return /* @__PURE__ */ jsx(Slot, {
|
|
1894
|
+
if (asChild === true) return /* @__PURE__ */ jsx(Slot, {
|
|
1627
1895
|
className: joinClassNames("relative min-h-0 flex-1", className),
|
|
1628
1896
|
"data-stacksheet-no-drag": "",
|
|
1629
1897
|
style,
|
|
@@ -1647,12 +1915,13 @@ const SheetBody = ({ asChild, className, style, children }) => {
|
|
|
1647
1915
|
//#region src/SheetParts/sheet-close.tsx
|
|
1648
1916
|
const SheetClose = ({ asChild, className, style, children }) => {
|
|
1649
1917
|
const { close } = useSheetPanel();
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1918
|
+
const isAsChild = asChild === true;
|
|
1919
|
+
return /* @__PURE__ */ jsx(isAsChild ? Slot : "button", {
|
|
1920
|
+
"aria-label": children === void 0 || children === null ? "Close" : void 0,
|
|
1921
|
+
className: joinClassNames(isAsChild ? void 0 : "flex min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded-md border-none bg-transparent p-0 text-inherit opacity-60 transition-opacity duration-150 hover:opacity-100", className),
|
|
1653
1922
|
onClick: close,
|
|
1654
1923
|
style,
|
|
1655
|
-
type:
|
|
1924
|
+
type: isAsChild ? void 0 : "button",
|
|
1656
1925
|
children: children ?? /* @__PURE__ */ jsx(XIcon, {})
|
|
1657
1926
|
});
|
|
1658
1927
|
};
|
|
@@ -1661,7 +1930,7 @@ const SheetClose = ({ asChild, className, style, children }) => {
|
|
|
1661
1930
|
const SheetDescription = ({ asChild, className, style, children }) => {
|
|
1662
1931
|
const { panelId, registerDescription } = useSheetPanel();
|
|
1663
1932
|
useEffect(() => registerDescription(), [registerDescription]);
|
|
1664
|
-
return /* @__PURE__ */ jsx(asChild ? Slot : "p", {
|
|
1933
|
+
return /* @__PURE__ */ jsx(asChild === true ? Slot : "p", {
|
|
1665
1934
|
className,
|
|
1666
1935
|
id: `${panelId}-desc`,
|
|
1667
1936
|
style,
|
|
@@ -1671,8 +1940,9 @@ const SheetDescription = ({ asChild, className, style, children }) => {
|
|
|
1671
1940
|
//#endregion
|
|
1672
1941
|
//#region src/SheetParts/sheet-footer.tsx
|
|
1673
1942
|
const SheetFooter = ({ asChild, className, style, children }) => {
|
|
1674
|
-
|
|
1675
|
-
|
|
1943
|
+
const isAsChild = asChild === true;
|
|
1944
|
+
return /* @__PURE__ */ jsx(isAsChild ? Slot : "footer", {
|
|
1945
|
+
className: joinClassNames(isAsChild ? "shrink-0" : "flex shrink-0 items-center gap-2 border-t px-6 py-3", className),
|
|
1676
1946
|
style,
|
|
1677
1947
|
children
|
|
1678
1948
|
});
|
|
@@ -1682,13 +1952,14 @@ const SheetFooter = ({ asChild, className, style, children }) => {
|
|
|
1682
1952
|
const SheetHandle = ({ asChild, className, style, children }) => {
|
|
1683
1953
|
const { close, back, isNested } = useSheetPanel();
|
|
1684
1954
|
const dismiss = isNested ? back : close;
|
|
1685
|
-
|
|
1955
|
+
const isAsChild = asChild === true;
|
|
1956
|
+
return /* @__PURE__ */ jsx(isAsChild ? Slot : "button", {
|
|
1686
1957
|
"aria-label": "Dismiss",
|
|
1687
|
-
className: joinClassNames(
|
|
1958
|
+
className: joinClassNames(isAsChild ? void 0 : "flex shrink-0 cursor-grab touch-none items-center justify-center border-none bg-transparent pt-4 pb-1 text-inherit", className),
|
|
1688
1959
|
"data-stacksheet-handle": "",
|
|
1689
1960
|
onClick: dismiss,
|
|
1690
1961
|
style,
|
|
1691
|
-
type:
|
|
1962
|
+
type: isAsChild ? void 0 : "button",
|
|
1692
1963
|
children: children ?? /* @__PURE__ */ jsx("div", {
|
|
1693
1964
|
"aria-hidden": "true",
|
|
1694
1965
|
className: "h-1 w-9 rounded-sm bg-current/25"
|
|
@@ -1698,8 +1969,9 @@ const SheetHandle = ({ asChild, className, style, children }) => {
|
|
|
1698
1969
|
//#endregion
|
|
1699
1970
|
//#region src/SheetParts/sheet-header.tsx
|
|
1700
1971
|
const SheetHeader = ({ asChild, className, style, children }) => {
|
|
1701
|
-
|
|
1702
|
-
|
|
1972
|
+
const isAsChild = asChild === true;
|
|
1973
|
+
return /* @__PURE__ */ jsx(isAsChild ? Slot : "header", {
|
|
1974
|
+
className: joinClassNames(isAsChild ? "shrink-0" : "flex h-14 shrink-0 items-center justify-between border-b px-6", className),
|
|
1703
1975
|
style,
|
|
1704
1976
|
children
|
|
1705
1977
|
});
|
|
@@ -1707,9 +1979,11 @@ const SheetHeader = ({ asChild, className, style, children }) => {
|
|
|
1707
1979
|
//#endregion
|
|
1708
1980
|
//#region src/SheetParts/sheet-title.tsx
|
|
1709
1981
|
const SheetTitle = ({ asChild, className, style, children }) => {
|
|
1710
|
-
const { panelId } = useSheetPanel();
|
|
1711
|
-
|
|
1712
|
-
|
|
1982
|
+
const { panelId, registerTitle } = useSheetPanel();
|
|
1983
|
+
useEffect(() => registerTitle(), [registerTitle]);
|
|
1984
|
+
const isAsChild = asChild === true;
|
|
1985
|
+
return /* @__PURE__ */ jsx(isAsChild ? Slot : "h2", {
|
|
1986
|
+
className: joinClassNames(isAsChild ? void 0 : "font-semibold text-sm", className),
|
|
1713
1987
|
id: `${panelId}-title`,
|
|
1714
1988
|
style,
|
|
1715
1989
|
children
|