@patternmode/stacksheet 1.3.5 → 1.3.6
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 +2 -8
- package/dist/Drag/{DragConstants.d.ts → drag-constants.d.ts} +1 -1
- package/dist/Drag/drag-constants.d.ts.map +1 -0
- package/dist/Drag/drag-geometry.d.ts +22 -0
- package/dist/Drag/drag-geometry.d.ts.map +1 -0
- package/dist/Drag/{DragTypes.d.ts → drag-types.d.ts} +1 -1
- package/dist/Drag/drag-types.d.ts.map +1 -0
- package/dist/Drag/{useDrag.d.ts → use-drag.d.ts} +4 -4
- package/dist/Drag/use-drag.d.ts.map +1 -0
- package/dist/SheetPanel/default-header.d.ts +5 -0
- package/dist/SheetPanel/default-header.d.ts.map +1 -0
- package/dist/SheetPanel/sheet-panel-content.d.ts +15 -0
- package/dist/SheetPanel/sheet-panel-content.d.ts.map +1 -0
- package/dist/SheetPanel/sheet-panel-focus.d.ts +8 -0
- package/dist/SheetPanel/sheet-panel-focus.d.ts.map +1 -0
- package/dist/SheetPanel/sheet-panel-handles.d.ts +10 -0
- package/dist/SheetPanel/sheet-panel-handles.d.ts.map +1 -0
- package/dist/SheetPanel/sheet-panel-layout.d.ts +4 -0
- package/dist/SheetPanel/sheet-panel-layout.d.ts.map +1 -0
- package/dist/SheetPanel/{SheetPanelTypes.d.ts → sheet-panel-types.d.ts} +2 -2
- package/dist/SheetPanel/sheet-panel-types.d.ts.map +1 -0
- package/dist/SheetPanel/sheet-panel.d.ts +3 -0
- package/dist/SheetPanel/sheet-panel.d.ts.map +1 -0
- package/dist/SheetParts/sheet-back.d.ts +3 -0
- package/dist/SheetParts/sheet-back.d.ts.map +1 -0
- package/dist/SheetParts/sheet-body.d.ts +3 -0
- package/dist/SheetParts/sheet-body.d.ts.map +1 -0
- package/dist/SheetParts/sheet-close.d.ts +3 -0
- package/dist/SheetParts/sheet-close.d.ts.map +1 -0
- package/dist/SheetParts/sheet-description.d.ts +3 -0
- package/dist/SheetParts/sheet-description.d.ts.map +1 -0
- package/dist/SheetParts/sheet-footer.d.ts +3 -0
- package/dist/SheetParts/sheet-footer.d.ts.map +1 -0
- package/dist/SheetParts/sheet-handle.d.ts +3 -0
- package/dist/SheetParts/sheet-handle.d.ts.map +1 -0
- package/dist/SheetParts/sheet-header.d.ts +3 -0
- package/dist/SheetParts/sheet-header.d.ts.map +1 -0
- package/dist/SheetParts/{SheetPartTypes.d.ts → sheet-part-types.d.ts} +1 -1
- package/dist/SheetParts/sheet-part-types.d.ts.map +1 -0
- package/dist/SheetParts/sheet-parts.d.ts +20 -0
- package/dist/SheetParts/sheet-parts.d.ts.map +1 -0
- package/dist/SheetParts/sheet-title.d.ts +3 -0
- package/dist/SheetParts/sheet-title.d.ts.map +1 -0
- package/dist/Store/{createSheetStore.d.ts → create-sheet-store.d.ts} +3 -3
- package/dist/Store/create-sheet-store.d.ts.map +1 -0
- package/dist/Store/store-args.d.ts +15 -0
- package/dist/Store/store-args.d.ts.map +1 -0
- package/dist/Store/{StoreTypes.d.ts → store-types.d.ts} +3 -4
- package/dist/Store/store-types.d.ts.map +1 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/create.d.ts +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 +640 -650
- package/dist/index.mjs.map +1 -1
- package/dist/media.d.ts +2 -2
- package/dist/media.d.ts.map +1 -1
- package/dist/panel-context.d.ts +1 -1
- package/dist/panel-context.d.ts.map +1 -1
- package/dist/parts.d.ts +10 -10
- package/dist/parts.d.ts.map +1 -1
- package/dist/renderer-effects.d.ts +4 -4
- package/dist/renderer-effects.d.ts.map +1 -1
- package/dist/renderer-helpers.d.ts +19 -19
- package/dist/renderer-helpers.d.ts.map +1 -1
- package/dist/renderer.d.ts +7 -6
- package/dist/renderer.d.ts.map +1 -1
- package/dist/sheet-panel.d.ts +2 -2
- package/dist/sheet-panel.d.ts.map +1 -1
- package/dist/snap-points.d.ts +3 -3
- package/dist/snap-points.d.ts.map +1 -1
- package/dist/springs.d.ts +7 -7
- package/dist/stacking.d.ts +6 -6
- package/dist/stacking.d.ts.map +1 -1
- package/dist/store.d.ts +2 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +5 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/dist/Drag/DragConstants.d.ts.map +0 -1
- package/dist/Drag/DragGeometry.d.ts +0 -22
- package/dist/Drag/DragGeometry.d.ts.map +0 -1
- package/dist/Drag/DragTypes.d.ts.map +0 -1
- package/dist/Drag/useDrag.d.ts.map +0 -1
- package/dist/SheetPanel/DefaultHeader.d.ts +0 -5
- package/dist/SheetPanel/DefaultHeader.d.ts.map +0 -1
- package/dist/SheetPanel/SheetPanel.d.ts +0 -3
- package/dist/SheetPanel/SheetPanel.d.ts.map +0 -1
- package/dist/SheetPanel/SheetPanelContent.d.ts +0 -12
- package/dist/SheetPanel/SheetPanelContent.d.ts.map +0 -1
- package/dist/SheetPanel/SheetPanelFocus.d.ts +0 -8
- package/dist/SheetPanel/SheetPanelFocus.d.ts.map +0 -1
- package/dist/SheetPanel/SheetPanelHandles.d.ts +0 -10
- package/dist/SheetPanel/SheetPanelHandles.d.ts.map +0 -1
- package/dist/SheetPanel/SheetPanelLayout.d.ts +0 -4
- package/dist/SheetPanel/SheetPanelLayout.d.ts.map +0 -1
- package/dist/SheetPanel/SheetPanelTypes.d.ts.map +0 -1
- package/dist/SheetParts/SheetBack.d.ts +0 -3
- package/dist/SheetParts/SheetBack.d.ts.map +0 -1
- package/dist/SheetParts/SheetBody.d.ts +0 -3
- package/dist/SheetParts/SheetBody.d.ts.map +0 -1
- package/dist/SheetParts/SheetClose.d.ts +0 -3
- package/dist/SheetParts/SheetClose.d.ts.map +0 -1
- package/dist/SheetParts/SheetDescription.d.ts +0 -3
- package/dist/SheetParts/SheetDescription.d.ts.map +0 -1
- package/dist/SheetParts/SheetFooter.d.ts +0 -3
- package/dist/SheetParts/SheetFooter.d.ts.map +0 -1
- package/dist/SheetParts/SheetHandle.d.ts +0 -3
- package/dist/SheetParts/SheetHandle.d.ts.map +0 -1
- package/dist/SheetParts/SheetHeader.d.ts +0 -3
- package/dist/SheetParts/SheetHeader.d.ts.map +0 -1
- package/dist/SheetParts/SheetPartTypes.d.ts.map +0 -1
- package/dist/SheetParts/SheetParts.d.ts +0 -28
- package/dist/SheetParts/SheetParts.d.ts.map +0 -1
- package/dist/SheetParts/SheetTitle.d.ts +0 -3
- package/dist/SheetParts/SheetTitle.d.ts.map +0 -1
- package/dist/Store/StoreArgs.d.ts +0 -15
- package/dist/Store/StoreArgs.d.ts.map +0 -1
- package/dist/Store/StoreTypes.d.ts.map +0 -1
- package/dist/Store/createSheetStore.d.ts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Portal } from "@radix-ui/react-portal";
|
|
3
|
-
import { createContext,
|
|
3
|
+
import { createContext, use, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { createStore, useStore } from "zustand";
|
|
5
5
|
import { useShallow } from "zustand/react/shallow";
|
|
6
6
|
import { AnimatePresence, LazyMotion, domMax, m, useReducedMotion } from "motion/react";
|
|
@@ -19,37 +19,58 @@ import { Root, Scrollbar, Thumb, Viewport } from "@radix-ui/react-scroll-area";
|
|
|
19
19
|
* - `stiff` — Very quick, controlled. Panels, drawers. **(default)**
|
|
20
20
|
*/
|
|
21
21
|
const springs = {
|
|
22
|
-
subtle: {
|
|
23
|
-
stiffness: 300,
|
|
24
|
-
damping: 30,
|
|
25
|
-
mass: 1
|
|
26
|
-
},
|
|
27
22
|
snappy: {
|
|
28
|
-
stiffness: 400,
|
|
29
23
|
damping: 28,
|
|
30
|
-
mass: .8
|
|
24
|
+
mass: .8,
|
|
25
|
+
stiffness: 400
|
|
31
26
|
},
|
|
32
27
|
stiff: {
|
|
33
|
-
stiffness: 400,
|
|
34
28
|
damping: 40,
|
|
35
|
-
mass: 1
|
|
29
|
+
mass: 1,
|
|
30
|
+
stiffness: 400
|
|
31
|
+
},
|
|
32
|
+
subtle: {
|
|
33
|
+
damping: 30,
|
|
34
|
+
mass: 1,
|
|
35
|
+
stiffness: 300
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
38
|
//#endregion
|
|
39
39
|
//#region src/config.ts
|
|
40
40
|
const DEFAULT_STACKING = {
|
|
41
|
-
scaleStep: .04,
|
|
42
41
|
offsetStep: 16,
|
|
43
42
|
opacityStep: 0,
|
|
44
43
|
radius: 12,
|
|
45
|
-
renderThreshold: 3
|
|
44
|
+
renderThreshold: 3,
|
|
45
|
+
scaleStep: .04
|
|
46
46
|
};
|
|
47
47
|
const DEFAULT_SIDE = {
|
|
48
48
|
desktop: "right",
|
|
49
49
|
mobile: "bottom"
|
|
50
50
|
};
|
|
51
|
+
const DEFAULT_CONFIG = {
|
|
52
|
+
ariaLabel: "Sheet dialog",
|
|
53
|
+
breakpoint: 768,
|
|
54
|
+
closeOnBackdrop: true,
|
|
55
|
+
closeOnEscape: true,
|
|
56
|
+
closeThreshold: .25,
|
|
57
|
+
dismissible: true,
|
|
58
|
+
drag: true,
|
|
59
|
+
lockScroll: true,
|
|
60
|
+
maxDepth: Number.POSITIVE_INFINITY,
|
|
61
|
+
maxWidth: "90vw",
|
|
62
|
+
modal: true,
|
|
63
|
+
scaleBackgroundAmount: .97,
|
|
64
|
+
shouldScaleBackground: false,
|
|
65
|
+
showOverlay: true,
|
|
66
|
+
snapPoints: [],
|
|
67
|
+
snapToSequentialPoints: false,
|
|
68
|
+
velocityThreshold: .5,
|
|
69
|
+
width: 420,
|
|
70
|
+
zIndex: 100
|
|
71
|
+
};
|
|
51
72
|
/** Normalize a string side to a responsive object, merging with defaults. */
|
|
52
|
-
|
|
73
|
+
const resolveSide = (side) => {
|
|
53
74
|
if (typeof side === "string") return {
|
|
54
75
|
desktop: side,
|
|
55
76
|
mobile: side
|
|
@@ -58,56 +79,37 @@ function resolveSide(side) {
|
|
|
58
79
|
...DEFAULT_SIDE,
|
|
59
80
|
...side
|
|
60
81
|
};
|
|
61
|
-
}
|
|
82
|
+
};
|
|
62
83
|
/** Resolve a preset name to a SpringConfig, or merge partial config with defaults. */
|
|
63
|
-
|
|
84
|
+
const resolveSpring = (spring) => {
|
|
64
85
|
if (typeof spring === "string") return springs[spring];
|
|
65
86
|
return {
|
|
66
87
|
...springs.stiff,
|
|
67
88
|
...spring
|
|
68
89
|
};
|
|
69
|
-
}
|
|
90
|
+
};
|
|
70
91
|
/** Merge user-provided config with defaults. Resolves union types (side, spring) to concrete values. */
|
|
71
|
-
|
|
92
|
+
const resolveConfig = (config = {}) => {
|
|
93
|
+
const { side, spring, stacking, ...scalarConfig } = config;
|
|
94
|
+
const definedConfig = Object.fromEntries(Object.entries(scalarConfig).filter(([, value]) => value !== void 0));
|
|
72
95
|
return {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
lockScroll: config.lockScroll ?? true,
|
|
78
|
-
width: config.width ?? 420,
|
|
79
|
-
maxWidth: config.maxWidth ?? "90vw",
|
|
80
|
-
breakpoint: config.breakpoint ?? 768,
|
|
81
|
-
side: resolveSide(config.side),
|
|
96
|
+
...DEFAULT_CONFIG,
|
|
97
|
+
...definedConfig,
|
|
98
|
+
side: resolveSide(side),
|
|
99
|
+
spring: resolveSpring(spring),
|
|
82
100
|
stacking: {
|
|
83
101
|
...DEFAULT_STACKING,
|
|
84
|
-
...
|
|
85
|
-
}
|
|
86
|
-
spring: resolveSpring(config.spring),
|
|
87
|
-
zIndex: config.zIndex ?? 100,
|
|
88
|
-
ariaLabel: config.ariaLabel ?? "Sheet dialog",
|
|
89
|
-
onOpenComplete: config.onOpenComplete,
|
|
90
|
-
onCloseComplete: config.onCloseComplete,
|
|
91
|
-
snapPoints: config.snapPoints ?? [],
|
|
92
|
-
snapPointIndex: config.snapPointIndex,
|
|
93
|
-
onSnapPointChange: config.onSnapPointChange,
|
|
94
|
-
snapToSequentialPoints: config.snapToSequentialPoints ?? false,
|
|
95
|
-
drag: config.drag ?? true,
|
|
96
|
-
closeThreshold: config.closeThreshold ?? .25,
|
|
97
|
-
velocityThreshold: config.velocityThreshold ?? .5,
|
|
98
|
-
dismissible: config.dismissible ?? true,
|
|
99
|
-
modal: config.modal ?? true,
|
|
100
|
-
shouldScaleBackground: config.shouldScaleBackground ?? false,
|
|
101
|
-
scaleBackgroundAmount: config.scaleBackgroundAmount ?? .97
|
|
102
|
+
...stacking
|
|
103
|
+
}
|
|
102
104
|
};
|
|
103
|
-
}
|
|
105
|
+
};
|
|
104
106
|
//#endregion
|
|
105
107
|
//#region src/media.ts
|
|
106
108
|
/**
|
|
107
109
|
* Returns true when viewport width is at or below the breakpoint.
|
|
108
110
|
* SSR-safe: defaults to false (desktop).
|
|
109
111
|
*/
|
|
110
|
-
|
|
112
|
+
const useIsMobile = (breakpoint) => {
|
|
111
113
|
const [isMobile, setIsMobile] = useState(false);
|
|
112
114
|
useEffect(() => {
|
|
113
115
|
const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
|
|
@@ -117,14 +119,14 @@ function useIsMobile(breakpoint) {
|
|
|
117
119
|
return () => mql.removeEventListener("change", handler);
|
|
118
120
|
}, [breakpoint]);
|
|
119
121
|
return isMobile;
|
|
120
|
-
}
|
|
122
|
+
};
|
|
121
123
|
/** Resolve the current side from config + viewport. */
|
|
122
|
-
|
|
124
|
+
const useResolvedSide = (config) => {
|
|
123
125
|
return useIsMobile(config.breakpoint) ? config.side.mobile : config.side.desktop;
|
|
124
|
-
}
|
|
126
|
+
};
|
|
125
127
|
//#endregion
|
|
126
128
|
//#region src/renderer-effects.ts
|
|
127
|
-
|
|
129
|
+
const usePanelHeight = (panelRef, hasSnapPoints) => {
|
|
128
130
|
const [height, setHeight] = useState(0);
|
|
129
131
|
useEffect(() => {
|
|
130
132
|
const el = panelRef.current;
|
|
@@ -137,24 +139,23 @@ function usePanelHeight(panelRef, hasSnapPoints) {
|
|
|
137
139
|
return () => observer.disconnect();
|
|
138
140
|
}, [panelRef, hasSnapPoints]);
|
|
139
141
|
return height;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const [height, setHeight] = useState(() =>
|
|
142
|
+
};
|
|
143
|
+
const getViewportHeight = () => typeof window === "undefined" ? 0 : window.visualViewport?.height ?? window.innerHeight;
|
|
144
|
+
const useViewportHeight = (active) => {
|
|
145
|
+
const [height, setHeight] = useState(() => typeof window === "undefined" ? void 0 : getViewportHeight());
|
|
144
146
|
useEffect(() => {
|
|
145
147
|
if (typeof window === "undefined") return;
|
|
146
|
-
const update = () => setHeight(
|
|
147
|
-
update();
|
|
148
|
+
const update = () => setHeight(getViewportHeight());
|
|
148
149
|
window.addEventListener("resize", update);
|
|
149
150
|
window.visualViewport?.addEventListener("resize", update);
|
|
150
151
|
return () => {
|
|
151
152
|
window.removeEventListener("resize", update);
|
|
152
153
|
window.visualViewport?.removeEventListener("resize", update);
|
|
153
154
|
};
|
|
154
|
-
}, [
|
|
155
|
-
return active ? height : 0;
|
|
156
|
-
}
|
|
157
|
-
|
|
155
|
+
}, []);
|
|
156
|
+
return active ? height ?? 0 : 0;
|
|
157
|
+
};
|
|
158
|
+
const useBodyScale = (config, isOpen, prefersReducedMotion) => {
|
|
158
159
|
useEffect(() => {
|
|
159
160
|
if (!config.shouldScaleBackground || prefersReducedMotion) return;
|
|
160
161
|
const wrapper = document.querySelector("[data-stacksheet-wrapper]");
|
|
@@ -183,23 +184,24 @@ function useBodyScale(config, isOpen, prefersReducedMotion) {
|
|
|
183
184
|
config.scaleBackgroundAmount,
|
|
184
185
|
prefersReducedMotion
|
|
185
186
|
]);
|
|
186
|
-
}
|
|
187
|
+
};
|
|
187
188
|
//#endregion
|
|
188
189
|
//#region src/snap-points.ts
|
|
189
|
-
const SNAP_POINT_RE = /^(\d+(?:\.\d+)?)(px|rem|em|vh|%)
|
|
190
|
+
const SNAP_POINT_RE = /^(\d+(?:\.\d+)?)(px|rem|em|vh|%)$/u;
|
|
190
191
|
/**
|
|
191
192
|
* Resolve a snap point value to pixels given the viewport height.
|
|
192
193
|
* - number 0-1: fraction of viewport (0.5 → 50% of vh)
|
|
193
194
|
* - number > 1: pixel value (300 → 300px)
|
|
194
195
|
* - string: parsed via regex for CSS unit support
|
|
195
196
|
*/
|
|
196
|
-
|
|
197
|
+
const resolveSnapPointPx = (point, viewportHeight) => {
|
|
197
198
|
if (typeof point === "number") return point <= 1 ? point * viewportHeight : point;
|
|
198
199
|
if (typeof point === "string") {
|
|
199
200
|
const match = point.match(SNAP_POINT_RE);
|
|
200
201
|
if (!match?.[1]) return 0;
|
|
201
|
-
const
|
|
202
|
-
|
|
202
|
+
const [, rawValue, unit] = match;
|
|
203
|
+
const value = Number.parseFloat(rawValue);
|
|
204
|
+
switch (unit) {
|
|
203
205
|
case "px": return value;
|
|
204
206
|
case "rem":
|
|
205
207
|
case "em": return value * (typeof document === "undefined" ? 16 : Number.parseFloat(getComputedStyle(document.documentElement).fontSize));
|
|
@@ -209,12 +211,12 @@ function resolveSnapPointPx(point, viewportHeight) {
|
|
|
209
211
|
}
|
|
210
212
|
}
|
|
211
213
|
return 0;
|
|
212
|
-
}
|
|
214
|
+
};
|
|
213
215
|
/**
|
|
214
216
|
* Resolve all snap points to sorted pixel heights (ascending).
|
|
215
217
|
* Returns an array of heights in px that the drawer should snap to.
|
|
216
218
|
*/
|
|
217
|
-
|
|
219
|
+
const resolveSnapPoints = (points, viewportHeight) => {
|
|
218
220
|
if (points.length === 0) return [];
|
|
219
221
|
const resolved = [];
|
|
220
222
|
for (const point of points) {
|
|
@@ -228,7 +230,7 @@ function resolveSnapPoints(points, viewportHeight) {
|
|
|
228
230
|
if (last === void 0 || Math.abs(px - last) > 1) deduped.push(px);
|
|
229
231
|
}
|
|
230
232
|
return deduped;
|
|
231
|
-
}
|
|
233
|
+
};
|
|
232
234
|
/** Velocity threshold (px/ms) for skipping intermediate snap points */
|
|
233
235
|
const SNAP_VELOCITY_THRESHOLD = .4;
|
|
234
236
|
/** Max velocity to consider for snap offset calculation */
|
|
@@ -244,7 +246,7 @@ const SNAP_VELOCITY_MULTIPLIER = 150;
|
|
|
244
246
|
*
|
|
245
247
|
* Returns -1 if the gesture indicates full dismissal.
|
|
246
248
|
*/
|
|
247
|
-
|
|
249
|
+
const findSnapTarget = (dragOffset, panelHeight, snapHeights, velocity, currentIndex, sequential) => {
|
|
248
250
|
if (snapHeights.length === 0) return -1;
|
|
249
251
|
const snapOffsets = snapHeights.map((h) => panelHeight - h);
|
|
250
252
|
const currentPos = dragOffset;
|
|
@@ -258,7 +260,7 @@ function findSnapTarget(dragOffset, panelHeight, snapHeights, velocity, currentI
|
|
|
258
260
|
const first = snapOffsets[0] ?? 0;
|
|
259
261
|
let bestIndex = 0;
|
|
260
262
|
let bestDist = Math.abs(projectedPos - first);
|
|
261
|
-
for (let i = 1; i < snapOffsets.length; i
|
|
263
|
+
for (let i = 1; i < snapOffsets.length; i += 1) {
|
|
262
264
|
const offset = snapOffsets[i] ?? 0;
|
|
263
265
|
const dist = Math.abs(projectedPos - offset);
|
|
264
266
|
if (dist < bestDist) {
|
|
@@ -268,31 +270,31 @@ function findSnapTarget(dragOffset, panelHeight, snapHeights, velocity, currentI
|
|
|
268
270
|
}
|
|
269
271
|
if (Math.abs(projectedPos - panelHeight) <= bestDist) return -1;
|
|
270
272
|
return bestIndex;
|
|
271
|
-
}
|
|
273
|
+
};
|
|
272
274
|
/**
|
|
273
275
|
* Get the Y offset for a snap point relative to fully open (0 offset).
|
|
274
276
|
* Returns the number of pixels the drawer should be translated down from fully open.
|
|
275
277
|
*/
|
|
276
|
-
|
|
278
|
+
const getSnapOffset = (snapIndex, snapHeights, panelHeight) => {
|
|
277
279
|
if (snapIndex < 0 || snapIndex >= snapHeights.length) return 0;
|
|
278
280
|
return panelHeight - (snapHeights[snapIndex] ?? 0);
|
|
279
|
-
}
|
|
281
|
+
};
|
|
280
282
|
//#endregion
|
|
281
283
|
//#region src/renderer-helpers.ts
|
|
282
284
|
const EMPTY_CLASSNAMES = {
|
|
283
285
|
backdrop: "",
|
|
284
|
-
|
|
285
|
-
|
|
286
|
+
header: "",
|
|
287
|
+
panel: ""
|
|
286
288
|
};
|
|
287
|
-
|
|
289
|
+
const resolveClassNames = (cn) => {
|
|
288
290
|
if (!cn) return EMPTY_CLASSNAMES;
|
|
289
291
|
return {
|
|
290
292
|
backdrop: cn.backdrop ?? "",
|
|
291
|
-
|
|
292
|
-
|
|
293
|
+
header: cn.header ?? "",
|
|
294
|
+
panel: cn.panel ?? ""
|
|
293
295
|
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
+
};
|
|
297
|
+
const buildAriaProps = (isTop, isModal, isComposable, ariaLabel, panelId, hasDescription) => {
|
|
296
298
|
if (!isTop) return {};
|
|
297
299
|
const props = { role: "dialog" };
|
|
298
300
|
if (isModal) props["aria-modal"] = "true";
|
|
@@ -301,8 +303,8 @@ function buildAriaProps(isTop, isModal, isComposable, ariaLabel, panelId, hasDes
|
|
|
301
303
|
if (hasDescription) props["aria-describedby"] = `${panelId}-desc`;
|
|
302
304
|
} else props["aria-label"] = ariaLabel;
|
|
303
305
|
return props;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
+
};
|
|
307
|
+
const getDragTransform = (side, offset) => {
|
|
306
308
|
if (offset === 0) return {};
|
|
307
309
|
switch (side) {
|
|
308
310
|
case "right": return { x: offset };
|
|
@@ -310,57 +312,58 @@ function getDragTransform(side, offset) {
|
|
|
310
312
|
case "bottom": return { y: offset };
|
|
311
313
|
default: return {};
|
|
312
314
|
}
|
|
313
|
-
}
|
|
315
|
+
};
|
|
314
316
|
const VISUAL_TWEEN = {
|
|
315
|
-
type: "tween",
|
|
316
317
|
duration: .25,
|
|
317
|
-
ease: "easeOut"
|
|
318
|
+
ease: "easeOut",
|
|
319
|
+
type: "tween"
|
|
318
320
|
};
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
|
|
321
|
+
const SHADOW_SM = "0px 1px 3px 0px rgba(0,0,0,0.06), 0px 6px 12px 0px rgba(0,0,0,0.06)";
|
|
322
|
+
const SHADOW_LG = "0px 8px 24px 0px rgba(0,0,0,0.06), 0px 24px 48px 0px rgba(0,0,0,0.04), 0px 48px 96px 0px rgba(0,0,0,0.03)";
|
|
323
|
+
const getShadow = (isNested) => isNested ? SHADOW_SM : SHADOW_LG;
|
|
324
|
+
const buildPanelStyle = (panelStyles, isTop, hasPanelClass, isDragging) => ({
|
|
325
|
+
...panelStyles,
|
|
326
|
+
pointerEvents: isTop ? "auto" : "none",
|
|
327
|
+
...isTop ? {} : { contain: "layout style paint" },
|
|
328
|
+
...isDragging ? { transition: "none" } : {},
|
|
329
|
+
...hasPanelClass ? {} : {
|
|
330
|
+
background: "var(--background, #fff)",
|
|
331
|
+
borderColor: "var(--border, transparent)"
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
const buildPanelTransition = (isDragging, isTop, spring, stackSpring) => {
|
|
332
335
|
if (isDragging) return {
|
|
333
|
-
|
|
334
|
-
|
|
336
|
+
duration: 0,
|
|
337
|
+
type: "tween"
|
|
335
338
|
};
|
|
336
339
|
return {
|
|
337
340
|
...isTop ? spring : stackSpring,
|
|
338
341
|
borderRadius: VISUAL_TWEEN,
|
|
339
342
|
boxShadow: VISUAL_TWEEN
|
|
340
343
|
};
|
|
341
|
-
}
|
|
342
|
-
|
|
344
|
+
};
|
|
345
|
+
const computeSnapYOffset = (side, snapHeights, activeSnapIndex, measuredHeight) => {
|
|
343
346
|
if (side !== "bottom" || snapHeights.length === 0 || measuredHeight <= 0) return 0;
|
|
344
347
|
return getSnapOffset(activeSnapIndex, snapHeights, measuredHeight);
|
|
345
|
-
}
|
|
346
|
-
|
|
348
|
+
};
|
|
349
|
+
const getBottomSlideDistance = (measuredHeight) => {
|
|
347
350
|
if (measuredHeight > 0) return measuredHeight;
|
|
348
351
|
if (typeof window !== "undefined") return window.innerHeight;
|
|
349
352
|
return 1e3;
|
|
350
|
-
}
|
|
351
|
-
|
|
353
|
+
};
|
|
354
|
+
const resolveSlideFrom = (side, slideFrom, measuredHeight) => {
|
|
352
355
|
if (side !== "bottom") return slideFrom;
|
|
353
356
|
return { y: getBottomSlideDistance(measuredHeight) };
|
|
354
|
-
}
|
|
355
|
-
|
|
357
|
+
};
|
|
358
|
+
const buildAnimateTarget = (slideTarget, stackOffset, dragOffset, transform, animatedRadius, transition, snapYOffset, isTop) => {
|
|
356
359
|
const base = {
|
|
357
360
|
...slideTarget,
|
|
358
361
|
...stackOffset,
|
|
359
362
|
...dragOffset,
|
|
360
|
-
scale: transform.scale,
|
|
361
|
-
opacity: transform.opacity,
|
|
362
363
|
...animatedRadius,
|
|
363
364
|
boxShadow: getShadow(!isTop),
|
|
365
|
+
opacity: transform.opacity,
|
|
366
|
+
scale: transform.scale,
|
|
364
367
|
transition
|
|
365
368
|
};
|
|
366
369
|
if (snapYOffset > 0) return {
|
|
@@ -368,23 +371,18 @@ function buildAnimateTarget(slideTarget, stackOffset, dragOffset, transform, ani
|
|
|
368
371
|
y: (dragOffset.y ?? 0) + snapYOffset
|
|
369
372
|
};
|
|
370
373
|
return base;
|
|
371
|
-
}
|
|
372
|
-
|
|
374
|
+
};
|
|
375
|
+
const getInitialRadius = (side) => {
|
|
373
376
|
if (side === "bottom") return {
|
|
374
|
-
borderTopLeftRadius: 0,
|
|
375
|
-
borderTopRightRadius: 0,
|
|
376
377
|
borderBottomLeftRadius: 0,
|
|
377
|
-
borderBottomRightRadius: 0
|
|
378
|
+
borderBottomRightRadius: 0,
|
|
379
|
+
borderTopLeftRadius: 0,
|
|
380
|
+
borderTopRightRadius: 0
|
|
378
381
|
};
|
|
379
382
|
return { borderRadius: 0 };
|
|
380
|
-
}
|
|
381
|
-
const SHADOW_SM = "0px 1px 3px 0px rgba(0,0,0,0.06), 0px 6px 12px 0px rgba(0,0,0,0.06)";
|
|
382
|
-
const SHADOW_LG = "0px 8px 24px 0px rgba(0,0,0,0.06), 0px 24px 48px 0px rgba(0,0,0,0.04), 0px 48px 96px 0px rgba(0,0,0,0.03)";
|
|
383
|
-
function getShadow(isNested) {
|
|
384
|
-
return isNested ? SHADOW_SM : SHADOW_LG;
|
|
385
|
-
}
|
|
383
|
+
};
|
|
386
384
|
//#endregion
|
|
387
|
-
//#region src/Drag/
|
|
385
|
+
//#region src/Drag/drag-constants.ts
|
|
388
386
|
/** Elements that should never initiate a drag */
|
|
389
387
|
const INTERACTIVE_TAGS = new Set([
|
|
390
388
|
"INPUT",
|
|
@@ -396,19 +394,19 @@ const INTERACTIVE_TAGS = new Set([
|
|
|
396
394
|
/** Rubber-band resistance factor for dragging past resting position */
|
|
397
395
|
const RUBBER_BAND_FACTOR = .6;
|
|
398
396
|
//#endregion
|
|
399
|
-
//#region src/Drag/
|
|
400
|
-
|
|
397
|
+
//#region src/Drag/drag-geometry.ts
|
|
398
|
+
const isInteractiveElement = (el) => {
|
|
401
399
|
if (INTERACTIVE_TAGS.has(el.tagName)) return true;
|
|
402
400
|
if (el.isContentEditable) return true;
|
|
403
401
|
if (el.closest("button, a, input, textarea, select, [contenteditable]")) return true;
|
|
404
402
|
if (el.closest("[data-stacksheet-no-drag]")) return true;
|
|
405
403
|
return false;
|
|
406
|
-
}
|
|
404
|
+
};
|
|
407
405
|
/**
|
|
408
406
|
* Walk up from `el` to find the nearest scrollable ancestor.
|
|
409
407
|
* Returns null if nothing is scrollable in the dismiss axis.
|
|
410
408
|
*/
|
|
411
|
-
|
|
409
|
+
const findScrollableAncestor = (el, axis) => {
|
|
412
410
|
let current = el;
|
|
413
411
|
while (current) {
|
|
414
412
|
if (current instanceof HTMLElement) {
|
|
@@ -421,23 +419,23 @@ function findScrollableAncestor(el, axis) {
|
|
|
421
419
|
current = current.parentElement;
|
|
422
420
|
}
|
|
423
421
|
return null;
|
|
424
|
-
}
|
|
422
|
+
};
|
|
425
423
|
/**
|
|
426
424
|
* Check if a scrollable element is at its edge in the dismiss direction.
|
|
427
425
|
* For bottom sheets (sign=1, axis=y), "at edge" means scrolled to top.
|
|
428
426
|
* For left panels (sign=-1, axis=x), "at edge" means scrolled to right end.
|
|
429
427
|
*/
|
|
430
|
-
|
|
428
|
+
const isAtScrollEdge = (el, axis, sign) => {
|
|
431
429
|
if (axis === "y") return sign === 1 ? el.scrollTop <= 0 : el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
|
|
432
430
|
return sign === 1 ? el.scrollLeft <= 0 : el.scrollLeft + el.clientWidth >= el.scrollWidth - 1;
|
|
433
|
-
}
|
|
431
|
+
};
|
|
434
432
|
/**
|
|
435
433
|
* Get the dismiss direction axis and sign for a given side.
|
|
436
434
|
* - right panel → dismiss by dragging right (+x)
|
|
437
435
|
* - left panel → dismiss by dragging left (-x)
|
|
438
436
|
* - bottom panel → dismiss by dragging down (+y)
|
|
439
437
|
*/
|
|
440
|
-
|
|
438
|
+
const getDismissAxis = (side) => {
|
|
441
439
|
switch (side) {
|
|
442
440
|
case "right": return {
|
|
443
441
|
axis: "x",
|
|
@@ -456,13 +454,13 @@ function getDismissAxis(side) {
|
|
|
456
454
|
sign: 1
|
|
457
455
|
};
|
|
458
456
|
}
|
|
459
|
-
}
|
|
457
|
+
};
|
|
460
458
|
/**
|
|
461
459
|
* Decide whether a gesture past the dead zone qualifies as a dismiss drag.
|
|
462
460
|
* Returns "drag" if it's a valid dismiss gesture, "none" if it's off-axis
|
|
463
461
|
* or moving in the wrong direction.
|
|
464
462
|
*/
|
|
465
|
-
|
|
463
|
+
const classifyGesture = (dx, dy, axis, sign) => {
|
|
466
464
|
const absDx = Math.abs(dx);
|
|
467
465
|
const absDy = Math.abs(dy);
|
|
468
466
|
let angleDeg;
|
|
@@ -471,19 +469,19 @@ function classifyGesture(dx, dy, axis, sign) {
|
|
|
471
469
|
if (angleDeg > 35) return "none";
|
|
472
470
|
if ((axis === "x" ? dx : dy) * sign < 0) return "none";
|
|
473
471
|
return "drag";
|
|
474
|
-
}
|
|
472
|
+
};
|
|
475
473
|
/** Decide whether a pointer gesture commits as a drag or should be ignored. */
|
|
476
|
-
|
|
474
|
+
const commitGesture = (dx, dy, axis, sign, scrollEl) => {
|
|
477
475
|
if (classifyGesture(dx, dy, axis, sign) === "none") return "none";
|
|
478
476
|
if (scrollEl && !isAtScrollEdge(scrollEl, axis, sign)) return "none";
|
|
479
477
|
return "drag";
|
|
480
|
-
}
|
|
481
|
-
|
|
478
|
+
};
|
|
479
|
+
const getPanelDimension = (panel, axis) => {
|
|
482
480
|
if (!panel) return 300;
|
|
483
481
|
return axis === "x" ? panel.offsetWidth : panel.offsetHeight;
|
|
484
|
-
}
|
|
482
|
+
};
|
|
485
483
|
//#endregion
|
|
486
|
-
//#region src/Drag/
|
|
484
|
+
//#region src/Drag/use-drag.ts
|
|
487
485
|
/**
|
|
488
486
|
* Hook that manages drag gestures that can dismiss a sheet panel.
|
|
489
487
|
*
|
|
@@ -500,13 +498,13 @@ function getPanelDimension(panel, axis) {
|
|
|
500
498
|
* When `snapHeights` is provided, release targeting uses
|
|
501
499
|
* `findSnapTarget()` instead of the simple threshold check.
|
|
502
500
|
*/
|
|
503
|
-
|
|
501
|
+
const useDrag = (panelRef, config, onDragUpdate) => {
|
|
504
502
|
const startRef = useRef(null);
|
|
505
503
|
const committedRef = useRef(null);
|
|
506
504
|
const offsetRef = useRef(0);
|
|
507
505
|
const scrollTargetRef = useRef(null);
|
|
508
506
|
const { axis, sign } = getDismissAxis(config.side);
|
|
509
|
-
const handlePointerDown =
|
|
507
|
+
const handlePointerDown = (e) => {
|
|
510
508
|
if (!config.enabled) return;
|
|
511
509
|
if (e.button !== 0) return;
|
|
512
510
|
const target = e.target;
|
|
@@ -515,19 +513,19 @@ function useDrag(panelRef, config, onDragUpdate) {
|
|
|
515
513
|
if (!isHandle && isInteractiveElement(target)) return;
|
|
516
514
|
scrollTargetRef.current = isHandle ? null : findScrollableAncestor(target, axis);
|
|
517
515
|
startRef.current = {
|
|
516
|
+
time: Date.now(),
|
|
518
517
|
x: e.clientX,
|
|
519
|
-
y: e.clientY
|
|
520
|
-
time: Date.now()
|
|
518
|
+
y: e.clientY
|
|
521
519
|
};
|
|
522
520
|
committedRef.current = null;
|
|
523
521
|
offsetRef.current = 0;
|
|
524
522
|
e.currentTarget?.setPointerCapture?.(e.pointerId);
|
|
525
|
-
}
|
|
526
|
-
const handlePointerMove =
|
|
523
|
+
};
|
|
524
|
+
const handlePointerMove = (e) => {
|
|
527
525
|
if (!startRef.current) return;
|
|
528
526
|
const dx = e.clientX - startRef.current.x;
|
|
529
527
|
const dy = e.clientY - startRef.current.y;
|
|
530
|
-
const dist = Math.
|
|
528
|
+
const dist = Math.hypot(dx, dy);
|
|
531
529
|
if (committedRef.current === null && dist < 10) return;
|
|
532
530
|
if (committedRef.current === null) {
|
|
533
531
|
committedRef.current = commitGesture(dx, dy, axis, sign, scrollTargetRef.current);
|
|
@@ -541,20 +539,16 @@ function useDrag(panelRef, config, onDragUpdate) {
|
|
|
541
539
|
const clampedOffset = directional >= 0 ? directional : -Math.sqrt(Math.abs(directional)) * RUBBER_BAND_FACTOR;
|
|
542
540
|
offsetRef.current = clampedOffset;
|
|
543
541
|
onDragUpdate({
|
|
544
|
-
|
|
545
|
-
|
|
542
|
+
isDragging: true,
|
|
543
|
+
offset: clampedOffset
|
|
546
544
|
});
|
|
547
545
|
e.preventDefault();
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
sign,
|
|
551
|
-
onDragUpdate
|
|
552
|
-
]);
|
|
553
|
-
const dismiss = useCallback(() => {
|
|
546
|
+
};
|
|
547
|
+
const dismiss = () => {
|
|
554
548
|
if (config.isNested) config.onPop();
|
|
555
549
|
else config.onClose();
|
|
556
|
-
}
|
|
557
|
-
const handlePointerUp =
|
|
550
|
+
};
|
|
551
|
+
const handlePointerUp = (_e) => {
|
|
558
552
|
if (!startRef.current || committedRef.current !== "drag") {
|
|
559
553
|
startRef.current = null;
|
|
560
554
|
committedRef.current = null;
|
|
@@ -575,8 +569,8 @@ function useDrag(panelRef, config, onDragUpdate) {
|
|
|
575
569
|
else {
|
|
576
570
|
config.onSnap(targetIndex);
|
|
577
571
|
onDragUpdate({
|
|
578
|
-
|
|
579
|
-
|
|
572
|
+
isDragging: false,
|
|
573
|
+
offset: 0
|
|
580
574
|
});
|
|
581
575
|
}
|
|
582
576
|
return;
|
|
@@ -585,26 +579,20 @@ function useDrag(panelRef, config, onDragUpdate) {
|
|
|
585
579
|
const fastEnough = velocity > config.velocityThreshold;
|
|
586
580
|
if (pastThreshold || fastEnough) dismiss();
|
|
587
581
|
else onDragUpdate({
|
|
588
|
-
|
|
589
|
-
|
|
582
|
+
isDragging: false,
|
|
583
|
+
offset: 0
|
|
590
584
|
});
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
axis,
|
|
594
|
-
config,
|
|
595
|
-
onDragUpdate,
|
|
596
|
-
dismiss
|
|
597
|
-
]);
|
|
598
|
-
const handlePointerCancel = useCallback(() => {
|
|
585
|
+
};
|
|
586
|
+
const handlePointerCancel = () => {
|
|
599
587
|
startRef.current = null;
|
|
600
588
|
committedRef.current = null;
|
|
601
589
|
offsetRef.current = 0;
|
|
602
590
|
scrollTargetRef.current = null;
|
|
603
591
|
onDragUpdate({
|
|
604
|
-
|
|
605
|
-
|
|
592
|
+
isDragging: false,
|
|
593
|
+
offset: 0
|
|
606
594
|
});
|
|
607
|
-
}
|
|
595
|
+
};
|
|
608
596
|
const handlersRef = useRef({
|
|
609
597
|
handlePointerCancel,
|
|
610
598
|
handlePointerDown,
|
|
@@ -635,7 +623,7 @@ function useDrag(panelRef, config, onDragUpdate) {
|
|
|
635
623
|
el.removeEventListener("pointercancel", onPointerCancel);
|
|
636
624
|
};
|
|
637
625
|
}, [panelRef, config.enabled]);
|
|
638
|
-
}
|
|
626
|
+
};
|
|
639
627
|
//#endregion
|
|
640
628
|
//#region src/panel-context.tsx
|
|
641
629
|
const SheetPanelContext = createContext(null);
|
|
@@ -643,11 +631,11 @@ const SheetPanelContext = createContext(null);
|
|
|
643
631
|
* Access the current sheet panel's context.
|
|
644
632
|
* Must be called inside a component rendered by the sheet stack.
|
|
645
633
|
*/
|
|
646
|
-
|
|
647
|
-
const ctx =
|
|
634
|
+
const useSheetPanel = () => {
|
|
635
|
+
const ctx = use(SheetPanelContext);
|
|
648
636
|
if (!ctx) throw new Error("Sheet.* components must be used inside a sheet panel. They should be rendered by a component opened via actions.open(), push(), etc.");
|
|
649
637
|
return ctx;
|
|
650
|
-
}
|
|
638
|
+
};
|
|
651
639
|
//#endregion
|
|
652
640
|
//#region src/stacking.ts
|
|
653
641
|
/**
|
|
@@ -655,58 +643,56 @@ function useSheetPanel() {
|
|
|
655
643
|
* depth=0 is the top (foreground) panel.
|
|
656
644
|
* Panels beyond renderThreshold are clamped to the edge position and faded out.
|
|
657
645
|
*/
|
|
658
|
-
|
|
646
|
+
const getStackTransform = (depth, stacking) => {
|
|
659
647
|
if (depth <= 0) return {
|
|
660
|
-
|
|
648
|
+
borderRadius: 0,
|
|
661
649
|
offset: 0,
|
|
662
650
|
opacity: 1,
|
|
663
|
-
|
|
651
|
+
scale: 1
|
|
664
652
|
};
|
|
665
653
|
const beyondThreshold = depth >= stacking.renderThreshold;
|
|
666
654
|
const visualDepth = beyondThreshold ? stacking.renderThreshold - 1 : depth;
|
|
667
655
|
return {
|
|
668
|
-
|
|
656
|
+
borderRadius: stacking.radius,
|
|
669
657
|
offset: visualDepth * stacking.offsetStep,
|
|
670
658
|
opacity: beyondThreshold ? 0 : Math.max(0, 1 - visualDepth * stacking.opacityStep),
|
|
671
|
-
|
|
659
|
+
scale: Math.max(.5, 1 - visualDepth * stacking.scaleStep)
|
|
672
660
|
};
|
|
673
|
-
}
|
|
661
|
+
};
|
|
674
662
|
/**
|
|
675
663
|
* Border radius values for the animate target.
|
|
676
664
|
* Must be animated (not static CSS) so Motion applies scale correction
|
|
677
665
|
* when panels are scaled. See: https://motion.dev/docs/react-layout-animations#scale-correction
|
|
678
666
|
*/
|
|
679
|
-
|
|
667
|
+
const getAnimatedBorderRadius = (side, depth, stacking) => {
|
|
680
668
|
if (side === "bottom") {
|
|
681
669
|
const radius = depth > 0 ? stacking.radius : 16;
|
|
682
670
|
return {
|
|
683
|
-
borderTopLeftRadius: radius,
|
|
684
|
-
borderTopRightRadius: radius,
|
|
685
671
|
borderBottomLeftRadius: 0,
|
|
686
|
-
borderBottomRightRadius: 0
|
|
672
|
+
borderBottomRightRadius: 0,
|
|
673
|
+
borderTopLeftRadius: radius,
|
|
674
|
+
borderTopRightRadius: radius
|
|
687
675
|
};
|
|
688
676
|
}
|
|
689
677
|
if (depth > 0) return { borderRadius: stacking.radius };
|
|
690
678
|
return { borderRadius: 0 };
|
|
691
|
-
}
|
|
679
|
+
};
|
|
692
680
|
/** Motion initial/exit values for sliding from the given side. */
|
|
693
|
-
|
|
681
|
+
const getSlideFrom = (side) => {
|
|
694
682
|
switch (side) {
|
|
695
683
|
case "right": return { x: "100%" };
|
|
696
684
|
case "left": return { x: "-100%" };
|
|
697
685
|
case "bottom": return { y: "100%" };
|
|
698
686
|
default: return { x: "100%" };
|
|
699
687
|
}
|
|
700
|
-
}
|
|
688
|
+
};
|
|
701
689
|
/** Motion animate target — the resting position. */
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
};
|
|
707
|
-
}
|
|
690
|
+
const getSlideTarget = () => ({
|
|
691
|
+
x: 0,
|
|
692
|
+
y: 0
|
|
693
|
+
});
|
|
708
694
|
/** Translate offset that pushes stacked panels away from the stack edge. */
|
|
709
|
-
|
|
695
|
+
const getStackOffset = (side, offset) => {
|
|
710
696
|
if (offset === 0) return {};
|
|
711
697
|
switch (side) {
|
|
712
698
|
case "right": return { x: -offset };
|
|
@@ -714,107 +700,101 @@ function getStackOffset(side, offset) {
|
|
|
714
700
|
case "bottom": return { y: -offset };
|
|
715
701
|
default: return {};
|
|
716
702
|
}
|
|
717
|
-
}
|
|
703
|
+
};
|
|
718
704
|
/** Opposite-side origin so stacked panels recede away from the stack edge. */
|
|
719
|
-
|
|
705
|
+
const getTransformOrigin = (side) => {
|
|
720
706
|
if (side === "right") return "left center";
|
|
721
707
|
if (side === "left") return "right center";
|
|
722
708
|
return "center top";
|
|
723
|
-
}
|
|
709
|
+
};
|
|
724
710
|
/**
|
|
725
711
|
* Fixed-position styles for a panel, accounting for side, width, and depth.
|
|
726
712
|
*/
|
|
727
|
-
|
|
713
|
+
const getPanelStyles = (side, config, index) => {
|
|
728
714
|
const { width, maxWidth, zIndex } = config;
|
|
729
715
|
const base = {
|
|
730
|
-
position: "fixed",
|
|
731
|
-
zIndex: zIndex + 10 + index,
|
|
732
716
|
display: "flex",
|
|
733
717
|
flexDirection: "column",
|
|
718
|
+
position: "fixed",
|
|
719
|
+
transformOrigin: getTransformOrigin(side),
|
|
734
720
|
willChange: "transform",
|
|
735
|
-
|
|
721
|
+
zIndex: zIndex + 10 + index
|
|
736
722
|
};
|
|
737
723
|
if (side === "bottom") return {
|
|
738
724
|
...base,
|
|
739
|
-
left: 0,
|
|
740
|
-
right: 0,
|
|
741
725
|
bottom: 0,
|
|
742
726
|
height: "85dvh",
|
|
743
|
-
|
|
727
|
+
left: 0,
|
|
728
|
+
maxHeight: "85dvh",
|
|
729
|
+
right: 0
|
|
744
730
|
};
|
|
745
731
|
const sideStyles = side === "right" ? {
|
|
746
|
-
|
|
732
|
+
bottom: 0,
|
|
747
733
|
right: 0,
|
|
748
|
-
|
|
734
|
+
top: 0
|
|
749
735
|
} : {
|
|
750
|
-
|
|
736
|
+
bottom: 0,
|
|
751
737
|
left: 0,
|
|
752
|
-
|
|
738
|
+
top: 0
|
|
753
739
|
};
|
|
754
740
|
return {
|
|
755
741
|
...base,
|
|
756
742
|
...sideStyles,
|
|
757
|
-
|
|
758
|
-
|
|
743
|
+
maxWidth,
|
|
744
|
+
width
|
|
759
745
|
};
|
|
760
|
-
}
|
|
746
|
+
};
|
|
761
747
|
//#endregion
|
|
762
748
|
//#region src/icons.tsx
|
|
763
749
|
/** Inline SVG icons — no external dependency */
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
width: 16,
|
|
789
|
-
children: /* @__PURE__ */ jsx("path", { d: "M18 6L6 18M6 6l12 12" })
|
|
790
|
-
});
|
|
791
|
-
}
|
|
750
|
+
const ArrowLeftIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
751
|
+
"aria-hidden": "true",
|
|
752
|
+
fill: "none",
|
|
753
|
+
height: 16,
|
|
754
|
+
stroke: "currentColor",
|
|
755
|
+
strokeLinecap: "round",
|
|
756
|
+
strokeLinejoin: "round",
|
|
757
|
+
strokeWidth: 2,
|
|
758
|
+
viewBox: "0 0 24 24",
|
|
759
|
+
width: 16,
|
|
760
|
+
children: /* @__PURE__ */ jsx("path", { d: "M19 12H5M12 19l-7-7 7-7" })
|
|
761
|
+
});
|
|
762
|
+
const XIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
763
|
+
"aria-hidden": "true",
|
|
764
|
+
fill: "none",
|
|
765
|
+
height: 16,
|
|
766
|
+
stroke: "currentColor",
|
|
767
|
+
strokeLinecap: "round",
|
|
768
|
+
strokeLinejoin: "round",
|
|
769
|
+
strokeWidth: 2,
|
|
770
|
+
viewBox: "0 0 24 24",
|
|
771
|
+
width: 16,
|
|
772
|
+
children: /* @__PURE__ */ jsx("path", { d: "M18 6L6 18M6 6l12 12" })
|
|
773
|
+
});
|
|
792
774
|
//#endregion
|
|
793
|
-
//#region src/SheetPanel/
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
"aria-label": "Back",
|
|
801
|
-
className: "flex min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded-full border-none bg-black/5 p-0 text-inherit opacity-70 transition-opacity duration-150 hover:opacity-100",
|
|
802
|
-
onClick: onBack,
|
|
803
|
-
type: "button",
|
|
804
|
-
children: /* @__PURE__ */ jsx(ArrowLeftIcon, {})
|
|
805
|
-
})
|
|
806
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
807
|
-
"aria-label": "Close",
|
|
775
|
+
//#region src/SheetPanel/default-header.tsx
|
|
776
|
+
const DefaultHeader = ({ isNested, onBack, onClose, className }) => /* @__PURE__ */ jsxs("div", {
|
|
777
|
+
className: `flex shrink-0 items-center justify-between px-4 pt-4 pb-2 ${className ?? ""}`,
|
|
778
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
779
|
+
className: "flex items-center gap-2",
|
|
780
|
+
children: isNested && /* @__PURE__ */ jsx("button", {
|
|
781
|
+
"aria-label": "Back",
|
|
808
782
|
className: "flex min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded-full border-none bg-black/5 p-0 text-inherit opacity-70 transition-opacity duration-150 hover:opacity-100",
|
|
809
|
-
onClick:
|
|
783
|
+
onClick: onBack,
|
|
810
784
|
type: "button",
|
|
811
|
-
children: /* @__PURE__ */ jsx(
|
|
812
|
-
})
|
|
813
|
-
})
|
|
814
|
-
|
|
785
|
+
children: /* @__PURE__ */ jsx(ArrowLeftIcon, {})
|
|
786
|
+
})
|
|
787
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
788
|
+
"aria-label": "Close",
|
|
789
|
+
className: "flex min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded-full border-none bg-black/5 p-0 text-inherit opacity-70 transition-opacity duration-150 hover:opacity-100",
|
|
790
|
+
onClick: onClose,
|
|
791
|
+
type: "button",
|
|
792
|
+
children: /* @__PURE__ */ jsx(XIcon, {})
|
|
793
|
+
})]
|
|
794
|
+
});
|
|
815
795
|
//#endregion
|
|
816
|
-
//#region src/SheetPanel/
|
|
817
|
-
const PanelInnerContent =
|
|
796
|
+
//#region src/SheetPanel/sheet-panel-content.tsx
|
|
797
|
+
const PanelInnerContent = ({ isComposable, shouldRender, Content, data, renderHeader, headerProps, headerClassName }) => {
|
|
818
798
|
if (isComposable) return shouldRender && Content ? /* @__PURE__ */ jsx(Content, { ...data }) : null;
|
|
819
799
|
return /* @__PURE__ */ jsxs(Fragment, { children: [renderHeader ? renderHeader(headerProps) : /* @__PURE__ */ jsx(DefaultHeader, {
|
|
820
800
|
...headerProps,
|
|
@@ -824,176 +804,188 @@ const PanelInnerContent = memo(function PanelInnerContent({ isComposable, should
|
|
|
824
804
|
"data-stacksheet-no-drag": "",
|
|
825
805
|
children: /* @__PURE__ */ jsx(Content, { ...data })
|
|
826
806
|
})] });
|
|
827
|
-
}
|
|
807
|
+
};
|
|
828
808
|
PanelInnerContent.displayName = "PanelInnerContent";
|
|
829
809
|
//#endregion
|
|
830
|
-
//#region src/SheetPanel/
|
|
810
|
+
//#region src/SheetPanel/sheet-panel-focus.tsx
|
|
831
811
|
const LAYERED_MODAL_SELECTORS = [
|
|
832
812
|
"[role=\"dialog\"][data-state=\"open\"]",
|
|
833
813
|
"[role=\"alertdialog\"][data-state=\"open\"]",
|
|
834
814
|
"[data-radix-popper-content-wrapper]",
|
|
835
815
|
"[data-radix-focus-guard]"
|
|
836
816
|
].join(", ");
|
|
837
|
-
|
|
817
|
+
const subscribeToFocusTarget = (onStoreChange) => {
|
|
818
|
+
document.addEventListener("focusin", onStoreChange, true);
|
|
819
|
+
return () => document.removeEventListener("focusin", onStoreChange, true);
|
|
820
|
+
};
|
|
821
|
+
const getServerLayeredModalFocused = () => false;
|
|
822
|
+
const getLayeredModalFocused = () => {
|
|
823
|
+
if (typeof document === "undefined") return false;
|
|
824
|
+
const target = document.activeElement;
|
|
825
|
+
return !!target && target !== document.body && target instanceof Element && target.closest(LAYERED_MODAL_SELECTORS) !== null;
|
|
826
|
+
};
|
|
827
|
+
const useLayeredModalFocused = (active) => {
|
|
828
|
+
const layered = useSyncExternalStore(subscribeToFocusTarget, getLayeredModalFocused, getServerLayeredModalFocused);
|
|
829
|
+
return active && layered;
|
|
830
|
+
};
|
|
831
|
+
const ModalFocusTrap = ({ enabled, active, fallbackRef, children }) => {
|
|
838
832
|
const paused = useLayeredModalFocused(enabled && active);
|
|
839
833
|
if (!enabled) return children;
|
|
840
834
|
return /* @__PURE__ */ jsx(FocusTrap, {
|
|
841
835
|
active,
|
|
842
836
|
focusTrapOptions: {
|
|
843
|
-
initialFocus: false,
|
|
844
|
-
returnFocusOnDeactivate: true,
|
|
845
|
-
escapeDeactivates: false,
|
|
846
837
|
allowOutsideClick: true,
|
|
847
|
-
|
|
838
|
+
escapeDeactivates: false,
|
|
848
839
|
fallbackFocus: () => {
|
|
849
840
|
if (fallbackRef.current) return fallbackRef.current;
|
|
850
841
|
return document.body;
|
|
851
|
-
}
|
|
842
|
+
},
|
|
843
|
+
initialFocus: false,
|
|
844
|
+
returnFocusOnDeactivate: true
|
|
852
845
|
},
|
|
853
846
|
paused,
|
|
854
847
|
children
|
|
855
848
|
});
|
|
856
|
-
}
|
|
857
|
-
function useLayeredModalFocused(active) {
|
|
858
|
-
const layered = useSyncExternalStore(subscribeToFocusTarget, getLayeredModalFocused, getServerLayeredModalFocused);
|
|
859
|
-
return active && layered;
|
|
860
|
-
}
|
|
861
|
-
function subscribeToFocusTarget(onStoreChange) {
|
|
862
|
-
document.addEventListener("focusin", onStoreChange, true);
|
|
863
|
-
return () => document.removeEventListener("focusin", onStoreChange, true);
|
|
864
|
-
}
|
|
865
|
-
function getServerLayeredModalFocused() {
|
|
866
|
-
return false;
|
|
867
|
-
}
|
|
868
|
-
function getLayeredModalFocused() {
|
|
869
|
-
if (typeof document === "undefined") return false;
|
|
870
|
-
const target = document.activeElement;
|
|
871
|
-
return !!target && target !== document.body && target instanceof Element && target.closest(LAYERED_MODAL_SELECTORS) !== null;
|
|
872
|
-
}
|
|
849
|
+
};
|
|
873
850
|
//#endregion
|
|
874
|
-
//#region src/SheetPanel/
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
}
|
|
888
|
-
function SideHandle({ side, isHovered, onDismiss }) {
|
|
851
|
+
//#region src/SheetPanel/sheet-panel-handles.tsx
|
|
852
|
+
const BottomHandle = ({ onDismiss }) => /* @__PURE__ */ jsx("button", {
|
|
853
|
+
"aria-label": "Dismiss",
|
|
854
|
+
className: "absolute inset-x-0 top-0 z-10 flex w-full cursor-grab touch-none items-center justify-center border-none bg-transparent pt-2.5 pb-2",
|
|
855
|
+
"data-stacksheet-handle": "",
|
|
856
|
+
onClick: onDismiss,
|
|
857
|
+
type: "button",
|
|
858
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
859
|
+
"aria-hidden": "true",
|
|
860
|
+
className: "h-[5px] w-9 rounded-full bg-current/15"
|
|
861
|
+
})
|
|
862
|
+
});
|
|
863
|
+
const SideHandle = ({ side, isHovered, onDismiss }) => {
|
|
889
864
|
const position = side === "right" ? { right: "100%" } : { left: "100%" };
|
|
890
|
-
return /* @__PURE__ */ jsx(m.
|
|
865
|
+
return /* @__PURE__ */ jsx(m.button, {
|
|
891
866
|
animate: { opacity: isHovered ? 1 : 0 },
|
|
892
867
|
"aria-label": "Dismiss",
|
|
893
|
-
className: "absolute top-0 bottom-0 flex w-6 cursor-grab touch-none items-center justify-center",
|
|
868
|
+
className: "absolute top-0 bottom-0 flex w-6 cursor-grab touch-none items-center justify-center border-none bg-transparent p-0 text-inherit",
|
|
894
869
|
"data-stacksheet-handle": "",
|
|
895
870
|
onClick: onDismiss,
|
|
896
|
-
onKeyDown: (e) => {
|
|
897
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
898
|
-
e.preventDefault();
|
|
899
|
-
onDismiss?.();
|
|
900
|
-
}
|
|
901
|
-
},
|
|
902
|
-
role: "button",
|
|
903
871
|
style: position,
|
|
904
|
-
tabIndex: 0,
|
|
905
872
|
transition: {
|
|
906
873
|
duration: isHovered ? .15 : .4,
|
|
907
874
|
ease: "easeOut"
|
|
908
875
|
},
|
|
876
|
+
type: "button",
|
|
909
877
|
children: /* @__PURE__ */ jsx("div", {
|
|
910
878
|
"aria-hidden": "true",
|
|
911
879
|
className: "h-8 w-1 rounded-full bg-current/20"
|
|
912
880
|
})
|
|
913
881
|
});
|
|
914
|
-
}
|
|
882
|
+
};
|
|
915
883
|
//#endregion
|
|
916
|
-
//#region src/SheetPanel/
|
|
917
|
-
|
|
884
|
+
//#region src/SheetPanel/sheet-panel-layout.ts
|
|
885
|
+
const resolvePanelLayout = (layout, renderHeader) => {
|
|
918
886
|
if (layout) return layout;
|
|
919
887
|
return renderHeader === false ? "composable" : "classic";
|
|
920
|
-
}
|
|
888
|
+
};
|
|
921
889
|
//#endregion
|
|
922
|
-
//#region src/SheetPanel/
|
|
923
|
-
const
|
|
890
|
+
//#region src/SheetPanel/sheet-panel.tsx
|
|
891
|
+
const getPanelAriaLabel = (item, fallbackLabel) => item.ariaLabel ?? (typeof item.data?.__ariaLabel === "string" ? item.data.__ariaLabel : void 0) ?? fallbackLabel;
|
|
892
|
+
const getPanelHoverProps = (enabled, setIsHovered) => enabled ? {
|
|
893
|
+
onBlur: () => setIsHovered(false),
|
|
894
|
+
onFocus: () => setIsHovered(true),
|
|
895
|
+
onMouseEnter: () => setIsHovered(true),
|
|
896
|
+
onMouseLeave: () => setIsHovered(false)
|
|
897
|
+
} : {};
|
|
898
|
+
const getInactivePanelProps = (isTop) => isTop ? {} : {
|
|
899
|
+
"aria-hidden": "true",
|
|
900
|
+
inert: true
|
|
901
|
+
};
|
|
902
|
+
const getHeaderProps = ({ close, isNested, pop, side }) => ({
|
|
903
|
+
isNested,
|
|
904
|
+
onBack: pop,
|
|
905
|
+
onClose: close,
|
|
906
|
+
side
|
|
907
|
+
});
|
|
908
|
+
const getPanelContext = ({ close, hasDescription, isNested, isTop, panelId, pop, registerDescription, side }) => ({
|
|
909
|
+
back: pop,
|
|
910
|
+
close,
|
|
911
|
+
hasDescription,
|
|
912
|
+
isNested,
|
|
913
|
+
isTop,
|
|
914
|
+
panelId,
|
|
915
|
+
registerDescription,
|
|
916
|
+
side
|
|
917
|
+
});
|
|
918
|
+
const getOptionalSideHandle = ({ isHovered, onDismiss, show, side }) => show ? /* @__PURE__ */ jsx(SideHandle, {
|
|
919
|
+
isHovered,
|
|
920
|
+
onDismiss,
|
|
921
|
+
side
|
|
922
|
+
}) : null;
|
|
923
|
+
const getOptionalBottomHandle = (show, onDismiss) => show ? /* @__PURE__ */ jsx(BottomHandle, { onDismiss }) : null;
|
|
924
|
+
const completeOpeningAnimation = (hasEnteredRef, isTop, onOpenCompleteRef) => {
|
|
925
|
+
if (isTop && !hasEnteredRef.current) {
|
|
926
|
+
hasEnteredRef.current = true;
|
|
927
|
+
onOpenCompleteRef.current?.();
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
const SheetPanel = ({ item, index, depth, isTop, isNested, side, config, classNames, Content, shouldRender, pop, close, swipeClose, swipePop, snapHeights, activeSnapIndex, onSnap, layout, renderHeader, slideFrom, slideTarget, spring, stackSpring, prefersReducedMotion }) => {
|
|
924
931
|
const panelRef = useRef(null);
|
|
925
932
|
const hasEnteredRef = useRef(false);
|
|
933
|
+
const onOpenCompleteRef = useRef(config.onOpenComplete);
|
|
926
934
|
const [dragState, setDragState] = useState({
|
|
927
|
-
|
|
928
|
-
|
|
935
|
+
isDragging: false,
|
|
936
|
+
offset: 0
|
|
929
937
|
});
|
|
930
938
|
const [isHovered, setIsHovered] = useState(false);
|
|
931
939
|
const measuredHeight = usePanelHeight(panelRef, snapHeights.length > 0);
|
|
932
940
|
const transform = getStackTransform(depth, config.stacking);
|
|
933
941
|
const panelStyles = getPanelStyles(side, config, index);
|
|
942
|
+
if (!isTop && hasEnteredRef.current) hasEnteredRef.current = false;
|
|
934
943
|
useEffect(() => {
|
|
935
|
-
|
|
936
|
-
}, [
|
|
937
|
-
const handleAnimationComplete =
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
config.onOpenComplete?.();
|
|
941
|
-
}
|
|
942
|
-
}, [isTop, config]);
|
|
944
|
+
onOpenCompleteRef.current = config.onOpenComplete;
|
|
945
|
+
}, [config.onOpenComplete]);
|
|
946
|
+
const handleAnimationComplete = () => {
|
|
947
|
+
completeOpeningAnimation(hasEnteredRef, isTop, onOpenCompleteRef);
|
|
948
|
+
};
|
|
943
949
|
useDrag(panelRef, {
|
|
944
|
-
|
|
950
|
+
activeSnapIndex,
|
|
945
951
|
closeThreshold: config.closeThreshold,
|
|
946
|
-
|
|
947
|
-
|
|
952
|
+
enabled: isTop && config.drag && config.dismissible && !prefersReducedMotion,
|
|
953
|
+
isNested,
|
|
948
954
|
onClose: swipeClose,
|
|
949
955
|
onPop: swipePop,
|
|
950
|
-
isNested,
|
|
951
|
-
snapHeights,
|
|
952
|
-
activeSnapIndex,
|
|
953
956
|
onSnap,
|
|
954
|
-
sequential: config.snapToSequentialPoints
|
|
957
|
+
sequential: config.snapToSequentialPoints,
|
|
958
|
+
side,
|
|
959
|
+
snapHeights,
|
|
960
|
+
velocityThreshold: config.velocityThreshold
|
|
955
961
|
}, setDragState);
|
|
956
|
-
const ariaLabel =
|
|
962
|
+
const ariaLabel = getPanelAriaLabel(item, config.ariaLabel);
|
|
957
963
|
const panelId = `stacksheet-${item.id}`;
|
|
958
964
|
const [hasDescription, setHasDescription] = useState(false);
|
|
959
|
-
const registerDescription =
|
|
965
|
+
const registerDescription = () => {
|
|
960
966
|
setHasDescription(true);
|
|
961
967
|
return () => setHasDescription(false);
|
|
962
|
-
}
|
|
963
|
-
const panelContext =
|
|
968
|
+
};
|
|
969
|
+
const panelContext = getPanelContext({
|
|
964
970
|
close,
|
|
965
|
-
back: pop,
|
|
966
|
-
isNested,
|
|
967
|
-
isTop,
|
|
968
|
-
panelId,
|
|
969
|
-
side,
|
|
970
971
|
hasDescription,
|
|
971
|
-
registerDescription
|
|
972
|
-
}), [
|
|
973
|
-
close,
|
|
974
|
-
pop,
|
|
975
972
|
isNested,
|
|
976
973
|
isTop,
|
|
977
974
|
panelId,
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
975
|
+
pop,
|
|
976
|
+
registerDescription,
|
|
977
|
+
side
|
|
978
|
+
});
|
|
982
979
|
const isComposable = resolvePanelLayout(layout, renderHeader) === "composable";
|
|
983
980
|
const hasPanelClass = classNames.panel !== "";
|
|
984
981
|
const dragOffset = getDragTransform(side, dragState.offset);
|
|
985
982
|
const panelStyle = buildPanelStyle(panelStyles, isTop, hasPanelClass, dragState.isDragging);
|
|
986
|
-
const headerProps =
|
|
987
|
-
isNested,
|
|
988
|
-
onBack: pop,
|
|
989
|
-
onClose: close,
|
|
990
|
-
side
|
|
991
|
-
}), [
|
|
983
|
+
const headerProps = getHeaderProps({
|
|
992
984
|
close,
|
|
993
985
|
isNested,
|
|
994
986
|
pop,
|
|
995
987
|
side
|
|
996
|
-
|
|
988
|
+
});
|
|
997
989
|
const ariaProps = buildAriaProps(isTop, config.modal, isComposable, ariaLabel, panelId, hasDescription);
|
|
998
990
|
const transition = buildPanelTransition(dragState.isDragging, isTop, spring, stackSpring);
|
|
999
991
|
const animatedRadius = getAnimatedBorderRadius(side, depth, config.stacking);
|
|
@@ -1003,18 +995,28 @@ const SheetPanel = memo(function SheetPanel({ item, index, depth, isTop, isNeste
|
|
|
1003
995
|
const initialRadius = getInitialRadius(side);
|
|
1004
996
|
const showSideHandle = isTop && side !== "bottom";
|
|
1005
997
|
const showBottomHandle = isTop && side === "bottom";
|
|
998
|
+
const dismiss = isNested ? pop : close;
|
|
999
|
+
const sideHandle = getOptionalSideHandle({
|
|
1000
|
+
isHovered,
|
|
1001
|
+
onDismiss: dismiss,
|
|
1002
|
+
show: showSideHandle,
|
|
1003
|
+
side
|
|
1004
|
+
});
|
|
1005
|
+
const bottomHandle = getOptionalBottomHandle(showBottomHandle, dismiss);
|
|
1006
|
+
const hoverProps = getPanelHoverProps(showSideHandle, setIsHovered);
|
|
1007
|
+
const inactivePanelProps = getInactivePanelProps(isTop);
|
|
1006
1008
|
const panelContent = /* @__PURE__ */ jsxs(m.div, {
|
|
1007
1009
|
animate: animateTarget,
|
|
1008
1010
|
className: classNames.panel || void 0,
|
|
1009
1011
|
exit: {
|
|
1010
1012
|
...resolvedSlideFrom,
|
|
1011
|
-
opacity: .6,
|
|
1012
1013
|
boxShadow: getShadow(false),
|
|
1014
|
+
opacity: .6,
|
|
1013
1015
|
transition: {
|
|
1014
|
-
|
|
1016
|
+
boxShadow: VISUAL_TWEEN,
|
|
1015
1017
|
duration: prefersReducedMotion ? 0 : .24,
|
|
1016
1018
|
ease: "easeOut",
|
|
1017
|
-
|
|
1019
|
+
type: "tween"
|
|
1018
1020
|
}
|
|
1019
1021
|
},
|
|
1020
1022
|
initial: {
|
|
@@ -1024,25 +1026,15 @@ const SheetPanel = memo(function SheetPanel({ item, index, depth, isTop, isNeste
|
|
|
1024
1026
|
boxShadow: getShadow(false)
|
|
1025
1027
|
},
|
|
1026
1028
|
onAnimationComplete: handleAnimationComplete,
|
|
1027
|
-
onBlur: showSideHandle ? () => setIsHovered(false) : void 0,
|
|
1028
|
-
onFocus: showSideHandle ? () => setIsHovered(true) : void 0,
|
|
1029
|
-
onMouseEnter: showSideHandle ? () => setIsHovered(true) : void 0,
|
|
1030
|
-
onMouseLeave: showSideHandle ? () => setIsHovered(false) : void 0,
|
|
1031
1029
|
ref: panelRef,
|
|
1032
1030
|
style: panelStyle,
|
|
1033
1031
|
tabIndex: isTop ? -1 : void 0,
|
|
1034
|
-
...
|
|
1035
|
-
|
|
1036
|
-
inert: true
|
|
1037
|
-
},
|
|
1032
|
+
...hoverProps,
|
|
1033
|
+
...inactivePanelProps,
|
|
1038
1034
|
...ariaProps,
|
|
1039
|
-
children: [
|
|
1040
|
-
isHovered,
|
|
1041
|
-
onDismiss: isNested ? pop : close,
|
|
1042
|
-
side
|
|
1043
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1035
|
+
children: [sideHandle, /* @__PURE__ */ jsxs("div", {
|
|
1044
1036
|
className: "relative flex min-h-0 flex-1 flex-col overflow-hidden rounded-[inherit]",
|
|
1045
|
-
children: [
|
|
1037
|
+
children: [bottomHandle, /* @__PURE__ */ jsx(PanelInnerContent, {
|
|
1046
1038
|
Content,
|
|
1047
1039
|
data: item.data,
|
|
1048
1040
|
headerClassName: classNames.header || void 0,
|
|
@@ -1062,9 +1054,31 @@ const SheetPanel = memo(function SheetPanel({ item, index, depth, isTop, isNeste
|
|
|
1062
1054
|
children: panelContent
|
|
1063
1055
|
})
|
|
1064
1056
|
});
|
|
1065
|
-
}
|
|
1057
|
+
};
|
|
1066
1058
|
//#endregion
|
|
1067
1059
|
//#region src/renderer.tsx
|
|
1060
|
+
const handleBackdropExitComplete = () => {
|
|
1061
|
+
requestAnimationFrame(() => {
|
|
1062
|
+
document.body.offsetHeight;
|
|
1063
|
+
});
|
|
1064
|
+
};
|
|
1065
|
+
const getResolvedSnapHeights = (side, snapPoints, viewportHeight) => side === "bottom" && snapPoints.length > 0 ? resolveSnapPoints(snapPoints, viewportHeight) : [];
|
|
1066
|
+
const getActiveInternalSnapIndex = ({ defaultSnapIndex, internalSnap, snapContext, snapHeights }) => internalSnap.context === snapContext && internalSnap.snapCount === snapHeights.length ? internalSnap.index : defaultSnapIndex;
|
|
1067
|
+
const getMotionSpring = (prefersReducedMotion, config) => prefersReducedMotion ? {
|
|
1068
|
+
duration: 0,
|
|
1069
|
+
type: "tween"
|
|
1070
|
+
} : {
|
|
1071
|
+
damping: config.spring.damping,
|
|
1072
|
+
mass: config.spring.mass,
|
|
1073
|
+
stiffness: config.spring.stiffness,
|
|
1074
|
+
type: "spring"
|
|
1075
|
+
};
|
|
1076
|
+
const getBackdropStyle = (config, hasBackdropClass) => ({
|
|
1077
|
+
cursor: config.closeOnBackdrop && config.dismissible ? "pointer" : void 0,
|
|
1078
|
+
willChange: "opacity",
|
|
1079
|
+
zIndex: config.zIndex,
|
|
1080
|
+
...hasBackdropClass ? {} : { background: "var(--overlay, rgba(0, 0, 0, 0.15))" }
|
|
1081
|
+
});
|
|
1068
1082
|
/**
|
|
1069
1083
|
* Root renderer component — manages the backdrop, scroll lock, snap points,
|
|
1070
1084
|
* close reasons, focus restoration, keyboard/CloseWatcher dismissal, and
|
|
@@ -1072,20 +1086,16 @@ const SheetPanel = memo(function SheetPanel({ item, index, depth, isTop, isNeste
|
|
|
1072
1086
|
*
|
|
1073
1087
|
* Mounted inside a Portal by `StacksheetProvider`.
|
|
1074
1088
|
*/
|
|
1075
|
-
|
|
1089
|
+
const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classNamesProp, layout, renderHeader }) => {
|
|
1076
1090
|
const isOpen = useStore(store, (s) => s.isOpen);
|
|
1077
1091
|
const stack = useStore(store, (s) => s.stack);
|
|
1078
1092
|
const rawClose = useStore(store, (s) => s.close);
|
|
1079
1093
|
const rawPop = useStore(store, (s) => s.pop);
|
|
1080
1094
|
const side = useResolvedSide(config);
|
|
1081
1095
|
const prefersReducedMotion = useReducedMotion() ?? false;
|
|
1082
|
-
const classNames =
|
|
1096
|
+
const classNames = resolveClassNames(classNamesProp);
|
|
1083
1097
|
const viewportHeight = useViewportHeight(isOpen && side === "bottom" && config.snapPoints.length > 0);
|
|
1084
|
-
const snapHeights =
|
|
1085
|
-
side,
|
|
1086
|
-
config.snapPoints,
|
|
1087
|
-
viewportHeight
|
|
1088
|
-
]);
|
|
1098
|
+
const snapHeights = getResolvedSnapHeights(side, config.snapPoints, viewportHeight);
|
|
1089
1099
|
const snapContext = isOpen ? stack.map((item) => item.id).join("\0") : "";
|
|
1090
1100
|
const defaultSnapIndex = snapHeights.length > 0 ? snapHeights.length - 1 : 0;
|
|
1091
1101
|
const [internalSnap, setInternalSnap] = useState({
|
|
@@ -1093,31 +1103,32 @@ function SheetRenderer({ store, config, sheets, componentMap, classNames: classN
|
|
|
1093
1103
|
index: defaultSnapIndex,
|
|
1094
1104
|
snapCount: snapHeights.length
|
|
1095
1105
|
});
|
|
1096
|
-
const internalSnapIndex =
|
|
1106
|
+
const internalSnapIndex = getActiveInternalSnapIndex({
|
|
1107
|
+
defaultSnapIndex,
|
|
1108
|
+
internalSnap,
|
|
1109
|
+
snapContext,
|
|
1110
|
+
snapHeights
|
|
1111
|
+
});
|
|
1097
1112
|
const activeSnapIndex = config.snapPointIndex ?? internalSnapIndex;
|
|
1098
|
-
const handleSnap =
|
|
1113
|
+
const handleSnap = (index) => {
|
|
1099
1114
|
setInternalSnap({
|
|
1100
1115
|
context: snapContext,
|
|
1101
1116
|
index,
|
|
1102
1117
|
snapCount: snapHeights.length
|
|
1103
1118
|
});
|
|
1104
1119
|
config.onSnapPointChange?.(index);
|
|
1105
|
-
}
|
|
1106
|
-
config,
|
|
1107
|
-
snapContext,
|
|
1108
|
-
snapHeights.length
|
|
1109
|
-
]);
|
|
1120
|
+
};
|
|
1110
1121
|
const closeReasonRef = useRef("programmatic");
|
|
1111
|
-
const closeWith =
|
|
1122
|
+
const closeWith = (reason) => {
|
|
1112
1123
|
closeReasonRef.current = reason;
|
|
1113
1124
|
rawClose();
|
|
1114
|
-
}
|
|
1115
|
-
const popWith =
|
|
1125
|
+
};
|
|
1126
|
+
const popWith = (reason) => {
|
|
1116
1127
|
closeReasonRef.current = reason;
|
|
1117
1128
|
rawPop();
|
|
1118
|
-
}
|
|
1119
|
-
const close =
|
|
1120
|
-
const pop =
|
|
1129
|
+
};
|
|
1130
|
+
const close = () => closeWith("programmatic");
|
|
1131
|
+
const pop = () => popWith("programmatic");
|
|
1121
1132
|
useBodyScale(config, isOpen, prefersReducedMotion);
|
|
1122
1133
|
const triggerRef = useRef(null);
|
|
1123
1134
|
const wasOpenRef = useRef(false);
|
|
@@ -1136,72 +1147,62 @@ function SheetRenderer({ store, config, sheets, componentMap, classNames: classN
|
|
|
1136
1147
|
}, [stack.length]);
|
|
1137
1148
|
useEffect(() => {
|
|
1138
1149
|
if (!(isOpen && config.closeOnEscape && config.dismissible)) return;
|
|
1139
|
-
|
|
1150
|
+
const handleKeyDown = (e) => {
|
|
1140
1151
|
if (e.key === "Escape") {
|
|
1141
1152
|
e.preventDefault();
|
|
1142
|
-
if (stackLengthRef.current > 1)
|
|
1143
|
-
|
|
1153
|
+
if (stackLengthRef.current > 1) {
|
|
1154
|
+
closeReasonRef.current = "escape";
|
|
1155
|
+
rawPop();
|
|
1156
|
+
} else {
|
|
1157
|
+
closeReasonRef.current = "escape";
|
|
1158
|
+
rawClose();
|
|
1159
|
+
}
|
|
1144
1160
|
}
|
|
1145
|
-
}
|
|
1161
|
+
};
|
|
1146
1162
|
document.addEventListener("keydown", handleKeyDown);
|
|
1147
1163
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
1148
1164
|
}, [
|
|
1149
1165
|
isOpen,
|
|
1150
1166
|
config.closeOnEscape,
|
|
1151
1167
|
config.dismissible,
|
|
1152
|
-
|
|
1153
|
-
|
|
1168
|
+
rawPop,
|
|
1169
|
+
rawClose
|
|
1154
1170
|
]);
|
|
1155
1171
|
useEffect(() => {
|
|
1156
|
-
if (!(isOpen && config.dismissible) ||
|
|
1172
|
+
if (!(isOpen && config.dismissible) || globalThis.CloseWatcher === void 0) return;
|
|
1157
1173
|
const watcher = new globalThis.CloseWatcher();
|
|
1158
|
-
|
|
1159
|
-
if (stackLengthRef.current > 1)
|
|
1160
|
-
|
|
1174
|
+
const handleClose = () => {
|
|
1175
|
+
if (stackLengthRef.current > 1) {
|
|
1176
|
+
closeReasonRef.current = "escape";
|
|
1177
|
+
rawPop();
|
|
1178
|
+
} else {
|
|
1179
|
+
closeReasonRef.current = "escape";
|
|
1180
|
+
rawClose();
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
watcher.addEventListener("close", handleClose);
|
|
1184
|
+
return () => {
|
|
1185
|
+
watcher.removeEventListener("close", handleClose);
|
|
1186
|
+
watcher.destroy();
|
|
1161
1187
|
};
|
|
1162
|
-
return () => watcher.destroy();
|
|
1163
1188
|
}, [
|
|
1164
1189
|
isOpen,
|
|
1165
1190
|
config.dismissible,
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
]);
|
|
1169
|
-
const slideFrom = useMemo(() => getSlideFrom(side), [side]);
|
|
1170
|
-
const slideTarget = useMemo(() => getSlideTarget(), []);
|
|
1171
|
-
const spring = useMemo(() => prefersReducedMotion ? {
|
|
1172
|
-
type: "tween",
|
|
1173
|
-
duration: 0
|
|
1174
|
-
} : {
|
|
1175
|
-
type: "spring",
|
|
1176
|
-
damping: config.spring.damping,
|
|
1177
|
-
stiffness: config.spring.stiffness,
|
|
1178
|
-
mass: config.spring.mass
|
|
1179
|
-
}, [
|
|
1180
|
-
prefersReducedMotion,
|
|
1181
|
-
config.spring.damping,
|
|
1182
|
-
config.spring.stiffness,
|
|
1183
|
-
config.spring.mass
|
|
1191
|
+
rawPop,
|
|
1192
|
+
rawClose
|
|
1184
1193
|
]);
|
|
1194
|
+
const slideFrom = getSlideFrom(side);
|
|
1195
|
+
const slideTarget = getSlideTarget();
|
|
1196
|
+
const spring = getMotionSpring(prefersReducedMotion, config);
|
|
1185
1197
|
const stackSpring = spring;
|
|
1186
1198
|
const isModal = config.modal;
|
|
1187
1199
|
const showOverlay = isModal && config.showOverlay;
|
|
1188
|
-
const
|
|
1189
|
-
const
|
|
1190
|
-
zIndex: config.zIndex,
|
|
1191
|
-
willChange: "opacity",
|
|
1192
|
-
cursor: config.closeOnBackdrop && config.dismissible ? "pointer" : void 0,
|
|
1193
|
-
...hasBackdropClass ? {} : { background: "var(--overlay, rgba(0, 0, 0, 0.15))" }
|
|
1194
|
-
};
|
|
1195
|
-
const handleExitComplete = useCallback(() => {
|
|
1200
|
+
const backdropStyle = getBackdropStyle(config, classNames.backdrop !== "");
|
|
1201
|
+
const handleExitComplete = () => {
|
|
1196
1202
|
if (stack.length === 0) config.onCloseComplete?.(closeReasonRef.current);
|
|
1197
|
-
}
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
document.body.offsetHeight;
|
|
1201
|
-
});
|
|
1202
|
-
}, []);
|
|
1203
|
-
const swipeClose = useCallback(() => closeWith("swipe"), [closeWith]);
|
|
1204
|
-
const swipePop = useCallback(() => popWith("swipe"), [popWith]);
|
|
1203
|
+
};
|
|
1204
|
+
const swipeClose = () => closeWith("swipe");
|
|
1205
|
+
const swipePop = () => popWith("swipe");
|
|
1205
1206
|
const shouldLockScroll = isOpen && isModal && config.lockScroll;
|
|
1206
1207
|
return /* @__PURE__ */ jsxs(LazyMotion, {
|
|
1207
1208
|
features: domMax,
|
|
@@ -1260,22 +1261,22 @@ function SheetRenderer({ store, config, sheets, componentMap, classNames: classN
|
|
|
1260
1261
|
})
|
|
1261
1262
|
})]
|
|
1262
1263
|
});
|
|
1263
|
-
}
|
|
1264
|
+
};
|
|
1264
1265
|
//#endregion
|
|
1265
|
-
//#region src/Store/
|
|
1266
|
-
|
|
1266
|
+
//#region src/Store/store-args.ts
|
|
1267
|
+
const resolvePresentationOptions = (value) => {
|
|
1267
1268
|
if (!(value && typeof value === "object")) return;
|
|
1268
1269
|
const candidate = value;
|
|
1269
1270
|
if (candidate.ariaLabel !== void 0 && typeof candidate.ariaLabel !== "string") return;
|
|
1270
1271
|
return candidate;
|
|
1271
|
-
}
|
|
1272
|
+
};
|
|
1272
1273
|
/**
|
|
1273
1274
|
* Dev-mode warning: detect likely inline arrow functions passed as ad-hoc components.
|
|
1274
1275
|
* When a new component reference has the same displayName/name as an existing one,
|
|
1275
1276
|
* it's almost always an inline arrow being re-created every render.
|
|
1276
1277
|
*/
|
|
1277
|
-
|
|
1278
|
-
if (
|
|
1278
|
+
const warnInlineComponent = (component, componentRegistry, warnedNames) => {
|
|
1279
|
+
if (process === void 0 || process?.env?.NODE_ENV === "production") return;
|
|
1279
1280
|
const name = component.displayName || component.name;
|
|
1280
1281
|
if (!name) return;
|
|
1281
1282
|
if (warnedNames.has(name)) return;
|
|
@@ -1284,12 +1285,12 @@ function warnInlineComponent(component, componentRegistry, warnedNames) {
|
|
|
1284
1285
|
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.`);
|
|
1285
1286
|
return;
|
|
1286
1287
|
}
|
|
1287
|
-
}
|
|
1288
|
+
};
|
|
1288
1289
|
/**
|
|
1289
1290
|
* If `first` is a function (component), register it and return { type, id, data }.
|
|
1290
1291
|
* Otherwise, pass through the string-based (type, id, data) args unchanged.
|
|
1291
1292
|
*/
|
|
1292
|
-
|
|
1293
|
+
const resolveArgs = (componentRegistry, componentMap, getNextKey, warnedNames, first, second, third, fourth) => {
|
|
1293
1294
|
if (typeof first === "function") {
|
|
1294
1295
|
const component = first;
|
|
1295
1296
|
let typeKey = componentRegistry.get(component);
|
|
@@ -1301,26 +1302,26 @@ function resolveArgs(componentRegistry, componentMap, getNextKey, warnedNames, f
|
|
|
1301
1302
|
}
|
|
1302
1303
|
if (typeof second === "string") return {
|
|
1303
1304
|
ariaLabel: resolvePresentationOptions(fourth)?.ariaLabel,
|
|
1304
|
-
|
|
1305
|
+
data: third ?? {},
|
|
1305
1306
|
id: second,
|
|
1306
|
-
|
|
1307
|
+
type: typeKey
|
|
1307
1308
|
};
|
|
1308
1309
|
return {
|
|
1309
1310
|
ariaLabel: resolvePresentationOptions(third)?.ariaLabel,
|
|
1310
|
-
|
|
1311
|
+
data: second ?? {},
|
|
1311
1312
|
id: crypto.randomUUID(),
|
|
1312
|
-
|
|
1313
|
+
type: typeKey
|
|
1313
1314
|
};
|
|
1314
1315
|
}
|
|
1315
1316
|
return {
|
|
1316
1317
|
ariaLabel: resolvePresentationOptions(fourth)?.ariaLabel,
|
|
1317
|
-
|
|
1318
|
+
data: third ?? {},
|
|
1318
1319
|
id: second,
|
|
1319
|
-
|
|
1320
|
+
type: first
|
|
1320
1321
|
};
|
|
1321
|
-
}
|
|
1322
|
+
};
|
|
1322
1323
|
//#endregion
|
|
1323
|
-
//#region src/Store/
|
|
1324
|
+
//#region src/Store/create-sheet-store.ts
|
|
1324
1325
|
/**
|
|
1325
1326
|
* Create an isolated Zustand store for a sheet stack instance.
|
|
1326
1327
|
*
|
|
@@ -1332,119 +1333,86 @@ function resolveArgs(componentRegistry, componentMap, getNextKey, warnedNames, f
|
|
|
1332
1333
|
* The ad-hoc counter is scoped per instance to prevent identity leaks across
|
|
1333
1334
|
* multiple `createStacksheet()` calls or test runs.
|
|
1334
1335
|
*/
|
|
1335
|
-
|
|
1336
|
+
const createSheetStore = (config) => {
|
|
1336
1337
|
const componentRegistry = /* @__PURE__ */ new Map();
|
|
1337
1338
|
const componentMap = /* @__PURE__ */ new Map();
|
|
1338
1339
|
let adhocCounter = 0;
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1340
|
+
const getNextKey = () => {
|
|
1341
|
+
const key = `__adhoc_${adhocCounter}`;
|
|
1342
|
+
adhocCounter += 1;
|
|
1343
|
+
return key;
|
|
1344
|
+
};
|
|
1342
1345
|
const warnedNames = /* @__PURE__ */ new Set();
|
|
1343
|
-
|
|
1344
|
-
return resolveArgs(componentRegistry, componentMap, getNextKey, warnedNames, first, second, third, fourth);
|
|
1345
|
-
}
|
|
1346
|
+
const resolve = (first, second, third, fourth) => resolveArgs(componentRegistry, componentMap, getNextKey, warnedNames, first, second, third, fourth);
|
|
1346
1347
|
/** Remove registry entries for type keys no longer in the stack */
|
|
1347
|
-
|
|
1348
|
+
const pruneRegistry = (remainingStack) => {
|
|
1348
1349
|
const usedTypes = new Set(remainingStack.map((item) => item.type));
|
|
1349
1350
|
for (const [component, typeKey] of componentRegistry) if (!usedTypes.has(typeKey)) {
|
|
1350
1351
|
componentRegistry.delete(component);
|
|
1351
1352
|
componentMap.delete(typeKey);
|
|
1352
1353
|
}
|
|
1353
|
-
}
|
|
1354
|
+
};
|
|
1354
1355
|
return {
|
|
1356
|
+
componentMap,
|
|
1357
|
+
componentRegistry,
|
|
1355
1358
|
store: createStore()((set, get) => {
|
|
1356
|
-
|
|
1359
|
+
const _openResolved = ({ type, id, data, ariaLabel }) => {
|
|
1357
1360
|
set({
|
|
1361
|
+
isOpen: true,
|
|
1358
1362
|
stack: [{
|
|
1359
|
-
|
|
1360
|
-
type,
|
|
1363
|
+
ariaLabel,
|
|
1361
1364
|
data,
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
+
id,
|
|
1366
|
+
type
|
|
1367
|
+
}]
|
|
1365
1368
|
});
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1369
|
+
};
|
|
1370
|
+
const _pushResolved = ({ type, id, data, ariaLabel }) => {
|
|
1368
1371
|
set((state) => {
|
|
1369
1372
|
const item = {
|
|
1370
|
-
|
|
1371
|
-
type,
|
|
1373
|
+
ariaLabel,
|
|
1372
1374
|
data,
|
|
1373
|
-
|
|
1375
|
+
id,
|
|
1376
|
+
type
|
|
1374
1377
|
};
|
|
1375
1378
|
if (Number.isFinite(config.maxDepth) && state.stack.length >= config.maxDepth) return {
|
|
1376
|
-
|
|
1377
|
-
|
|
1379
|
+
isOpen: true,
|
|
1380
|
+
stack: [...state.stack.slice(0, -1), item]
|
|
1378
1381
|
};
|
|
1379
1382
|
return {
|
|
1380
|
-
|
|
1381
|
-
|
|
1383
|
+
isOpen: true,
|
|
1384
|
+
stack: [...state.stack, item]
|
|
1382
1385
|
};
|
|
1383
1386
|
});
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1387
|
+
};
|
|
1388
|
+
const _replaceResolved = ({ type, id, data, ariaLabel }) => {
|
|
1386
1389
|
set((state) => {
|
|
1387
1390
|
const item = {
|
|
1388
|
-
|
|
1389
|
-
type,
|
|
1391
|
+
ariaLabel,
|
|
1390
1392
|
data,
|
|
1391
|
-
|
|
1393
|
+
id,
|
|
1394
|
+
type
|
|
1392
1395
|
};
|
|
1393
1396
|
if (state.stack.length === 0) return {
|
|
1394
|
-
|
|
1395
|
-
|
|
1397
|
+
isOpen: true,
|
|
1398
|
+
stack: [item]
|
|
1396
1399
|
};
|
|
1397
1400
|
return {
|
|
1398
|
-
|
|
1399
|
-
|
|
1401
|
+
isOpen: true,
|
|
1402
|
+
stack: [...state.stack.slice(0, -1), item]
|
|
1400
1403
|
};
|
|
1401
1404
|
});
|
|
1402
|
-
}
|
|
1405
|
+
};
|
|
1403
1406
|
return {
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
push(first, second, third, fourth) {
|
|
1410
|
-
_pushResolved(resolve(first, second, third, fourth));
|
|
1411
|
-
},
|
|
1412
|
-
replace(first, second, third, fourth) {
|
|
1413
|
-
_replaceResolved(resolve(first, second, third, fourth));
|
|
1414
|
-
},
|
|
1415
|
-
swap(first, second, third) {
|
|
1416
|
-
let type;
|
|
1417
|
-
let data;
|
|
1418
|
-
const ariaLabel = resolvePresentationOptions(third)?.ariaLabel;
|
|
1419
|
-
if (typeof first === "function") {
|
|
1420
|
-
const component = first;
|
|
1421
|
-
let typeKey = componentRegistry.get(component);
|
|
1422
|
-
if (!typeKey) {
|
|
1423
|
-
warnInlineComponent(component, componentRegistry, warnedNames);
|
|
1424
|
-
typeKey = getNextKey();
|
|
1425
|
-
componentRegistry.set(component, typeKey);
|
|
1426
|
-
componentMap.set(typeKey, component);
|
|
1427
|
-
}
|
|
1428
|
-
type = typeKey;
|
|
1429
|
-
data = second ?? {};
|
|
1430
|
-
} else {
|
|
1431
|
-
type = first;
|
|
1432
|
-
data = second ?? {};
|
|
1433
|
-
}
|
|
1434
|
-
set((state) => {
|
|
1435
|
-
const top = state.stack.at(-1);
|
|
1436
|
-
if (!top) return state;
|
|
1437
|
-
const newStack = [...state.stack];
|
|
1438
|
-
newStack[newStack.length - 1] = {
|
|
1439
|
-
id: top.id,
|
|
1440
|
-
type,
|
|
1441
|
-
data,
|
|
1442
|
-
ariaLabel: ariaLabel ?? top.ariaLabel
|
|
1443
|
-
};
|
|
1444
|
-
return { stack: newStack };
|
|
1407
|
+
close: () => {
|
|
1408
|
+
pruneRegistry([]);
|
|
1409
|
+
set({
|
|
1410
|
+
isOpen: false,
|
|
1411
|
+
stack: []
|
|
1445
1412
|
});
|
|
1446
1413
|
},
|
|
1447
|
-
|
|
1414
|
+
isOpen: false,
|
|
1415
|
+
navigate: (first, second, third, fourth) => {
|
|
1448
1416
|
const resolved = resolve(first, second, third, fourth);
|
|
1449
1417
|
const { stack } = get();
|
|
1450
1418
|
const top = stack.at(-1);
|
|
@@ -1460,61 +1428,94 @@ function createSheetStore(config) {
|
|
|
1460
1428
|
}
|
|
1461
1429
|
_pushResolved(resolved);
|
|
1462
1430
|
},
|
|
1463
|
-
|
|
1464
|
-
|
|
1431
|
+
open: (first, second, third, fourth) => {
|
|
1432
|
+
_openResolved(resolve(first, second, third, fourth));
|
|
1433
|
+
},
|
|
1434
|
+
pop: () => {
|
|
1465
1435
|
set((state) => {
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
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
|
|
1472
1448
|
};
|
|
1473
|
-
return { stack: updated };
|
|
1474
1449
|
});
|
|
1475
1450
|
},
|
|
1476
|
-
|
|
1451
|
+
push: (first, second, third, fourth) => {
|
|
1452
|
+
_pushResolved(resolve(first, second, third, fourth));
|
|
1453
|
+
},
|
|
1454
|
+
remove: (id) => {
|
|
1477
1455
|
set((state) => {
|
|
1478
1456
|
const next = state.stack.filter((item) => item.id !== id);
|
|
1479
1457
|
if (next.length === state.stack.length) return state;
|
|
1480
1458
|
pruneRegistry(next);
|
|
1481
1459
|
if (next.length === 0) return {
|
|
1482
|
-
|
|
1483
|
-
|
|
1460
|
+
isOpen: false,
|
|
1461
|
+
stack: []
|
|
1484
1462
|
};
|
|
1485
1463
|
return { stack: next };
|
|
1486
1464
|
});
|
|
1487
1465
|
},
|
|
1488
|
-
|
|
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);
|
|
1489
1471
|
set((state) => {
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
}
|
|
1497
|
-
const next = state.stack.slice(0, -1);
|
|
1498
|
-
pruneRegistry(next);
|
|
1499
|
-
return {
|
|
1500
|
-
stack: next,
|
|
1501
|
-
isOpen: true
|
|
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
|
|
1502
1478
|
};
|
|
1479
|
+
return { stack: updated };
|
|
1503
1480
|
});
|
|
1504
1481
|
},
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
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 };
|
|
1510
1513
|
});
|
|
1511
1514
|
}
|
|
1512
1515
|
};
|
|
1513
|
-
})
|
|
1514
|
-
componentRegistry,
|
|
1515
|
-
componentMap
|
|
1516
|
+
})
|
|
1516
1517
|
};
|
|
1517
|
-
}
|
|
1518
|
+
};
|
|
1518
1519
|
//#endregion
|
|
1519
1520
|
//#region src/create.tsx
|
|
1520
1521
|
/**
|
|
@@ -1547,71 +1548,67 @@ function createSheetStore(config) {
|
|
|
1547
1548
|
*
|
|
1548
1549
|
* Use `useSheetPanel()` inside content components to access `close()` and `back()`.
|
|
1549
1550
|
*/
|
|
1550
|
-
|
|
1551
|
+
const createStacksheet = (config) => {
|
|
1551
1552
|
const resolved = resolveConfig(config);
|
|
1552
1553
|
const { store, componentMap } = createSheetStore(resolved);
|
|
1553
1554
|
const StoreContext = createContext(null);
|
|
1554
|
-
|
|
1555
|
-
const ctx =
|
|
1555
|
+
const useStoreContext = () => {
|
|
1556
|
+
const ctx = use(StoreContext);
|
|
1556
1557
|
if (!ctx) throw new Error("useSheet/useStacksheetState must be used within <StacksheetProvider>");
|
|
1557
1558
|
return ctx;
|
|
1558
|
-
}
|
|
1559
|
+
};
|
|
1559
1560
|
const EMPTY_SHEETS = {};
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
}
|
|
1581
|
-
function useSheet() {
|
|
1561
|
+
const providerValue = {
|
|
1562
|
+
config: resolved,
|
|
1563
|
+
store
|
|
1564
|
+
};
|
|
1565
|
+
const StacksheetProvider = ({ sheets = EMPTY_SHEETS, children, classNames, layout, renderHeader }) => /* @__PURE__ */ jsxs(StoreContext.Provider, {
|
|
1566
|
+
value: providerValue,
|
|
1567
|
+
children: [children, /* @__PURE__ */ jsx(Portal, {
|
|
1568
|
+
asChild: false,
|
|
1569
|
+
children: /* @__PURE__ */ jsx(SheetRenderer, {
|
|
1570
|
+
classNames,
|
|
1571
|
+
componentMap,
|
|
1572
|
+
config: resolved,
|
|
1573
|
+
layout,
|
|
1574
|
+
renderHeader,
|
|
1575
|
+
sheets,
|
|
1576
|
+
store
|
|
1577
|
+
})
|
|
1578
|
+
})]
|
|
1579
|
+
});
|
|
1580
|
+
const useSheet = () => {
|
|
1582
1581
|
const { store: s } = useStoreContext();
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
}
|
|
1598
|
-
function useStacksheetState() {
|
|
1582
|
+
const state = s.getState();
|
|
1583
|
+
return {
|
|
1584
|
+
close: state.close,
|
|
1585
|
+
navigate: state.navigate,
|
|
1586
|
+
open: state.open,
|
|
1587
|
+
pop: state.pop,
|
|
1588
|
+
push: state.push,
|
|
1589
|
+
remove: state.remove,
|
|
1590
|
+
replace: state.replace,
|
|
1591
|
+
setData: state.setData,
|
|
1592
|
+
swap: state.swap
|
|
1593
|
+
};
|
|
1594
|
+
};
|
|
1595
|
+
const useStacksheetState = () => {
|
|
1599
1596
|
const { store: s } = useStoreContext();
|
|
1600
1597
|
return useStore(s, useShallow((state) => ({
|
|
1601
|
-
|
|
1602
|
-
|
|
1598
|
+
isOpen: state.isOpen,
|
|
1599
|
+
stack: state.stack
|
|
1603
1600
|
})));
|
|
1604
|
-
}
|
|
1601
|
+
};
|
|
1605
1602
|
return {
|
|
1606
1603
|
StacksheetProvider,
|
|
1604
|
+
store,
|
|
1607
1605
|
useSheet,
|
|
1608
|
-
useStacksheetState
|
|
1609
|
-
store
|
|
1606
|
+
useStacksheetState
|
|
1610
1607
|
};
|
|
1611
|
-
}
|
|
1608
|
+
};
|
|
1612
1609
|
//#endregion
|
|
1613
|
-
//#region src/SheetParts/
|
|
1614
|
-
|
|
1610
|
+
//#region src/SheetParts/sheet-back.tsx
|
|
1611
|
+
const SheetBack = ({ asChild, className, style, children }) => {
|
|
1615
1612
|
const { back, isNested } = useSheetPanel();
|
|
1616
1613
|
if (!isNested) return null;
|
|
1617
1614
|
return /* @__PURE__ */ jsx(asChild ? Slot : "button", {
|
|
@@ -1622,10 +1619,10 @@ function SheetBack({ asChild, className, style, children }) {
|
|
|
1622
1619
|
type: asChild ? void 0 : "button",
|
|
1623
1620
|
children: children ?? /* @__PURE__ */ jsx(ArrowLeftIcon, {})
|
|
1624
1621
|
});
|
|
1625
|
-
}
|
|
1622
|
+
};
|
|
1626
1623
|
//#endregion
|
|
1627
|
-
//#region src/SheetParts/
|
|
1628
|
-
|
|
1624
|
+
//#region src/SheetParts/sheet-body.tsx
|
|
1625
|
+
const SheetBody = ({ asChild, className, style, children }) => {
|
|
1629
1626
|
if (asChild) return /* @__PURE__ */ jsx(Slot, {
|
|
1630
1627
|
className: joinClassNames("relative min-h-0 flex-1", className),
|
|
1631
1628
|
"data-stacksheet-no-drag": "",
|
|
@@ -1645,10 +1642,10 @@ function SheetBody({ asChild, className, style, children }) {
|
|
|
1645
1642
|
children: /* @__PURE__ */ jsx(Thumb, { className: "relative flex-1 rounded bg-current/15" })
|
|
1646
1643
|
})]
|
|
1647
1644
|
});
|
|
1648
|
-
}
|
|
1645
|
+
};
|
|
1649
1646
|
//#endregion
|
|
1650
|
-
//#region src/SheetParts/
|
|
1651
|
-
|
|
1647
|
+
//#region src/SheetParts/sheet-close.tsx
|
|
1648
|
+
const SheetClose = ({ asChild, className, style, children }) => {
|
|
1652
1649
|
const { close } = useSheetPanel();
|
|
1653
1650
|
return /* @__PURE__ */ jsx(asChild ? Slot : "button", {
|
|
1654
1651
|
"aria-label": children ? void 0 : "Close",
|
|
@@ -1658,10 +1655,10 @@ function SheetClose({ asChild, className, style, children }) {
|
|
|
1658
1655
|
type: asChild ? void 0 : "button",
|
|
1659
1656
|
children: children ?? /* @__PURE__ */ jsx(XIcon, {})
|
|
1660
1657
|
});
|
|
1661
|
-
}
|
|
1658
|
+
};
|
|
1662
1659
|
//#endregion
|
|
1663
|
-
//#region src/SheetParts/
|
|
1664
|
-
|
|
1660
|
+
//#region src/SheetParts/sheet-description.tsx
|
|
1661
|
+
const SheetDescription = ({ asChild, className, style, children }) => {
|
|
1665
1662
|
const { panelId, registerDescription } = useSheetPanel();
|
|
1666
1663
|
useEffect(() => registerDescription(), [registerDescription]);
|
|
1667
1664
|
return /* @__PURE__ */ jsx(asChild ? Slot : "p", {
|
|
@@ -1670,53 +1667,46 @@ function SheetDescription({ asChild, className, style, children }) {
|
|
|
1670
1667
|
style,
|
|
1671
1668
|
children
|
|
1672
1669
|
});
|
|
1673
|
-
}
|
|
1670
|
+
};
|
|
1674
1671
|
//#endregion
|
|
1675
|
-
//#region src/SheetParts/
|
|
1676
|
-
|
|
1672
|
+
//#region src/SheetParts/sheet-footer.tsx
|
|
1673
|
+
const SheetFooter = ({ asChild, className, style, children }) => {
|
|
1677
1674
|
return /* @__PURE__ */ jsx(asChild ? Slot : "footer", {
|
|
1678
1675
|
className: joinClassNames(asChild ? "shrink-0" : "flex shrink-0 items-center gap-2 border-t px-6 py-3", className),
|
|
1679
1676
|
style,
|
|
1680
1677
|
children
|
|
1681
1678
|
});
|
|
1682
|
-
}
|
|
1679
|
+
};
|
|
1683
1680
|
//#endregion
|
|
1684
|
-
//#region src/SheetParts/
|
|
1685
|
-
|
|
1681
|
+
//#region src/SheetParts/sheet-handle.tsx
|
|
1682
|
+
const SheetHandle = ({ asChild, className, style, children }) => {
|
|
1686
1683
|
const { close, back, isNested } = useSheetPanel();
|
|
1687
1684
|
const dismiss = isNested ? back : close;
|
|
1688
|
-
return /* @__PURE__ */ jsx(asChild ? Slot : "
|
|
1685
|
+
return /* @__PURE__ */ jsx(asChild ? Slot : "button", {
|
|
1689
1686
|
"aria-label": "Dismiss",
|
|
1690
|
-
className: joinClassNames(asChild ? void 0 : "flex shrink-0 cursor-grab touch-none items-center justify-center pt-4 pb-1", className),
|
|
1687
|
+
className: joinClassNames(asChild ? void 0 : "flex shrink-0 cursor-grab touch-none items-center justify-center border-none bg-transparent pt-4 pb-1 text-inherit", className),
|
|
1691
1688
|
"data-stacksheet-handle": "",
|
|
1692
1689
|
onClick: dismiss,
|
|
1693
|
-
onKeyDown: (e) => {
|
|
1694
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
1695
|
-
e.preventDefault();
|
|
1696
|
-
dismiss();
|
|
1697
|
-
}
|
|
1698
|
-
},
|
|
1699
|
-
role: "button",
|
|
1700
1690
|
style,
|
|
1701
|
-
|
|
1691
|
+
type: asChild ? void 0 : "button",
|
|
1702
1692
|
children: children ?? /* @__PURE__ */ jsx("div", {
|
|
1703
1693
|
"aria-hidden": "true",
|
|
1704
1694
|
className: "h-1 w-9 rounded-sm bg-current/25"
|
|
1705
1695
|
})
|
|
1706
1696
|
});
|
|
1707
|
-
}
|
|
1697
|
+
};
|
|
1708
1698
|
//#endregion
|
|
1709
|
-
//#region src/SheetParts/
|
|
1710
|
-
|
|
1699
|
+
//#region src/SheetParts/sheet-header.tsx
|
|
1700
|
+
const SheetHeader = ({ asChild, className, style, children }) => {
|
|
1711
1701
|
return /* @__PURE__ */ jsx(asChild ? Slot : "header", {
|
|
1712
1702
|
className: joinClassNames(asChild ? "shrink-0" : "flex h-14 shrink-0 items-center justify-between border-b px-6", className),
|
|
1713
1703
|
style,
|
|
1714
1704
|
children
|
|
1715
1705
|
});
|
|
1716
|
-
}
|
|
1706
|
+
};
|
|
1717
1707
|
//#endregion
|
|
1718
|
-
//#region src/SheetParts/
|
|
1719
|
-
|
|
1708
|
+
//#region src/SheetParts/sheet-title.tsx
|
|
1709
|
+
const SheetTitle = ({ asChild, className, style, children }) => {
|
|
1720
1710
|
const { panelId } = useSheetPanel();
|
|
1721
1711
|
return /* @__PURE__ */ jsx(asChild ? Slot : "h2", {
|
|
1722
1712
|
className: joinClassNames(asChild ? void 0 : "font-semibold text-sm", className),
|
|
@@ -1724,9 +1714,9 @@ function SheetTitle({ asChild, className, style, children }) {
|
|
|
1724
1714
|
style,
|
|
1725
1715
|
children
|
|
1726
1716
|
});
|
|
1727
|
-
}
|
|
1717
|
+
};
|
|
1728
1718
|
//#endregion
|
|
1729
|
-
//#region src/SheetParts/
|
|
1719
|
+
//#region src/SheetParts/sheet-parts.ts
|
|
1730
1720
|
/**
|
|
1731
1721
|
* Composable Sheet Parts for building custom Sheet layouts.
|
|
1732
1722
|
*
|