@patternmode/stacksheet 1.3.5 → 1.3.7
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 +26 -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 +1066 -893
- 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 +8 -7
- 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 +11 -17
- package/dist/springs.d.ts.map +1 -1
- 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 +31 -35
- package/dist/types.d.ts.map +1 -1
- package/package.json +13 -12
- 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,8 +1,9 @@
|
|
|
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
|
+
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,45 +12,65 @@ 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
|
-
|
|
23
|
-
stiffness: 300,
|
|
24
|
-
damping: 30,
|
|
25
|
-
mass: 1
|
|
26
|
-
},
|
|
27
|
-
snappy: {
|
|
28
|
-
stiffness: 400,
|
|
29
|
-
damping: 28,
|
|
30
|
-
mass: .8
|
|
31
|
-
},
|
|
30
|
+
snappy: toSpringConfig(springs$1.snappy),
|
|
32
31
|
stiff: {
|
|
33
|
-
stiffness: 400,
|
|
34
32
|
damping: 40,
|
|
35
|
-
mass: 1
|
|
36
|
-
|
|
33
|
+
mass: 1,
|
|
34
|
+
stiffness: 400
|
|
35
|
+
},
|
|
36
|
+
subtle: toSpringConfig(springs$1.subtle)
|
|
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,163 +79,168 @@ 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)`);
|
|
114
116
|
setIsMobile(mql.matches);
|
|
115
|
-
const handler = (e) =>
|
|
117
|
+
const handler = (e) => {
|
|
118
|
+
setIsMobile(e.matches);
|
|
119
|
+
};
|
|
116
120
|
mql.addEventListener("change", handler);
|
|
117
|
-
return () =>
|
|
121
|
+
return () => {
|
|
122
|
+
mql.removeEventListener("change", handler);
|
|
123
|
+
};
|
|
118
124
|
}, [breakpoint]);
|
|
119
125
|
return isMobile;
|
|
120
|
-
}
|
|
126
|
+
};
|
|
121
127
|
/** Resolve the current side from config + viewport. */
|
|
122
|
-
|
|
128
|
+
const useResolvedSide = (config) => {
|
|
123
129
|
return useIsMobile(config.breakpoint) ? config.side.mobile : config.side.desktop;
|
|
124
|
-
}
|
|
130
|
+
};
|
|
125
131
|
//#endregion
|
|
126
132
|
//#region src/renderer-effects.ts
|
|
127
|
-
|
|
133
|
+
const usePanelHeight = (panelRef, hasSnapPoints) => {
|
|
128
134
|
const [height, setHeight] = useState(0);
|
|
129
135
|
useEffect(() => {
|
|
130
136
|
const el = panelRef.current;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
};
|
|
138
148
|
}, [panelRef, hasSnapPoints]);
|
|
139
149
|
return height;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const [height, setHeight] = useState(() =>
|
|
150
|
+
};
|
|
151
|
+
const getViewportHeight = () => typeof window === "undefined" ? 0 : window.visualViewport?.height ?? window.innerHeight;
|
|
152
|
+
const useViewportHeight = (active) => {
|
|
153
|
+
const [height, setHeight] = useState(() => typeof window === "undefined" ? void 0 : getViewportHeight());
|
|
144
154
|
useEffect(() => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
window
|
|
149
|
-
|
|
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
|
+
}
|
|
150
163
|
return () => {
|
|
151
|
-
|
|
152
|
-
|
|
164
|
+
if (canListen) {
|
|
165
|
+
window.removeEventListener("resize", update);
|
|
166
|
+
window.visualViewport?.removeEventListener("resize", update);
|
|
167
|
+
}
|
|
153
168
|
};
|
|
154
|
-
}, [
|
|
155
|
-
return active ? height : 0;
|
|
156
|
-
}
|
|
157
|
-
|
|
169
|
+
}, []);
|
|
170
|
+
return active ? height ?? 0 : 0;
|
|
171
|
+
};
|
|
172
|
+
const useBodyScale = (config, isOpen, prefersReducedMotion) => {
|
|
158
173
|
useEffect(() => {
|
|
159
|
-
if (!config.shouldScaleBackground || prefersReducedMotion) return;
|
|
160
174
|
const wrapper = document.querySelector("[data-stacksheet-wrapper]");
|
|
161
|
-
|
|
162
|
-
|
|
175
|
+
const resetWrapper = () => {
|
|
176
|
+
if (wrapper instanceof HTMLElement) {
|
|
177
|
+
wrapper.style.transform = "";
|
|
178
|
+
wrapper.style.borderRadius = "";
|
|
179
|
+
wrapper.style.transition = "";
|
|
180
|
+
wrapper.style.overflow = "";
|
|
181
|
+
wrapper.style.transformOrigin = "";
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const canScale = config.shouldScaleBackground && !prefersReducedMotion && wrapper instanceof HTMLElement;
|
|
185
|
+
if (canScale && isOpen) {
|
|
163
186
|
const scale = config.scaleBackgroundAmount;
|
|
164
187
|
wrapper.style.transition = "transform 500ms cubic-bezier(0.32, 0.72, 0, 1), border-radius 500ms cubic-bezier(0.32, 0.72, 0, 1)";
|
|
165
188
|
wrapper.style.transform = `scale(${scale})`;
|
|
166
189
|
wrapper.style.borderRadius = "8px";
|
|
167
190
|
wrapper.style.overflow = "hidden";
|
|
168
191
|
wrapper.style.transformOrigin = "center top";
|
|
169
|
-
return;
|
|
192
|
+
return resetWrapper;
|
|
170
193
|
}
|
|
171
|
-
wrapper.style.transform = "";
|
|
172
|
-
wrapper.style.borderRadius = "";
|
|
173
194
|
const handleEnd = () => {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
195
|
+
resetWrapper();
|
|
196
|
+
};
|
|
197
|
+
if (canScale) {
|
|
198
|
+
wrapper.style.transform = "";
|
|
199
|
+
wrapper.style.borderRadius = "";
|
|
200
|
+
wrapper.addEventListener("transitionend", handleEnd, { once: true });
|
|
201
|
+
}
|
|
202
|
+
return () => {
|
|
203
|
+
if (wrapper instanceof HTMLElement) wrapper.removeEventListener("transitionend", handleEnd);
|
|
177
204
|
};
|
|
178
|
-
wrapper.addEventListener("transitionend", handleEnd, { once: true });
|
|
179
|
-
return () => wrapper.removeEventListener("transitionend", handleEnd);
|
|
180
205
|
}, [
|
|
181
206
|
isOpen,
|
|
182
207
|
config.shouldScaleBackground,
|
|
183
208
|
config.scaleBackgroundAmount,
|
|
184
209
|
prefersReducedMotion
|
|
185
210
|
]);
|
|
186
|
-
}
|
|
211
|
+
};
|
|
187
212
|
//#endregion
|
|
188
213
|
//#region src/snap-points.ts
|
|
189
|
-
const SNAP_POINT_RE = /^(
|
|
214
|
+
const SNAP_POINT_RE = /^(?<value>\d+(?:\.\d+)?)(?<unit>px|rem|em|vh|%)$/u;
|
|
215
|
+
const getRootFontSize = () => typeof document === "undefined" ? 16 : Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
216
|
+
const resolveUnitPx = (value, unit, viewportHeight) => {
|
|
217
|
+
if (unit === "px") return value;
|
|
218
|
+
if (unit === "rem" || unit === "em") return value * getRootFontSize();
|
|
219
|
+
if (unit === "vh" || unit === "%") return value / 100 * viewportHeight;
|
|
220
|
+
return 0;
|
|
221
|
+
};
|
|
190
222
|
/**
|
|
191
223
|
* Resolve a snap point value to pixels given the viewport height.
|
|
192
224
|
* - number 0-1: fraction of viewport (0.5 → 50% of vh)
|
|
193
225
|
* - number > 1: pixel value (300 → 300px)
|
|
194
226
|
* - string: parsed via regex for CSS unit support
|
|
195
227
|
*/
|
|
196
|
-
|
|
228
|
+
const resolveSnapPointPx = (point, viewportHeight) => {
|
|
197
229
|
if (typeof point === "number") return point <= 1 ? point * viewportHeight : point;
|
|
198
230
|
if (typeof point === "string") {
|
|
199
|
-
const match =
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
case "rem":
|
|
205
|
-
case "em": return value * (typeof document === "undefined" ? 16 : Number.parseFloat(getComputedStyle(document.documentElement).fontSize));
|
|
206
|
-
case "vh":
|
|
207
|
-
case "%": return value / 100 * viewportHeight;
|
|
208
|
-
default: return 0;
|
|
209
|
-
}
|
|
231
|
+
const match = SNAP_POINT_RE.exec(point);
|
|
232
|
+
const rawValue = match?.groups?.value;
|
|
233
|
+
const unit = match?.groups?.unit;
|
|
234
|
+
if (rawValue === void 0 || unit === void 0) return 0;
|
|
235
|
+
return resolveUnitPx(Number.parseFloat(rawValue), unit, viewportHeight);
|
|
210
236
|
}
|
|
211
237
|
return 0;
|
|
212
|
-
}
|
|
238
|
+
};
|
|
213
239
|
/**
|
|
214
240
|
* Resolve all snap points to sorted pixel heights (ascending).
|
|
215
241
|
* Returns an array of heights in px that the drawer should snap to.
|
|
216
242
|
*/
|
|
217
|
-
|
|
243
|
+
const resolveSnapPoints = (points, viewportHeight) => {
|
|
218
244
|
if (points.length === 0) return [];
|
|
219
245
|
const resolved = [];
|
|
220
246
|
for (const point of points) {
|
|
@@ -228,7 +254,7 @@ function resolveSnapPoints(points, viewportHeight) {
|
|
|
228
254
|
if (last === void 0 || Math.abs(px - last) > 1) deduped.push(px);
|
|
229
255
|
}
|
|
230
256
|
return deduped;
|
|
231
|
-
}
|
|
257
|
+
};
|
|
232
258
|
/** Velocity threshold (px/ms) for skipping intermediate snap points */
|
|
233
259
|
const SNAP_VELOCITY_THRESHOLD = .4;
|
|
234
260
|
/** Max velocity to consider for snap offset calculation */
|
|
@@ -244,7 +270,7 @@ const SNAP_VELOCITY_MULTIPLIER = 150;
|
|
|
244
270
|
*
|
|
245
271
|
* Returns -1 if the gesture indicates full dismissal.
|
|
246
272
|
*/
|
|
247
|
-
|
|
273
|
+
const findSnapTarget = (dragOffset, panelHeight, snapHeights, velocity, currentIndex, sequential) => {
|
|
248
274
|
if (snapHeights.length === 0) return -1;
|
|
249
275
|
const snapOffsets = snapHeights.map((h) => panelHeight - h);
|
|
250
276
|
const currentPos = dragOffset;
|
|
@@ -258,7 +284,7 @@ function findSnapTarget(dragOffset, panelHeight, snapHeights, velocity, currentI
|
|
|
258
284
|
const first = snapOffsets[0] ?? 0;
|
|
259
285
|
let bestIndex = 0;
|
|
260
286
|
let bestDist = Math.abs(projectedPos - first);
|
|
261
|
-
for (let i = 1; i < snapOffsets.length; i
|
|
287
|
+
for (let i = 1; i < snapOffsets.length; i += 1) {
|
|
262
288
|
const offset = snapOffsets[i] ?? 0;
|
|
263
289
|
const dist = Math.abs(projectedPos - offset);
|
|
264
290
|
if (dist < bestDist) {
|
|
@@ -268,31 +294,31 @@ function findSnapTarget(dragOffset, panelHeight, snapHeights, velocity, currentI
|
|
|
268
294
|
}
|
|
269
295
|
if (Math.abs(projectedPos - panelHeight) <= bestDist) return -1;
|
|
270
296
|
return bestIndex;
|
|
271
|
-
}
|
|
297
|
+
};
|
|
272
298
|
/**
|
|
273
299
|
* Get the Y offset for a snap point relative to fully open (0 offset).
|
|
274
300
|
* Returns the number of pixels the drawer should be translated down from fully open.
|
|
275
301
|
*/
|
|
276
|
-
|
|
302
|
+
const getSnapOffset = (snapIndex, snapHeights, panelHeight) => {
|
|
277
303
|
if (snapIndex < 0 || snapIndex >= snapHeights.length) return 0;
|
|
278
304
|
return panelHeight - (snapHeights[snapIndex] ?? 0);
|
|
279
|
-
}
|
|
305
|
+
};
|
|
280
306
|
//#endregion
|
|
281
307
|
//#region src/renderer-helpers.ts
|
|
282
308
|
const EMPTY_CLASSNAMES = {
|
|
283
309
|
backdrop: "",
|
|
284
|
-
|
|
285
|
-
|
|
310
|
+
header: "",
|
|
311
|
+
panel: ""
|
|
286
312
|
};
|
|
287
|
-
|
|
313
|
+
const resolveClassNames = (cn) => {
|
|
288
314
|
if (!cn) return EMPTY_CLASSNAMES;
|
|
289
315
|
return {
|
|
290
316
|
backdrop: cn.backdrop ?? "",
|
|
291
|
-
|
|
292
|
-
|
|
317
|
+
header: cn.header ?? "",
|
|
318
|
+
panel: cn.panel ?? ""
|
|
293
319
|
};
|
|
294
|
-
}
|
|
295
|
-
|
|
320
|
+
};
|
|
321
|
+
const buildAriaProps = (isTop, isModal, isComposable, ariaLabel, panelId, hasDescription) => {
|
|
296
322
|
if (!isTop) return {};
|
|
297
323
|
const props = { role: "dialog" };
|
|
298
324
|
if (isModal) props["aria-modal"] = "true";
|
|
@@ -301,8 +327,8 @@ function buildAriaProps(isTop, isModal, isComposable, ariaLabel, panelId, hasDes
|
|
|
301
327
|
if (hasDescription) props["aria-describedby"] = `${panelId}-desc`;
|
|
302
328
|
} else props["aria-label"] = ariaLabel;
|
|
303
329
|
return props;
|
|
304
|
-
}
|
|
305
|
-
|
|
330
|
+
};
|
|
331
|
+
const getDragTransform = (side, offset) => {
|
|
306
332
|
if (offset === 0) return {};
|
|
307
333
|
switch (side) {
|
|
308
334
|
case "right": return { x: offset };
|
|
@@ -310,57 +336,58 @@ function getDragTransform(side, offset) {
|
|
|
310
336
|
case "bottom": return { y: offset };
|
|
311
337
|
default: return {};
|
|
312
338
|
}
|
|
313
|
-
}
|
|
339
|
+
};
|
|
314
340
|
const VISUAL_TWEEN = {
|
|
315
|
-
type: "tween",
|
|
316
341
|
duration: .25,
|
|
317
|
-
ease: "easeOut"
|
|
342
|
+
ease: "easeOut",
|
|
343
|
+
type: "tween"
|
|
318
344
|
};
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
|
|
345
|
+
const SHADOW_SM = "0px 1px 3px 0px rgba(0,0,0,0.06), 0px 6px 12px 0px rgba(0,0,0,0.06)";
|
|
346
|
+
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)";
|
|
347
|
+
const getShadow = (isNested) => isNested ? SHADOW_SM : SHADOW_LG;
|
|
348
|
+
const buildPanelStyle = (panelStyles, isTop, hasPanelClass, isDragging) => ({
|
|
349
|
+
...panelStyles,
|
|
350
|
+
pointerEvents: isTop ? "auto" : "none",
|
|
351
|
+
...isTop ? {} : { contain: "layout style paint" },
|
|
352
|
+
...isDragging ? { transition: "none" } : {},
|
|
353
|
+
...hasPanelClass ? {} : {
|
|
354
|
+
background: "var(--background, #fff)",
|
|
355
|
+
borderColor: "var(--border, transparent)"
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
const buildPanelTransition = (isDragging, isTop, spring, stackSpring) => {
|
|
332
359
|
if (isDragging) return {
|
|
333
|
-
|
|
334
|
-
|
|
360
|
+
duration: 0,
|
|
361
|
+
type: "tween"
|
|
335
362
|
};
|
|
336
363
|
return {
|
|
337
364
|
...isTop ? spring : stackSpring,
|
|
338
365
|
borderRadius: VISUAL_TWEEN,
|
|
339
366
|
boxShadow: VISUAL_TWEEN
|
|
340
367
|
};
|
|
341
|
-
}
|
|
342
|
-
|
|
368
|
+
};
|
|
369
|
+
const computeSnapYOffset = (side, snapHeights, activeSnapIndex, measuredHeight) => {
|
|
343
370
|
if (side !== "bottom" || snapHeights.length === 0 || measuredHeight <= 0) return 0;
|
|
344
371
|
return getSnapOffset(activeSnapIndex, snapHeights, measuredHeight);
|
|
345
|
-
}
|
|
346
|
-
|
|
372
|
+
};
|
|
373
|
+
const getBottomSlideDistance = (measuredHeight) => {
|
|
347
374
|
if (measuredHeight > 0) return measuredHeight;
|
|
348
375
|
if (typeof window !== "undefined") return window.innerHeight;
|
|
349
376
|
return 1e3;
|
|
350
|
-
}
|
|
351
|
-
|
|
377
|
+
};
|
|
378
|
+
const resolveSlideFrom = (side, slideFrom, measuredHeight) => {
|
|
352
379
|
if (side !== "bottom") return slideFrom;
|
|
353
380
|
return { y: getBottomSlideDistance(measuredHeight) };
|
|
354
|
-
}
|
|
355
|
-
|
|
381
|
+
};
|
|
382
|
+
const buildAnimateTarget = (slideTarget, stackOffset, dragOffset, transform, animatedRadius, transition, snapYOffset, isTop) => {
|
|
356
383
|
const base = {
|
|
357
384
|
...slideTarget,
|
|
358
385
|
...stackOffset,
|
|
359
386
|
...dragOffset,
|
|
360
|
-
scale: transform.scale,
|
|
361
|
-
opacity: transform.opacity,
|
|
362
387
|
...animatedRadius,
|
|
363
388
|
boxShadow: getShadow(!isTop),
|
|
389
|
+
opacity: transform.opacity,
|
|
390
|
+
scale: transform.scale,
|
|
364
391
|
transition
|
|
365
392
|
};
|
|
366
393
|
if (snapYOffset > 0) return {
|
|
@@ -368,23 +395,18 @@ function buildAnimateTarget(slideTarget, stackOffset, dragOffset, transform, ani
|
|
|
368
395
|
y: (dragOffset.y ?? 0) + snapYOffset
|
|
369
396
|
};
|
|
370
397
|
return base;
|
|
371
|
-
}
|
|
372
|
-
|
|
398
|
+
};
|
|
399
|
+
const getInitialRadius = (side) => {
|
|
373
400
|
if (side === "bottom") return {
|
|
374
|
-
borderTopLeftRadius: 0,
|
|
375
|
-
borderTopRightRadius: 0,
|
|
376
401
|
borderBottomLeftRadius: 0,
|
|
377
|
-
borderBottomRightRadius: 0
|
|
402
|
+
borderBottomRightRadius: 0,
|
|
403
|
+
borderTopLeftRadius: 0,
|
|
404
|
+
borderTopRightRadius: 0
|
|
378
405
|
};
|
|
379
406
|
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
|
-
}
|
|
407
|
+
};
|
|
386
408
|
//#endregion
|
|
387
|
-
//#region src/Drag/
|
|
409
|
+
//#region src/Drag/drag-constants.ts
|
|
388
410
|
/** Elements that should never initiate a drag */
|
|
389
411
|
const INTERACTIVE_TAGS = new Set([
|
|
390
412
|
"INPUT",
|
|
@@ -396,19 +418,19 @@ const INTERACTIVE_TAGS = new Set([
|
|
|
396
418
|
/** Rubber-band resistance factor for dragging past resting position */
|
|
397
419
|
const RUBBER_BAND_FACTOR = .6;
|
|
398
420
|
//#endregion
|
|
399
|
-
//#region src/Drag/
|
|
400
|
-
|
|
421
|
+
//#region src/Drag/drag-geometry.ts
|
|
422
|
+
const isInteractiveElement = (el) => {
|
|
401
423
|
if (INTERACTIVE_TAGS.has(el.tagName)) return true;
|
|
402
|
-
if (el.isContentEditable) return true;
|
|
424
|
+
if (el instanceof HTMLElement && el.isContentEditable) return true;
|
|
403
425
|
if (el.closest("button, a, input, textarea, select, [contenteditable]")) return true;
|
|
404
426
|
if (el.closest("[data-stacksheet-no-drag]")) return true;
|
|
405
427
|
return false;
|
|
406
|
-
}
|
|
428
|
+
};
|
|
407
429
|
/**
|
|
408
430
|
* Walk up from `el` to find the nearest scrollable ancestor.
|
|
409
431
|
* Returns null if nothing is scrollable in the dismiss axis.
|
|
410
432
|
*/
|
|
411
|
-
|
|
433
|
+
const findScrollableAncestor = (el, axis) => {
|
|
412
434
|
let current = el;
|
|
413
435
|
while (current) {
|
|
414
436
|
if (current instanceof HTMLElement) {
|
|
@@ -421,23 +443,23 @@ function findScrollableAncestor(el, axis) {
|
|
|
421
443
|
current = current.parentElement;
|
|
422
444
|
}
|
|
423
445
|
return null;
|
|
424
|
-
}
|
|
446
|
+
};
|
|
425
447
|
/**
|
|
426
448
|
* Check if a scrollable element is at its edge in the dismiss direction.
|
|
427
449
|
* For bottom sheets (sign=1, axis=y), "at edge" means scrolled to top.
|
|
428
450
|
* For left panels (sign=-1, axis=x), "at edge" means scrolled to right end.
|
|
429
451
|
*/
|
|
430
|
-
|
|
452
|
+
const isAtScrollEdge = (el, axis, sign) => {
|
|
431
453
|
if (axis === "y") return sign === 1 ? el.scrollTop <= 0 : el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
|
|
432
454
|
return sign === 1 ? el.scrollLeft <= 0 : el.scrollLeft + el.clientWidth >= el.scrollWidth - 1;
|
|
433
|
-
}
|
|
455
|
+
};
|
|
434
456
|
/**
|
|
435
457
|
* Get the dismiss direction axis and sign for a given side.
|
|
436
458
|
* - right panel → dismiss by dragging right (+x)
|
|
437
459
|
* - left panel → dismiss by dragging left (-x)
|
|
438
460
|
* - bottom panel → dismiss by dragging down (+y)
|
|
439
461
|
*/
|
|
440
|
-
|
|
462
|
+
const getDismissAxis = (side) => {
|
|
441
463
|
switch (side) {
|
|
442
464
|
case "right": return {
|
|
443
465
|
axis: "x",
|
|
@@ -456,13 +478,13 @@ function getDismissAxis(side) {
|
|
|
456
478
|
sign: 1
|
|
457
479
|
};
|
|
458
480
|
}
|
|
459
|
-
}
|
|
481
|
+
};
|
|
460
482
|
/**
|
|
461
483
|
* Decide whether a gesture past the dead zone qualifies as a dismiss drag.
|
|
462
484
|
* Returns "drag" if it's a valid dismiss gesture, "none" if it's off-axis
|
|
463
485
|
* or moving in the wrong direction.
|
|
464
486
|
*/
|
|
465
|
-
|
|
487
|
+
const classifyGesture = (dx, dy, axis, sign) => {
|
|
466
488
|
const absDx = Math.abs(dx);
|
|
467
489
|
const absDy = Math.abs(dy);
|
|
468
490
|
let angleDeg;
|
|
@@ -471,103 +493,77 @@ function classifyGesture(dx, dy, axis, sign) {
|
|
|
471
493
|
if (angleDeg > 35) return "none";
|
|
472
494
|
if ((axis === "x" ? dx : dy) * sign < 0) return "none";
|
|
473
495
|
return "drag";
|
|
474
|
-
}
|
|
496
|
+
};
|
|
475
497
|
/** Decide whether a pointer gesture commits as a drag or should be ignored. */
|
|
476
|
-
|
|
498
|
+
const commitGesture = (dx, dy, axis, sign, scrollEl) => {
|
|
477
499
|
if (classifyGesture(dx, dy, axis, sign) === "none") return "none";
|
|
478
|
-
if (scrollEl && !isAtScrollEdge(scrollEl, axis, sign)) return "none";
|
|
500
|
+
if (scrollEl !== null && !isAtScrollEdge(scrollEl, axis, sign)) return "none";
|
|
479
501
|
return "drag";
|
|
480
|
-
}
|
|
481
|
-
|
|
502
|
+
};
|
|
503
|
+
const getPanelDimension = (panel, axis) => {
|
|
482
504
|
if (!panel) return 300;
|
|
483
505
|
return axis === "x" ? panel.offsetWidth : panel.offsetHeight;
|
|
484
|
-
}
|
|
506
|
+
};
|
|
485
507
|
//#endregion
|
|
486
|
-
//#region src/Drag/
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
*/
|
|
503
|
-
function useDrag(panelRef, config, onDragUpdate) {
|
|
504
|
-
const startRef = useRef(null);
|
|
505
|
-
const committedRef = useRef(null);
|
|
506
|
-
const offsetRef = useRef(0);
|
|
507
|
-
const scrollTargetRef = useRef(null);
|
|
508
|
-
const { axis, sign } = getDismissAxis(config.side);
|
|
509
|
-
const handlePointerDown = useCallback((e) => {
|
|
510
|
-
if (!config.enabled) return;
|
|
511
|
-
if (e.button !== 0) return;
|
|
512
|
-
const target = e.target;
|
|
513
|
-
if (!target) return;
|
|
514
|
-
const isHandle = !!target.closest("[data-stacksheet-handle]");
|
|
508
|
+
//#region src/Drag/use-drag.ts
|
|
509
|
+
const resetDragRefs = ({ committedRef, offsetRef, scrollTargetRef, startRef }) => {
|
|
510
|
+
startRef.current = null;
|
|
511
|
+
committedRef.current = null;
|
|
512
|
+
offsetRef.current = 0;
|
|
513
|
+
scrollTargetRef.current = null;
|
|
514
|
+
};
|
|
515
|
+
const createDragHandlers = ({ axis, config, onDragUpdate, panelRef, refs, sign }) => {
|
|
516
|
+
const dismiss = () => {
|
|
517
|
+
if (config.isNested) config.onPop();
|
|
518
|
+
else config.onClose();
|
|
519
|
+
};
|
|
520
|
+
const handlePointerDown = (e) => {
|
|
521
|
+
if (!config.enabled || e.button !== 0 || !(e.target instanceof Element)) return;
|
|
522
|
+
const { target } = e;
|
|
523
|
+
const isHandle = target.closest("[data-stacksheet-handle]") !== null;
|
|
515
524
|
if (!isHandle && isInteractiveElement(target)) return;
|
|
516
|
-
scrollTargetRef.current = isHandle ? null : findScrollableAncestor(target, axis);
|
|
517
|
-
startRef.current = {
|
|
525
|
+
refs.scrollTargetRef.current = isHandle ? null : findScrollableAncestor(target, axis);
|
|
526
|
+
refs.startRef.current = {
|
|
527
|
+
time: Date.now(),
|
|
518
528
|
x: e.clientX,
|
|
519
|
-
y: e.clientY
|
|
520
|
-
time: Date.now()
|
|
529
|
+
y: e.clientY
|
|
521
530
|
};
|
|
522
|
-
committedRef.current = null;
|
|
523
|
-
offsetRef.current = 0;
|
|
524
|
-
e.currentTarget
|
|
525
|
-
}
|
|
526
|
-
const handlePointerMove =
|
|
527
|
-
if (
|
|
528
|
-
const dx = e.clientX - startRef.current.x;
|
|
529
|
-
const dy = e.clientY - startRef.current.y;
|
|
530
|
-
const dist = Math.
|
|
531
|
-
if (committedRef.current === null && dist < 10) return;
|
|
532
|
-
if (committedRef.current === null) {
|
|
533
|
-
committedRef.current = commitGesture(dx, dy, axis, sign, scrollTargetRef.current);
|
|
534
|
-
if (committedRef.current !== "drag") {
|
|
535
|
-
startRef.current = null;
|
|
531
|
+
refs.committedRef.current = null;
|
|
532
|
+
refs.offsetRef.current = 0;
|
|
533
|
+
if (e.currentTarget instanceof HTMLElement) e.currentTarget.setPointerCapture(e.pointerId);
|
|
534
|
+
};
|
|
535
|
+
const handlePointerMove = (e) => {
|
|
536
|
+
if (refs.startRef.current === null) return;
|
|
537
|
+
const dx = e.clientX - refs.startRef.current.x;
|
|
538
|
+
const dy = e.clientY - refs.startRef.current.y;
|
|
539
|
+
const dist = Math.hypot(dx, dy);
|
|
540
|
+
if (refs.committedRef.current === null && dist < 10) return;
|
|
541
|
+
if (refs.committedRef.current === null) {
|
|
542
|
+
refs.committedRef.current = commitGesture(dx, dy, axis, sign, refs.scrollTargetRef.current);
|
|
543
|
+
if (refs.committedRef.current !== "drag") {
|
|
544
|
+
refs.startRef.current = null;
|
|
536
545
|
return;
|
|
537
546
|
}
|
|
538
547
|
}
|
|
539
|
-
if (committedRef.current !== "drag") return;
|
|
548
|
+
if (refs.committedRef.current !== "drag") return;
|
|
540
549
|
const directional = (axis === "x" ? dx : dy) * sign;
|
|
541
550
|
const clampedOffset = directional >= 0 ? directional : -Math.sqrt(Math.abs(directional)) * RUBBER_BAND_FACTOR;
|
|
542
|
-
offsetRef.current = clampedOffset;
|
|
551
|
+
refs.offsetRef.current = clampedOffset;
|
|
543
552
|
onDragUpdate({
|
|
544
|
-
|
|
545
|
-
|
|
553
|
+
isDragging: true,
|
|
554
|
+
offset: clampedOffset
|
|
546
555
|
});
|
|
547
556
|
e.preventDefault();
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
]);
|
|
553
|
-
const dismiss = useCallback(() => {
|
|
554
|
-
if (config.isNested) config.onPop();
|
|
555
|
-
else config.onClose();
|
|
556
|
-
}, [config]);
|
|
557
|
-
const handlePointerUp = useCallback((_e) => {
|
|
558
|
-
if (!startRef.current || committedRef.current !== "drag") {
|
|
559
|
-
startRef.current = null;
|
|
560
|
-
committedRef.current = null;
|
|
561
|
-
scrollTargetRef.current = null;
|
|
557
|
+
};
|
|
558
|
+
const handlePointerUp = () => {
|
|
559
|
+
if (refs.startRef.current === null || refs.committedRef.current !== "drag") {
|
|
560
|
+
resetDragRefs(refs);
|
|
562
561
|
return;
|
|
563
562
|
}
|
|
564
|
-
const offset = Math.max(0, offsetRef.current);
|
|
565
|
-
const elapsed = Date.now() - startRef.current.time;
|
|
563
|
+
const offset = Math.max(0, refs.offsetRef.current);
|
|
564
|
+
const elapsed = Date.now() - refs.startRef.current.time;
|
|
566
565
|
const velocity = elapsed > 0 ? offset / elapsed : 0;
|
|
567
|
-
|
|
568
|
-
committedRef.current = null;
|
|
569
|
-
offsetRef.current = 0;
|
|
570
|
-
scrollTargetRef.current = null;
|
|
566
|
+
resetDragRefs(refs);
|
|
571
567
|
const panelSize = getPanelDimension(panelRef.current, axis);
|
|
572
568
|
if (config.snapHeights.length > 0) {
|
|
573
569
|
const targetIndex = findSnapTarget(offset, panelSize, config.snapHeights, velocity, config.activeSnapIndex, config.sequential);
|
|
@@ -575,8 +571,8 @@ function useDrag(panelRef, config, onDragUpdate) {
|
|
|
575
571
|
else {
|
|
576
572
|
config.onSnap(targetIndex);
|
|
577
573
|
onDragUpdate({
|
|
578
|
-
|
|
579
|
-
|
|
574
|
+
isDragging: false,
|
|
575
|
+
offset: 0
|
|
580
576
|
});
|
|
581
577
|
}
|
|
582
578
|
return;
|
|
@@ -585,26 +581,59 @@ function useDrag(panelRef, config, onDragUpdate) {
|
|
|
585
581
|
const fastEnough = velocity > config.velocityThreshold;
|
|
586
582
|
if (pastThreshold || fastEnough) dismiss();
|
|
587
583
|
else onDragUpdate({
|
|
588
|
-
|
|
589
|
-
|
|
584
|
+
isDragging: false,
|
|
585
|
+
offset: 0
|
|
590
586
|
});
|
|
591
|
-
}
|
|
592
|
-
|
|
587
|
+
};
|
|
588
|
+
const handlePointerCancel = () => {
|
|
589
|
+
resetDragRefs(refs);
|
|
590
|
+
onDragUpdate({
|
|
591
|
+
isDragging: false,
|
|
592
|
+
offset: 0
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
return {
|
|
596
|
+
handlePointerCancel,
|
|
597
|
+
handlePointerDown,
|
|
598
|
+
handlePointerMove,
|
|
599
|
+
handlePointerUp
|
|
600
|
+
};
|
|
601
|
+
};
|
|
602
|
+
/**
|
|
603
|
+
* Hook that manages drag gestures that can dismiss a sheet panel.
|
|
604
|
+
*
|
|
605
|
+
* Gesture pipeline:
|
|
606
|
+
* 1. Dead zone (10px) — ignores micro-movements
|
|
607
|
+
* 2. Angle check (35°) — must be roughly aligned with dismiss axis
|
|
608
|
+
* 3. Scroll conflict — yields to scrollable containers not at edge
|
|
609
|
+
* 4. Commit — drag is active, applies offset via `onDragUpdate`
|
|
610
|
+
* 5. Release — velocity + threshold determine close/snap/bounce-back
|
|
611
|
+
*
|
|
612
|
+
* Opposite-direction drag uses √(offset) damping for elastic
|
|
613
|
+
* rubber-band resistance (same physics as iOS over-scroll).
|
|
614
|
+
*
|
|
615
|
+
* When `snapHeights` is provided, release targeting uses
|
|
616
|
+
* `findSnapTarget()` instead of the simple threshold check.
|
|
617
|
+
*/
|
|
618
|
+
const useDrag = (panelRef, config, onDragUpdate) => {
|
|
619
|
+
const startRef = useRef(null);
|
|
620
|
+
const committedRef = useRef(null);
|
|
621
|
+
const offsetRef = useRef(0);
|
|
622
|
+
const scrollTargetRef = useRef(null);
|
|
623
|
+
const { axis, sign } = getDismissAxis(config.side);
|
|
624
|
+
const { handlePointerCancel, handlePointerDown, handlePointerMove, handlePointerUp } = createDragHandlers({
|
|
593
625
|
axis,
|
|
594
626
|
config,
|
|
595
627
|
onDragUpdate,
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
isDragging: false
|
|
606
|
-
});
|
|
607
|
-
}, [onDragUpdate]);
|
|
628
|
+
panelRef,
|
|
629
|
+
refs: {
|
|
630
|
+
committedRef,
|
|
631
|
+
offsetRef,
|
|
632
|
+
scrollTargetRef,
|
|
633
|
+
startRef
|
|
634
|
+
},
|
|
635
|
+
sign
|
|
636
|
+
});
|
|
608
637
|
const handlersRef = useRef({
|
|
609
638
|
handlePointerCancel,
|
|
610
639
|
handlePointerDown,
|
|
@@ -619,23 +648,35 @@ function useDrag(panelRef, config, onDragUpdate) {
|
|
|
619
648
|
};
|
|
620
649
|
useEffect(() => {
|
|
621
650
|
const el = panelRef.current;
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
651
|
+
const onPointerDown = (event) => {
|
|
652
|
+
handlersRef.current.handlePointerDown(event);
|
|
653
|
+
};
|
|
654
|
+
const onPointerMove = (event) => {
|
|
655
|
+
handlersRef.current.handlePointerMove(event);
|
|
656
|
+
};
|
|
657
|
+
const onPointerUp = () => {
|
|
658
|
+
handlersRef.current.handlePointerUp();
|
|
659
|
+
};
|
|
660
|
+
const onPointerCancel = () => {
|
|
661
|
+
handlersRef.current.handlePointerCancel();
|
|
662
|
+
};
|
|
663
|
+
const canListen = el !== null && config.enabled;
|
|
664
|
+
if (canListen) {
|
|
665
|
+
el.addEventListener("pointerdown", onPointerDown);
|
|
666
|
+
el.addEventListener("pointermove", onPointerMove);
|
|
667
|
+
el.addEventListener("pointerup", onPointerUp);
|
|
668
|
+
el.addEventListener("pointercancel", onPointerCancel);
|
|
669
|
+
}
|
|
631
670
|
return () => {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
671
|
+
if (canListen) {
|
|
672
|
+
el.removeEventListener("pointerdown", onPointerDown);
|
|
673
|
+
el.removeEventListener("pointermove", onPointerMove);
|
|
674
|
+
el.removeEventListener("pointerup", onPointerUp);
|
|
675
|
+
el.removeEventListener("pointercancel", onPointerCancel);
|
|
676
|
+
}
|
|
636
677
|
};
|
|
637
678
|
}, [panelRef, config.enabled]);
|
|
638
|
-
}
|
|
679
|
+
};
|
|
639
680
|
//#endregion
|
|
640
681
|
//#region src/panel-context.tsx
|
|
641
682
|
const SheetPanelContext = createContext(null);
|
|
@@ -643,11 +684,11 @@ const SheetPanelContext = createContext(null);
|
|
|
643
684
|
* Access the current sheet panel's context.
|
|
644
685
|
* Must be called inside a component rendered by the sheet stack.
|
|
645
686
|
*/
|
|
646
|
-
|
|
647
|
-
const ctx =
|
|
687
|
+
const useSheetPanel = () => {
|
|
688
|
+
const ctx = use(SheetPanelContext);
|
|
648
689
|
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
690
|
return ctx;
|
|
650
|
-
}
|
|
691
|
+
};
|
|
651
692
|
//#endregion
|
|
652
693
|
//#region src/stacking.ts
|
|
653
694
|
/**
|
|
@@ -655,58 +696,56 @@ function useSheetPanel() {
|
|
|
655
696
|
* depth=0 is the top (foreground) panel.
|
|
656
697
|
* Panels beyond renderThreshold are clamped to the edge position and faded out.
|
|
657
698
|
*/
|
|
658
|
-
|
|
699
|
+
const getStackTransform = (depth, stacking) => {
|
|
659
700
|
if (depth <= 0) return {
|
|
660
|
-
|
|
701
|
+
borderRadius: 0,
|
|
661
702
|
offset: 0,
|
|
662
703
|
opacity: 1,
|
|
663
|
-
|
|
704
|
+
scale: 1
|
|
664
705
|
};
|
|
665
706
|
const beyondThreshold = depth >= stacking.renderThreshold;
|
|
666
707
|
const visualDepth = beyondThreshold ? stacking.renderThreshold - 1 : depth;
|
|
667
708
|
return {
|
|
668
|
-
|
|
709
|
+
borderRadius: stacking.radius,
|
|
669
710
|
offset: visualDepth * stacking.offsetStep,
|
|
670
711
|
opacity: beyondThreshold ? 0 : Math.max(0, 1 - visualDepth * stacking.opacityStep),
|
|
671
|
-
|
|
712
|
+
scale: Math.max(.5, 1 - visualDepth * stacking.scaleStep)
|
|
672
713
|
};
|
|
673
|
-
}
|
|
714
|
+
};
|
|
674
715
|
/**
|
|
675
716
|
* Border radius values for the animate target.
|
|
676
717
|
* Must be animated (not static CSS) so Motion applies scale correction
|
|
677
718
|
* when panels are scaled. See: https://motion.dev/docs/react-layout-animations#scale-correction
|
|
678
719
|
*/
|
|
679
|
-
|
|
720
|
+
const getAnimatedBorderRadius = (side, depth, stacking) => {
|
|
680
721
|
if (side === "bottom") {
|
|
681
722
|
const radius = depth > 0 ? stacking.radius : 16;
|
|
682
723
|
return {
|
|
683
|
-
borderTopLeftRadius: radius,
|
|
684
|
-
borderTopRightRadius: radius,
|
|
685
724
|
borderBottomLeftRadius: 0,
|
|
686
|
-
borderBottomRightRadius: 0
|
|
725
|
+
borderBottomRightRadius: 0,
|
|
726
|
+
borderTopLeftRadius: radius,
|
|
727
|
+
borderTopRightRadius: radius
|
|
687
728
|
};
|
|
688
729
|
}
|
|
689
730
|
if (depth > 0) return { borderRadius: stacking.radius };
|
|
690
731
|
return { borderRadius: 0 };
|
|
691
|
-
}
|
|
732
|
+
};
|
|
692
733
|
/** Motion initial/exit values for sliding from the given side. */
|
|
693
|
-
|
|
734
|
+
const getSlideFrom = (side) => {
|
|
694
735
|
switch (side) {
|
|
695
736
|
case "right": return { x: "100%" };
|
|
696
737
|
case "left": return { x: "-100%" };
|
|
697
738
|
case "bottom": return { y: "100%" };
|
|
698
739
|
default: return { x: "100%" };
|
|
699
740
|
}
|
|
700
|
-
}
|
|
741
|
+
};
|
|
701
742
|
/** Motion animate target — the resting position. */
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
};
|
|
707
|
-
}
|
|
743
|
+
const getSlideTarget = () => ({
|
|
744
|
+
x: 0,
|
|
745
|
+
y: 0
|
|
746
|
+
});
|
|
708
747
|
/** Translate offset that pushes stacked panels away from the stack edge. */
|
|
709
|
-
|
|
748
|
+
const getStackOffset = (side, offset) => {
|
|
710
749
|
if (offset === 0) return {};
|
|
711
750
|
switch (side) {
|
|
712
751
|
case "right": return { x: -offset };
|
|
@@ -714,286 +753,319 @@ function getStackOffset(side, offset) {
|
|
|
714
753
|
case "bottom": return { y: -offset };
|
|
715
754
|
default: return {};
|
|
716
755
|
}
|
|
717
|
-
}
|
|
756
|
+
};
|
|
718
757
|
/** Opposite-side origin so stacked panels recede away from the stack edge. */
|
|
719
|
-
|
|
758
|
+
const getTransformOrigin = (side) => {
|
|
720
759
|
if (side === "right") return "left center";
|
|
721
760
|
if (side === "left") return "right center";
|
|
722
761
|
return "center top";
|
|
723
|
-
}
|
|
762
|
+
};
|
|
724
763
|
/**
|
|
725
764
|
* Fixed-position styles for a panel, accounting for side, width, and depth.
|
|
726
765
|
*/
|
|
727
|
-
|
|
766
|
+
const getPanelStyles = (side, config, index) => {
|
|
728
767
|
const { width, maxWidth, zIndex } = config;
|
|
729
768
|
const base = {
|
|
730
|
-
position: "fixed",
|
|
731
|
-
zIndex: zIndex + 10 + index,
|
|
732
769
|
display: "flex",
|
|
733
770
|
flexDirection: "column",
|
|
771
|
+
position: "fixed",
|
|
772
|
+
transformOrigin: getTransformOrigin(side),
|
|
734
773
|
willChange: "transform",
|
|
735
|
-
|
|
774
|
+
zIndex: zIndex + 10 + index
|
|
736
775
|
};
|
|
737
776
|
if (side === "bottom") return {
|
|
738
777
|
...base,
|
|
739
|
-
left: 0,
|
|
740
|
-
right: 0,
|
|
741
778
|
bottom: 0,
|
|
742
779
|
height: "85dvh",
|
|
743
|
-
|
|
780
|
+
left: 0,
|
|
781
|
+
maxHeight: "85dvh",
|
|
782
|
+
right: 0
|
|
744
783
|
};
|
|
745
784
|
const sideStyles = side === "right" ? {
|
|
746
|
-
|
|
785
|
+
bottom: 0,
|
|
747
786
|
right: 0,
|
|
748
|
-
|
|
787
|
+
top: 0
|
|
749
788
|
} : {
|
|
750
|
-
|
|
789
|
+
bottom: 0,
|
|
751
790
|
left: 0,
|
|
752
|
-
|
|
791
|
+
top: 0
|
|
753
792
|
};
|
|
754
793
|
return {
|
|
755
794
|
...base,
|
|
756
795
|
...sideStyles,
|
|
757
|
-
|
|
758
|
-
|
|
796
|
+
maxWidth,
|
|
797
|
+
width
|
|
759
798
|
};
|
|
760
|
-
}
|
|
799
|
+
};
|
|
761
800
|
//#endregion
|
|
762
801
|
//#region src/icons.tsx
|
|
763
802
|
/** 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
|
-
}
|
|
803
|
+
const ArrowLeftIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
804
|
+
"aria-hidden": "true",
|
|
805
|
+
fill: "none",
|
|
806
|
+
height: 16,
|
|
807
|
+
stroke: "currentColor",
|
|
808
|
+
strokeLinecap: "round",
|
|
809
|
+
strokeLinejoin: "round",
|
|
810
|
+
strokeWidth: 2,
|
|
811
|
+
viewBox: "0 0 24 24",
|
|
812
|
+
width: 16,
|
|
813
|
+
children: /* @__PURE__ */ jsx("path", { d: "M19 12H5M12 19l-7-7 7-7" })
|
|
814
|
+
});
|
|
815
|
+
const XIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
816
|
+
"aria-hidden": "true",
|
|
817
|
+
fill: "none",
|
|
818
|
+
height: 16,
|
|
819
|
+
stroke: "currentColor",
|
|
820
|
+
strokeLinecap: "round",
|
|
821
|
+
strokeLinejoin: "round",
|
|
822
|
+
strokeWidth: 2,
|
|
823
|
+
viewBox: "0 0 24 24",
|
|
824
|
+
width: 16,
|
|
825
|
+
children: /* @__PURE__ */ jsx("path", { d: "M18 6L6 18M6 6l12 12" })
|
|
826
|
+
});
|
|
792
827
|
//#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",
|
|
828
|
+
//#region src/SheetPanel/default-header.tsx
|
|
829
|
+
const DefaultHeader = ({ isNested, onBack, onClose, className }) => /* @__PURE__ */ jsxs("div", {
|
|
830
|
+
className: `flex shrink-0 items-center justify-between px-4 pt-4 pb-2 ${className ?? ""}`,
|
|
831
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
832
|
+
className: "flex items-center gap-2",
|
|
833
|
+
children: isNested && /* @__PURE__ */ jsx("button", {
|
|
834
|
+
"aria-label": "Back",
|
|
808
835
|
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:
|
|
836
|
+
onClick: onBack,
|
|
810
837
|
type: "button",
|
|
811
|
-
children: /* @__PURE__ */ jsx(
|
|
812
|
-
})
|
|
813
|
-
})
|
|
814
|
-
|
|
838
|
+
children: /* @__PURE__ */ jsx(ArrowLeftIcon, {})
|
|
839
|
+
})
|
|
840
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
841
|
+
"aria-label": "Close",
|
|
842
|
+
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",
|
|
843
|
+
onClick: onClose,
|
|
844
|
+
type: "button",
|
|
845
|
+
children: /* @__PURE__ */ jsx(XIcon, {})
|
|
846
|
+
})]
|
|
847
|
+
});
|
|
815
848
|
//#endregion
|
|
816
|
-
//#region src/SheetPanel/
|
|
817
|
-
const PanelInnerContent =
|
|
818
|
-
if (isComposable) return shouldRender && Content ? /* @__PURE__ */ jsx(Content, { ...data }) : null;
|
|
819
|
-
|
|
849
|
+
//#region src/SheetPanel/sheet-panel-content.tsx
|
|
850
|
+
const PanelInnerContent = ({ isComposable, shouldRender, Content, data, renderHeader, headerProps, headerClassName }) => {
|
|
851
|
+
if (isComposable) return shouldRender && Content !== void 0 ? /* @__PURE__ */ jsx(Content, { ...data }) : null;
|
|
852
|
+
const customHeader = renderHeader !== void 0 && renderHeader !== false ? renderHeader : void 0;
|
|
853
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [customHeader === void 0 ? /* @__PURE__ */ jsx(DefaultHeader, {
|
|
820
854
|
...headerProps,
|
|
821
855
|
className: headerClassName
|
|
822
|
-
}), shouldRender && Content && /* @__PURE__ */ jsx("div", {
|
|
856
|
+
}) : customHeader(headerProps), shouldRender && Content !== void 0 && /* @__PURE__ */ jsx("div", {
|
|
823
857
|
className: "min-h-0 flex-1 overflow-y-auto overscroll-contain",
|
|
824
858
|
"data-stacksheet-no-drag": "",
|
|
825
859
|
children: /* @__PURE__ */ jsx(Content, { ...data })
|
|
826
860
|
})] });
|
|
827
|
-
}
|
|
861
|
+
};
|
|
828
862
|
PanelInnerContent.displayName = "PanelInnerContent";
|
|
829
863
|
//#endregion
|
|
830
|
-
//#region src/SheetPanel/
|
|
864
|
+
//#region src/SheetPanel/sheet-panel-focus.tsx
|
|
831
865
|
const LAYERED_MODAL_SELECTORS = [
|
|
832
866
|
"[role=\"dialog\"][data-state=\"open\"]",
|
|
833
867
|
"[role=\"alertdialog\"][data-state=\"open\"]",
|
|
834
868
|
"[data-radix-popper-content-wrapper]",
|
|
835
869
|
"[data-radix-focus-guard]"
|
|
836
870
|
].join(", ");
|
|
837
|
-
|
|
871
|
+
const subscribeToFocusTarget = (onStoreChange) => {
|
|
872
|
+
document.addEventListener("focusin", onStoreChange, true);
|
|
873
|
+
return () => {
|
|
874
|
+
document.removeEventListener("focusin", onStoreChange, true);
|
|
875
|
+
};
|
|
876
|
+
};
|
|
877
|
+
const getServerLayeredModalFocused = () => false;
|
|
878
|
+
const getLayeredModalFocused = () => {
|
|
879
|
+
if (typeof document === "undefined") return false;
|
|
880
|
+
const target = document.activeElement;
|
|
881
|
+
return !!target && target !== document.body && target instanceof Element && target.closest(LAYERED_MODAL_SELECTORS) !== null;
|
|
882
|
+
};
|
|
883
|
+
const useLayeredModalFocused = (active) => {
|
|
884
|
+
const layered = useSyncExternalStore(subscribeToFocusTarget, getLayeredModalFocused, getServerLayeredModalFocused);
|
|
885
|
+
return active && layered;
|
|
886
|
+
};
|
|
887
|
+
const ModalFocusTrap = ({ enabled, active, fallbackRef, children }) => {
|
|
838
888
|
const paused = useLayeredModalFocused(enabled && active);
|
|
839
889
|
if (!enabled) return children;
|
|
840
890
|
return /* @__PURE__ */ jsx(FocusTrap, {
|
|
841
891
|
active,
|
|
842
892
|
focusTrapOptions: {
|
|
843
|
-
initialFocus: false,
|
|
844
|
-
returnFocusOnDeactivate: true,
|
|
845
|
-
escapeDeactivates: false,
|
|
846
893
|
allowOutsideClick: true,
|
|
847
|
-
|
|
894
|
+
escapeDeactivates: false,
|
|
848
895
|
fallbackFocus: () => {
|
|
849
|
-
if (fallbackRef.current) return fallbackRef.current;
|
|
896
|
+
if (fallbackRef.current !== null) return fallbackRef.current;
|
|
850
897
|
return document.body;
|
|
851
|
-
}
|
|
898
|
+
},
|
|
899
|
+
initialFocus: false,
|
|
900
|
+
returnFocusOnDeactivate: true
|
|
852
901
|
},
|
|
853
902
|
paused,
|
|
854
903
|
children
|
|
855
904
|
});
|
|
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
|
-
}
|
|
905
|
+
};
|
|
873
906
|
//#endregion
|
|
874
|
-
//#region src/SheetPanel/
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
}
|
|
888
|
-
function SideHandle({ side, isHovered, onDismiss }) {
|
|
907
|
+
//#region src/SheetPanel/sheet-panel-handles.tsx
|
|
908
|
+
const BottomHandle = ({ onDismiss }) => /* @__PURE__ */ jsx("button", {
|
|
909
|
+
"aria-label": "Dismiss",
|
|
910
|
+
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",
|
|
911
|
+
"data-stacksheet-handle": "",
|
|
912
|
+
onClick: onDismiss,
|
|
913
|
+
type: "button",
|
|
914
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
915
|
+
"aria-hidden": "true",
|
|
916
|
+
className: "h-[5px] w-9 rounded-full bg-current/15"
|
|
917
|
+
})
|
|
918
|
+
});
|
|
919
|
+
const SideHandle = ({ side, isHovered, onDismiss }) => {
|
|
889
920
|
const position = side === "right" ? { right: "100%" } : { left: "100%" };
|
|
890
|
-
return /* @__PURE__ */ jsx(m.
|
|
921
|
+
return /* @__PURE__ */ jsx(m.button, {
|
|
891
922
|
animate: { opacity: isHovered ? 1 : 0 },
|
|
892
923
|
"aria-label": "Dismiss",
|
|
893
|
-
className: "absolute top-0 bottom-0 flex w-6 cursor-grab touch-none items-center justify-center",
|
|
924
|
+
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
925
|
"data-stacksheet-handle": "",
|
|
895
926
|
onClick: onDismiss,
|
|
896
|
-
onKeyDown: (e) => {
|
|
897
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
898
|
-
e.preventDefault();
|
|
899
|
-
onDismiss?.();
|
|
900
|
-
}
|
|
901
|
-
},
|
|
902
|
-
role: "button",
|
|
903
927
|
style: position,
|
|
904
|
-
tabIndex: 0,
|
|
905
928
|
transition: {
|
|
906
929
|
duration: isHovered ? .15 : .4,
|
|
907
930
|
ease: "easeOut"
|
|
908
931
|
},
|
|
932
|
+
type: "button",
|
|
909
933
|
children: /* @__PURE__ */ jsx("div", {
|
|
910
934
|
"aria-hidden": "true",
|
|
911
935
|
className: "h-8 w-1 rounded-full bg-current/20"
|
|
912
936
|
})
|
|
913
937
|
});
|
|
914
|
-
}
|
|
938
|
+
};
|
|
915
939
|
//#endregion
|
|
916
|
-
//#region src/SheetPanel/
|
|
917
|
-
|
|
940
|
+
//#region src/SheetPanel/sheet-panel-layout.ts
|
|
941
|
+
const resolvePanelLayout = (layout, renderHeader) => {
|
|
918
942
|
if (layout) return layout;
|
|
919
943
|
return renderHeader === false ? "composable" : "classic";
|
|
920
|
-
}
|
|
944
|
+
};
|
|
921
945
|
//#endregion
|
|
922
|
-
//#region src/SheetPanel/
|
|
923
|
-
const
|
|
924
|
-
|
|
946
|
+
//#region src/SheetPanel/sheet-panel.tsx
|
|
947
|
+
const getPanelAriaLabel = (item, fallbackLabel) => item.ariaLabel ?? (typeof item.data?.__ariaLabel === "string" ? item.data.__ariaLabel : void 0) ?? fallbackLabel;
|
|
948
|
+
const getPanelHoverProps = (enabled, setIsHovered) => enabled ? {
|
|
949
|
+
onBlur: () => {
|
|
950
|
+
setIsHovered(false);
|
|
951
|
+
},
|
|
952
|
+
onFocus: () => {
|
|
953
|
+
setIsHovered(true);
|
|
954
|
+
},
|
|
955
|
+
onMouseEnter: () => {
|
|
956
|
+
setIsHovered(true);
|
|
957
|
+
},
|
|
958
|
+
onMouseLeave: () => {
|
|
959
|
+
setIsHovered(false);
|
|
960
|
+
}
|
|
961
|
+
} : {};
|
|
962
|
+
const getInactivePanelProps = (isTop) => isTop ? {} : {
|
|
963
|
+
"aria-hidden": "true",
|
|
964
|
+
inert: true
|
|
965
|
+
};
|
|
966
|
+
const getHeaderProps = ({ close, isNested, pop, side }) => ({
|
|
967
|
+
isNested,
|
|
968
|
+
onBack: pop,
|
|
969
|
+
onClose: close,
|
|
970
|
+
side
|
|
971
|
+
});
|
|
972
|
+
const getPanelContext = ({ close, hasDescription, isNested, isTop, panelId, pop, registerDescription, side }) => ({
|
|
973
|
+
back: pop,
|
|
974
|
+
close,
|
|
975
|
+
hasDescription,
|
|
976
|
+
isNested,
|
|
977
|
+
isTop,
|
|
978
|
+
panelId,
|
|
979
|
+
registerDescription,
|
|
980
|
+
side
|
|
981
|
+
});
|
|
982
|
+
const getOptionalSideHandle = ({ isHovered, onDismiss, show, side }) => show ? /* @__PURE__ */ jsx(SideHandle, {
|
|
983
|
+
isHovered,
|
|
984
|
+
onDismiss,
|
|
985
|
+
side
|
|
986
|
+
}) : null;
|
|
987
|
+
const getOptionalBottomHandle = (show, onDismiss) => show ? /* @__PURE__ */ jsx(BottomHandle, { onDismiss }) : null;
|
|
988
|
+
const completeOpeningAnimation = (hasEnteredRef, isTop, onOpenCompleteRef) => {
|
|
989
|
+
if (isTop && !hasEnteredRef.current) {
|
|
990
|
+
hasEnteredRef.current = true;
|
|
991
|
+
onOpenCompleteRef.current?.();
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
const useOpeningCompletion = (isTop, onOpenComplete) => {
|
|
925
995
|
const hasEnteredRef = useRef(false);
|
|
996
|
+
const onOpenCompleteRef = useRef(onOpenComplete);
|
|
997
|
+
if (!isTop && hasEnteredRef.current) hasEnteredRef.current = false;
|
|
998
|
+
useEffect(() => {
|
|
999
|
+
onOpenCompleteRef.current = onOpenComplete;
|
|
1000
|
+
}, [onOpenComplete]);
|
|
1001
|
+
return () => {
|
|
1002
|
+
completeOpeningAnimation(hasEnteredRef, isTop, onOpenCompleteRef);
|
|
1003
|
+
};
|
|
1004
|
+
};
|
|
1005
|
+
const useSheetPanelDrag = ({ activeSnapIndex, config, isNested, isTop, onSnap, prefersReducedMotion, side, snapHeights, swipeClose, swipePop }, panelRef) => {
|
|
926
1006
|
const [dragState, setDragState] = useState({
|
|
927
|
-
|
|
928
|
-
|
|
1007
|
+
isDragging: false,
|
|
1008
|
+
offset: 0
|
|
929
1009
|
});
|
|
930
|
-
const [isHovered, setIsHovered] = useState(false);
|
|
931
|
-
const measuredHeight = usePanelHeight(panelRef, snapHeights.length > 0);
|
|
932
|
-
const transform = getStackTransform(depth, config.stacking);
|
|
933
|
-
const panelStyles = getPanelStyles(side, config, index);
|
|
934
|
-
useEffect(() => {
|
|
935
|
-
if (!isTop) hasEnteredRef.current = false;
|
|
936
|
-
}, [isTop]);
|
|
937
|
-
const handleAnimationComplete = useCallback(() => {
|
|
938
|
-
if (isTop && !hasEnteredRef.current) {
|
|
939
|
-
hasEnteredRef.current = true;
|
|
940
|
-
config.onOpenComplete?.();
|
|
941
|
-
}
|
|
942
|
-
}, [isTop, config]);
|
|
943
1010
|
useDrag(panelRef, {
|
|
944
|
-
|
|
1011
|
+
activeSnapIndex,
|
|
945
1012
|
closeThreshold: config.closeThreshold,
|
|
946
|
-
|
|
947
|
-
|
|
1013
|
+
enabled: isTop && config.drag && config.dismissible && !prefersReducedMotion,
|
|
1014
|
+
isNested,
|
|
948
1015
|
onClose: swipeClose,
|
|
949
1016
|
onPop: swipePop,
|
|
950
|
-
isNested,
|
|
951
|
-
snapHeights,
|
|
952
|
-
activeSnapIndex,
|
|
953
1017
|
onSnap,
|
|
954
|
-
sequential: config.snapToSequentialPoints
|
|
1018
|
+
sequential: config.snapToSequentialPoints,
|
|
1019
|
+
side,
|
|
1020
|
+
snapHeights,
|
|
1021
|
+
velocityThreshold: config.velocityThreshold
|
|
955
1022
|
}, setDragState);
|
|
956
|
-
|
|
957
|
-
|
|
1023
|
+
return dragState;
|
|
1024
|
+
};
|
|
1025
|
+
const useSheetPanelContext = ({ close, isNested, isTop, pop, side }, panelId) => {
|
|
958
1026
|
const [hasDescription, setHasDescription] = useState(false);
|
|
959
|
-
const registerDescription =
|
|
1027
|
+
const registerDescription = () => {
|
|
960
1028
|
setHasDescription(true);
|
|
961
|
-
return () =>
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
isNested,
|
|
967
|
-
isTop,
|
|
968
|
-
panelId,
|
|
969
|
-
side,
|
|
970
|
-
hasDescription,
|
|
971
|
-
registerDescription
|
|
972
|
-
}), [
|
|
973
|
-
close,
|
|
974
|
-
pop,
|
|
975
|
-
isNested,
|
|
976
|
-
isTop,
|
|
977
|
-
panelId,
|
|
978
|
-
side,
|
|
1029
|
+
return () => {
|
|
1030
|
+
setHasDescription(false);
|
|
1031
|
+
};
|
|
1032
|
+
};
|
|
1033
|
+
return {
|
|
979
1034
|
hasDescription,
|
|
980
|
-
|
|
981
|
-
|
|
1035
|
+
panelContext: getPanelContext({
|
|
1036
|
+
close,
|
|
1037
|
+
hasDescription,
|
|
1038
|
+
isNested,
|
|
1039
|
+
isTop,
|
|
1040
|
+
panelId,
|
|
1041
|
+
pop,
|
|
1042
|
+
registerDescription,
|
|
1043
|
+
side
|
|
1044
|
+
})
|
|
1045
|
+
};
|
|
1046
|
+
};
|
|
1047
|
+
const useSheetPanelModel = (props) => {
|
|
1048
|
+
const { item, index, depth, isTop, isNested, side, config, classNames, pop, close, snapHeights, activeSnapIndex, layout, renderHeader, slideFrom, slideTarget, spring, stackSpring } = props;
|
|
1049
|
+
const panelRef = useRef(null);
|
|
1050
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
1051
|
+
const measuredHeight = usePanelHeight(panelRef, snapHeights.length > 0);
|
|
1052
|
+
const transform = getStackTransform(depth, config.stacking);
|
|
1053
|
+
const panelStyles = getPanelStyles(side, config, index);
|
|
1054
|
+
const handleAnimationComplete = useOpeningCompletion(isTop, config.onOpenComplete);
|
|
1055
|
+
const dragState = useSheetPanelDrag(props, panelRef);
|
|
1056
|
+
const ariaLabel = getPanelAriaLabel(item, config.ariaLabel);
|
|
1057
|
+
const panelId = `stacksheet-${item.id}`;
|
|
1058
|
+
const { hasDescription, panelContext } = useSheetPanelContext(props, panelId);
|
|
982
1059
|
const isComposable = resolvePanelLayout(layout, renderHeader) === "composable";
|
|
983
1060
|
const hasPanelClass = classNames.panel !== "";
|
|
984
1061
|
const dragOffset = getDragTransform(side, dragState.offset);
|
|
985
1062
|
const panelStyle = buildPanelStyle(panelStyles, isTop, hasPanelClass, dragState.isDragging);
|
|
986
|
-
const headerProps =
|
|
987
|
-
isNested,
|
|
988
|
-
onBack: pop,
|
|
989
|
-
onClose: close,
|
|
990
|
-
side
|
|
991
|
-
}), [
|
|
1063
|
+
const headerProps = getHeaderProps({
|
|
992
1064
|
close,
|
|
993
1065
|
isNested,
|
|
994
1066
|
pop,
|
|
995
1067
|
side
|
|
996
|
-
|
|
1068
|
+
});
|
|
997
1069
|
const ariaProps = buildAriaProps(isTop, config.modal, isComposable, ariaLabel, panelId, hasDescription);
|
|
998
1070
|
const transition = buildPanelTransition(dragState.isDragging, isTop, spring, stackSpring);
|
|
999
1071
|
const animatedRadius = getAnimatedBorderRadius(side, depth, config.stacking);
|
|
@@ -1003,18 +1075,45 @@ const SheetPanel = memo(function SheetPanel({ item, index, depth, isTop, isNeste
|
|
|
1003
1075
|
const initialRadius = getInitialRadius(side);
|
|
1004
1076
|
const showSideHandle = isTop && side !== "bottom";
|
|
1005
1077
|
const showBottomHandle = isTop && side === "bottom";
|
|
1078
|
+
const dismiss = isNested ? pop : close;
|
|
1079
|
+
const sideHandle = getOptionalSideHandle({
|
|
1080
|
+
isHovered,
|
|
1081
|
+
onDismiss: dismiss,
|
|
1082
|
+
show: showSideHandle,
|
|
1083
|
+
side
|
|
1084
|
+
});
|
|
1085
|
+
return {
|
|
1086
|
+
animateTarget,
|
|
1087
|
+
ariaProps,
|
|
1088
|
+
bottomHandle: getOptionalBottomHandle(showBottomHandle, dismiss),
|
|
1089
|
+
handleAnimationComplete,
|
|
1090
|
+
headerProps,
|
|
1091
|
+
hoverProps: getPanelHoverProps(showSideHandle, setIsHovered),
|
|
1092
|
+
inactivePanelProps: getInactivePanelProps(isTop),
|
|
1093
|
+
initialRadius,
|
|
1094
|
+
isComposable,
|
|
1095
|
+
panelContext,
|
|
1096
|
+
panelRef,
|
|
1097
|
+
panelStyle,
|
|
1098
|
+
resolvedSlideFrom,
|
|
1099
|
+
sideHandle
|
|
1100
|
+
};
|
|
1101
|
+
};
|
|
1102
|
+
const SheetPanel = (props) => {
|
|
1103
|
+
const { item, isTop, config, classNames, Content, shouldRender, renderHeader, prefersReducedMotion } = props;
|
|
1104
|
+
const { animateTarget, ariaProps, bottomHandle, handleAnimationComplete, headerProps, hoverProps, inactivePanelProps, initialRadius, isComposable, panelContext, panelRef, panelStyle, resolvedSlideFrom, sideHandle } = useSheetPanelModel(props);
|
|
1006
1105
|
const panelContent = /* @__PURE__ */ jsxs(m.div, {
|
|
1007
1106
|
animate: animateTarget,
|
|
1008
1107
|
className: classNames.panel || void 0,
|
|
1009
1108
|
exit: {
|
|
1010
1109
|
...resolvedSlideFrom,
|
|
1011
|
-
opacity: .6,
|
|
1012
1110
|
boxShadow: getShadow(false),
|
|
1111
|
+
opacity: .6,
|
|
1013
1112
|
transition: {
|
|
1014
|
-
|
|
1113
|
+
boxShadow: VISUAL_TWEEN,
|
|
1015
1114
|
duration: prefersReducedMotion ? 0 : .24,
|
|
1016
1115
|
ease: "easeOut",
|
|
1017
|
-
|
|
1116
|
+
type: "tween"
|
|
1018
1117
|
}
|
|
1019
1118
|
},
|
|
1020
1119
|
initial: {
|
|
@@ -1024,25 +1123,15 @@ const SheetPanel = memo(function SheetPanel({ item, index, depth, isTop, isNeste
|
|
|
1024
1123
|
boxShadow: getShadow(false)
|
|
1025
1124
|
},
|
|
1026
1125
|
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
1126
|
ref: panelRef,
|
|
1032
1127
|
style: panelStyle,
|
|
1033
1128
|
tabIndex: isTop ? -1 : void 0,
|
|
1034
|
-
...
|
|
1035
|
-
|
|
1036
|
-
inert: true
|
|
1037
|
-
},
|
|
1129
|
+
...hoverProps,
|
|
1130
|
+
...inactivePanelProps,
|
|
1038
1131
|
...ariaProps,
|
|
1039
|
-
children: [
|
|
1040
|
-
isHovered,
|
|
1041
|
-
onDismiss: isNested ? pop : close,
|
|
1042
|
-
side
|
|
1043
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1132
|
+
children: [sideHandle, /* @__PURE__ */ jsxs("div", {
|
|
1044
1133
|
className: "relative flex min-h-0 flex-1 flex-col overflow-hidden rounded-[inherit]",
|
|
1045
|
-
children: [
|
|
1134
|
+
children: [bottomHandle, /* @__PURE__ */ jsx(PanelInnerContent, {
|
|
1046
1135
|
Content,
|
|
1047
1136
|
data: item.data,
|
|
1048
1137
|
headerClassName: classNames.header || void 0,
|
|
@@ -1062,30 +1151,54 @@ const SheetPanel = memo(function SheetPanel({ item, index, depth, isTop, isNeste
|
|
|
1062
1151
|
children: panelContent
|
|
1063
1152
|
})
|
|
1064
1153
|
});
|
|
1065
|
-
}
|
|
1154
|
+
};
|
|
1066
1155
|
//#endregion
|
|
1067
1156
|
//#region src/renderer.tsx
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1157
|
+
const handleBackdropExitComplete = () => {
|
|
1158
|
+
requestAnimationFrame(() => {
|
|
1159
|
+
document.body.offsetHeight;
|
|
1160
|
+
});
|
|
1161
|
+
};
|
|
1162
|
+
const getResolvedSnapHeights = (side, snapPoints, viewportHeight) => side === "bottom" && snapPoints.length > 0 ? resolveSnapPoints(snapPoints, viewportHeight) : [];
|
|
1163
|
+
const getActiveInternalSnapIndex = ({ defaultSnapIndex, internalSnap, snapContext, snapHeights }) => internalSnap.context === snapContext && internalSnap.snapCount === snapHeights.length ? internalSnap.index : defaultSnapIndex;
|
|
1164
|
+
const getMotionSpring = (prefersReducedMotion, config) => prefersReducedMotion ? {
|
|
1165
|
+
duration: 0,
|
|
1166
|
+
type: "tween"
|
|
1167
|
+
} : {
|
|
1168
|
+
damping: config.spring.damping,
|
|
1169
|
+
mass: config.spring.mass,
|
|
1170
|
+
stiffness: config.spring.stiffness,
|
|
1171
|
+
type: "spring"
|
|
1172
|
+
};
|
|
1173
|
+
const getBackdropStyle = (config, hasBackdropClass) => ({
|
|
1174
|
+
cursor: config.closeOnBackdrop && config.dismissible ? "pointer" : void 0,
|
|
1175
|
+
willChange: "opacity",
|
|
1176
|
+
zIndex: config.zIndex,
|
|
1177
|
+
...hasBackdropClass ? {} : { background: "var(--overlay, rgba(0, 0, 0, 0.15))" }
|
|
1178
|
+
});
|
|
1179
|
+
const isContentComponent = (content) => typeof content === "function";
|
|
1180
|
+
const getStaticContent = (sheets, type) => {
|
|
1181
|
+
if (sheets === void 0) return;
|
|
1182
|
+
const content = Object.entries(sheets).find(([sheetType]) => sheetType === type)?.[1];
|
|
1183
|
+
return isContentComponent(content) ? content : void 0;
|
|
1184
|
+
};
|
|
1185
|
+
const useRendererStore = (store) => {
|
|
1076
1186
|
const isOpen = useStore(store, (s) => s.isOpen);
|
|
1077
1187
|
const stack = useStore(store, (s) => s.stack);
|
|
1078
|
-
const rawClose = useStore(store, (s) =>
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1188
|
+
const { rawClose, rawPop } = useStore(store, useShallow((s) => ({
|
|
1189
|
+
rawClose: s.close,
|
|
1190
|
+
rawPop: s.pop
|
|
1191
|
+
})));
|
|
1192
|
+
return {
|
|
1193
|
+
isOpen,
|
|
1194
|
+
rawClose,
|
|
1195
|
+
rawPop,
|
|
1196
|
+
stack
|
|
1197
|
+
};
|
|
1198
|
+
};
|
|
1199
|
+
const useSnapState = (config, isOpen, side, stack) => {
|
|
1083
1200
|
const viewportHeight = useViewportHeight(isOpen && side === "bottom" && config.snapPoints.length > 0);
|
|
1084
|
-
const snapHeights =
|
|
1085
|
-
side,
|
|
1086
|
-
config.snapPoints,
|
|
1087
|
-
viewportHeight
|
|
1088
|
-
]);
|
|
1201
|
+
const snapHeights = getResolvedSnapHeights(side, config.snapPoints, viewportHeight);
|
|
1089
1202
|
const snapContext = isOpen ? stack.map((item) => item.id).join("\0") : "";
|
|
1090
1203
|
const defaultSnapIndex = snapHeights.length > 0 ? snapHeights.length - 1 : 0;
|
|
1091
1204
|
const [internalSnap, setInternalSnap] = useState({
|
|
@@ -1093,32 +1206,50 @@ function SheetRenderer({ store, config, sheets, componentMap, classNames: classN
|
|
|
1093
1206
|
index: defaultSnapIndex,
|
|
1094
1207
|
snapCount: snapHeights.length
|
|
1095
1208
|
});
|
|
1096
|
-
const internalSnapIndex =
|
|
1209
|
+
const internalSnapIndex = getActiveInternalSnapIndex({
|
|
1210
|
+
defaultSnapIndex,
|
|
1211
|
+
internalSnap,
|
|
1212
|
+
snapContext,
|
|
1213
|
+
snapHeights
|
|
1214
|
+
});
|
|
1097
1215
|
const activeSnapIndex = config.snapPointIndex ?? internalSnapIndex;
|
|
1098
|
-
const handleSnap =
|
|
1216
|
+
const handleSnap = (index) => {
|
|
1099
1217
|
setInternalSnap({
|
|
1100
1218
|
context: snapContext,
|
|
1101
1219
|
index,
|
|
1102
1220
|
snapCount: snapHeights.length
|
|
1103
1221
|
});
|
|
1104
1222
|
config.onSnapPointChange?.(index);
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1223
|
+
};
|
|
1224
|
+
return {
|
|
1225
|
+
activeSnapIndex,
|
|
1226
|
+
handleSnap,
|
|
1227
|
+
snapHeights
|
|
1228
|
+
};
|
|
1229
|
+
};
|
|
1230
|
+
const useCloseControls = (rawClose, rawPop) => {
|
|
1110
1231
|
const closeReasonRef = useRef("programmatic");
|
|
1111
|
-
const closeWith =
|
|
1232
|
+
const closeWith = (reason) => {
|
|
1112
1233
|
closeReasonRef.current = reason;
|
|
1113
1234
|
rawClose();
|
|
1114
|
-
}
|
|
1115
|
-
const popWith =
|
|
1235
|
+
};
|
|
1236
|
+
const popWith = (reason) => {
|
|
1116
1237
|
closeReasonRef.current = reason;
|
|
1117
1238
|
rawPop();
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1239
|
+
};
|
|
1240
|
+
return {
|
|
1241
|
+
close: () => {
|
|
1242
|
+
closeWith("programmatic");
|
|
1243
|
+
},
|
|
1244
|
+
closeReasonRef,
|
|
1245
|
+
closeWith,
|
|
1246
|
+
pop: () => {
|
|
1247
|
+
popWith("programmatic");
|
|
1248
|
+
},
|
|
1249
|
+
popWith
|
|
1250
|
+
};
|
|
1251
|
+
};
|
|
1252
|
+
const useFocusRestore = (isOpen) => {
|
|
1122
1253
|
const triggerRef = useRef(null);
|
|
1123
1254
|
const wasOpenRef = useRef(false);
|
|
1124
1255
|
useEffect(() => {
|
|
@@ -1130,78 +1261,112 @@ function SheetRenderer({ store, config, sheets, componentMap, classNames: classN
|
|
|
1130
1261
|
}
|
|
1131
1262
|
wasOpenRef.current = isOpen;
|
|
1132
1263
|
}, [isOpen]);
|
|
1133
|
-
|
|
1264
|
+
};
|
|
1265
|
+
const dismissFromEscape = ({ closeReasonRef, rawClose, rawPop, stackLengthRef }) => {
|
|
1266
|
+
closeReasonRef.current = "escape";
|
|
1267
|
+
if (stackLengthRef.current > 1) rawPop();
|
|
1268
|
+
else rawClose();
|
|
1269
|
+
};
|
|
1270
|
+
const useDismissalEffects = ({ closeReasonRef, config, isOpen, rawClose, rawPop, stackLength }) => {
|
|
1271
|
+
const stackLengthRef = useRef(stackLength);
|
|
1134
1272
|
useEffect(() => {
|
|
1135
|
-
stackLengthRef.current =
|
|
1136
|
-
}, [
|
|
1273
|
+
stackLengthRef.current = stackLength;
|
|
1274
|
+
}, [stackLength]);
|
|
1137
1275
|
useEffect(() => {
|
|
1138
|
-
|
|
1139
|
-
|
|
1276
|
+
const shouldListen = isOpen && config.closeOnEscape && config.dismissible;
|
|
1277
|
+
const handleKeyDown = (e) => {
|
|
1140
1278
|
if (e.key === "Escape") {
|
|
1141
1279
|
e.preventDefault();
|
|
1142
|
-
|
|
1143
|
-
|
|
1280
|
+
dismissFromEscape({
|
|
1281
|
+
closeReasonRef,
|
|
1282
|
+
rawClose,
|
|
1283
|
+
rawPop,
|
|
1284
|
+
stackLengthRef
|
|
1285
|
+
});
|
|
1144
1286
|
}
|
|
1145
|
-
}
|
|
1146
|
-
document.addEventListener("keydown", handleKeyDown);
|
|
1147
|
-
return () =>
|
|
1287
|
+
};
|
|
1288
|
+
if (shouldListen) document.addEventListener("keydown", handleKeyDown);
|
|
1289
|
+
return () => {
|
|
1290
|
+
if (shouldListen) document.removeEventListener("keydown", handleKeyDown);
|
|
1291
|
+
};
|
|
1148
1292
|
}, [
|
|
1149
1293
|
isOpen,
|
|
1150
1294
|
config.closeOnEscape,
|
|
1151
1295
|
config.dismissible,
|
|
1152
|
-
|
|
1153
|
-
|
|
1296
|
+
rawPop,
|
|
1297
|
+
rawClose,
|
|
1298
|
+
closeReasonRef
|
|
1154
1299
|
]);
|
|
1155
1300
|
useEffect(() => {
|
|
1156
|
-
|
|
1157
|
-
const
|
|
1158
|
-
watcher
|
|
1159
|
-
|
|
1160
|
-
|
|
1301
|
+
const CloseWatcherConstructor = globalThis.CloseWatcher;
|
|
1302
|
+
const shouldListen = isOpen && config.dismissible && CloseWatcherConstructor !== void 0;
|
|
1303
|
+
let watcher;
|
|
1304
|
+
const handleClose = () => {
|
|
1305
|
+
dismissFromEscape({
|
|
1306
|
+
closeReasonRef,
|
|
1307
|
+
rawClose,
|
|
1308
|
+
rawPop,
|
|
1309
|
+
stackLengthRef
|
|
1310
|
+
});
|
|
1311
|
+
};
|
|
1312
|
+
if (shouldListen) {
|
|
1313
|
+
watcher = new CloseWatcherConstructor();
|
|
1314
|
+
watcher.addEventListener("close", handleClose);
|
|
1315
|
+
}
|
|
1316
|
+
return () => {
|
|
1317
|
+
if (watcher !== void 0) {
|
|
1318
|
+
watcher.removeEventListener("close", handleClose);
|
|
1319
|
+
watcher.destroy();
|
|
1320
|
+
}
|
|
1161
1321
|
};
|
|
1162
|
-
return () => watcher.destroy();
|
|
1163
1322
|
}, [
|
|
1164
1323
|
isOpen,
|
|
1165
1324
|
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
|
|
1325
|
+
rawPop,
|
|
1326
|
+
rawClose,
|
|
1327
|
+
closeReasonRef
|
|
1184
1328
|
]);
|
|
1329
|
+
};
|
|
1330
|
+
/**
|
|
1331
|
+
* Root renderer component — manages the backdrop, scroll lock, snap points,
|
|
1332
|
+
* close reasons, focus restoration, keyboard/CloseWatcher dismissal, and
|
|
1333
|
+
* delegates per-panel rendering to `SheetPanel`.
|
|
1334
|
+
*
|
|
1335
|
+
* Mounted inside a Portal by `StacksheetProvider`.
|
|
1336
|
+
*/
|
|
1337
|
+
const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classNamesProp, layout, renderHeader }) => {
|
|
1338
|
+
const { isOpen, rawClose, rawPop, stack } = useRendererStore(store);
|
|
1339
|
+
const side = useResolvedSide(config);
|
|
1340
|
+
const prefersReducedMotion = useReducedMotion() ?? false;
|
|
1341
|
+
const classNames = resolveClassNames(classNamesProp);
|
|
1342
|
+
const { activeSnapIndex, handleSnap, snapHeights } = useSnapState(config, isOpen, side, stack);
|
|
1343
|
+
const { close, closeReasonRef, closeWith, pop, popWith } = useCloseControls(rawClose, rawPop);
|
|
1344
|
+
useBodyScale(config, isOpen, prefersReducedMotion);
|
|
1345
|
+
useFocusRestore(isOpen);
|
|
1346
|
+
useDismissalEffects({
|
|
1347
|
+
closeReasonRef,
|
|
1348
|
+
config,
|
|
1349
|
+
isOpen,
|
|
1350
|
+
rawClose,
|
|
1351
|
+
rawPop,
|
|
1352
|
+
stackLength: stack.length
|
|
1353
|
+
});
|
|
1354
|
+
const slideFrom = getSlideFrom(side);
|
|
1355
|
+
const slideTarget = getSlideTarget();
|
|
1356
|
+
const spring = getMotionSpring(prefersReducedMotion, config);
|
|
1185
1357
|
const stackSpring = spring;
|
|
1186
1358
|
const isModal = config.modal;
|
|
1187
1359
|
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(() => {
|
|
1360
|
+
const backdropStyle = getBackdropStyle(config, classNames.backdrop !== "");
|
|
1361
|
+
const handleExitComplete = () => {
|
|
1196
1362
|
if (stack.length === 0) config.onCloseComplete?.(closeReasonRef.current);
|
|
1197
|
-
}
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
const swipePop = useCallback(() => popWith("swipe"), [popWith]);
|
|
1363
|
+
};
|
|
1364
|
+
const swipeClose = () => {
|
|
1365
|
+
closeWith("swipe");
|
|
1366
|
+
};
|
|
1367
|
+
const swipePop = () => {
|
|
1368
|
+
popWith("swipe");
|
|
1369
|
+
};
|
|
1205
1370
|
const shouldLockScroll = isOpen && isModal && config.lockScroll;
|
|
1206
1371
|
return /* @__PURE__ */ jsxs(LazyMotion, {
|
|
1207
1372
|
features: domMax,
|
|
@@ -1212,7 +1377,9 @@ function SheetRenderer({ store, config, sheets, componentMap, classNames: classN
|
|
|
1212
1377
|
className: `fixed inset-0 ${classNames.backdrop || ""}`,
|
|
1213
1378
|
exit: { opacity: 0 },
|
|
1214
1379
|
initial: { opacity: 0 },
|
|
1215
|
-
onClick: config.closeOnBackdrop && config.dismissible ? () =>
|
|
1380
|
+
onClick: config.closeOnBackdrop && config.dismissible ? () => {
|
|
1381
|
+
closeWith("backdrop");
|
|
1382
|
+
} : void 0,
|
|
1216
1383
|
style: backdropStyle,
|
|
1217
1384
|
transition: spring
|
|
1218
1385
|
}, "stacksheet-backdrop")
|
|
@@ -1231,7 +1398,7 @@ function SheetRenderer({ store, config, sheets, componentMap, classNames: classN
|
|
|
1231
1398
|
const shouldRender = depth <= config.stacking.renderThreshold;
|
|
1232
1399
|
return /* @__PURE__ */ jsx(SheetPanel, {
|
|
1233
1400
|
activeSnapIndex,
|
|
1234
|
-
Content: componentMap.get(item.type) ?? sheets
|
|
1401
|
+
Content: componentMap.get(item.type) ?? getStaticContent(sheets, item.type),
|
|
1235
1402
|
classNames,
|
|
1236
1403
|
close,
|
|
1237
1404
|
config,
|
|
@@ -1260,67 +1427,229 @@ function SheetRenderer({ store, config, sheets, componentMap, classNames: classN
|
|
|
1260
1427
|
})
|
|
1261
1428
|
})]
|
|
1262
1429
|
});
|
|
1263
|
-
}
|
|
1430
|
+
};
|
|
1264
1431
|
//#endregion
|
|
1265
|
-
//#region src/Store/
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1432
|
+
//#region src/Store/store-args.ts
|
|
1433
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
1434
|
+
const toRecord = (value) => isRecord(value) ? value : {};
|
|
1435
|
+
const isComponent = (value) => typeof value === "function";
|
|
1436
|
+
const getComponentName = (component) => {
|
|
1437
|
+
const name = component.displayName ?? component.name;
|
|
1438
|
+
return name === "" ? void 0 : name;
|
|
1439
|
+
};
|
|
1440
|
+
const getNodeEnv = () => globalThis.process?.env?.NODE_ENV;
|
|
1441
|
+
const getStringArg = (value, name) => {
|
|
1442
|
+
if (typeof value !== "string") throw new TypeError(`Expected ${name} to be a string.`);
|
|
1443
|
+
return value;
|
|
1444
|
+
};
|
|
1445
|
+
const resolvePresentationOptions = (value) => {
|
|
1446
|
+
if (!isRecord(value)) return;
|
|
1447
|
+
const { ariaLabel } = value;
|
|
1448
|
+
if (ariaLabel !== void 0 && typeof ariaLabel !== "string") return;
|
|
1449
|
+
return ariaLabel === void 0 ? {} : { ariaLabel };
|
|
1450
|
+
};
|
|
1272
1451
|
/**
|
|
1273
1452
|
* Dev-mode warning: detect likely inline arrow functions passed as ad-hoc components.
|
|
1274
1453
|
* When a new component reference has the same displayName/name as an existing one,
|
|
1275
1454
|
* it's almost always an inline arrow being re-created every render.
|
|
1276
1455
|
*/
|
|
1277
|
-
|
|
1278
|
-
if (
|
|
1279
|
-
const name = component
|
|
1280
|
-
if (
|
|
1456
|
+
const warnInlineComponent = (component, componentRegistry, warnedNames) => {
|
|
1457
|
+
if (getNodeEnv() === "production") return;
|
|
1458
|
+
const name = getComponentName(component);
|
|
1459
|
+
if (name === void 0) return;
|
|
1281
1460
|
if (warnedNames.has(name)) return;
|
|
1282
|
-
for (const [existing, key] of componentRegistry) if ((existing
|
|
1461
|
+
for (const [existing, key] of componentRegistry) if (getComponentName(existing) === name) {
|
|
1283
1462
|
warnedNames.add(name);
|
|
1284
1463
|
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
1464
|
return;
|
|
1286
1465
|
}
|
|
1287
|
-
}
|
|
1466
|
+
};
|
|
1467
|
+
const registerComponent = (component, componentRegistry, componentMap, getNextKey, warnedNames) => {
|
|
1468
|
+
const existingKey = componentRegistry.get(component);
|
|
1469
|
+
if (existingKey !== void 0) return existingKey;
|
|
1470
|
+
warnInlineComponent(component, componentRegistry, warnedNames);
|
|
1471
|
+
const nextKey = getNextKey();
|
|
1472
|
+
componentRegistry.set(component, nextKey);
|
|
1473
|
+
componentMap.set(nextKey, component);
|
|
1474
|
+
return nextKey;
|
|
1475
|
+
};
|
|
1288
1476
|
/**
|
|
1289
1477
|
* If `first` is a function (component), register it and return { type, id, data }.
|
|
1290
1478
|
* Otherwise, pass through the string-based (type, id, data) args unchanged.
|
|
1291
1479
|
*/
|
|
1292
|
-
|
|
1293
|
-
if (
|
|
1294
|
-
const
|
|
1295
|
-
let typeKey = componentRegistry.get(component);
|
|
1296
|
-
if (!typeKey) {
|
|
1297
|
-
warnInlineComponent(component, componentRegistry, warnedNames);
|
|
1298
|
-
typeKey = getNextKey();
|
|
1299
|
-
componentRegistry.set(component, typeKey);
|
|
1300
|
-
componentMap.set(typeKey, component);
|
|
1301
|
-
}
|
|
1480
|
+
const resolveArgs = (componentRegistry, componentMap, getNextKey, warnedNames, first, second, third, fourth) => {
|
|
1481
|
+
if (isComponent(first)) {
|
|
1482
|
+
const typeKey = registerComponent(first, componentRegistry, componentMap, getNextKey, warnedNames);
|
|
1302
1483
|
if (typeof second === "string") return {
|
|
1303
1484
|
ariaLabel: resolvePresentationOptions(fourth)?.ariaLabel,
|
|
1304
|
-
|
|
1485
|
+
data: toRecord(third),
|
|
1305
1486
|
id: second,
|
|
1306
|
-
|
|
1487
|
+
type: typeKey
|
|
1307
1488
|
};
|
|
1308
1489
|
return {
|
|
1309
1490
|
ariaLabel: resolvePresentationOptions(third)?.ariaLabel,
|
|
1310
|
-
|
|
1491
|
+
data: toRecord(second),
|
|
1311
1492
|
id: crypto.randomUUID(),
|
|
1312
|
-
|
|
1493
|
+
type: typeKey
|
|
1313
1494
|
};
|
|
1314
1495
|
}
|
|
1315
1496
|
return {
|
|
1316
1497
|
ariaLabel: resolvePresentationOptions(fourth)?.ariaLabel,
|
|
1317
|
-
|
|
1318
|
-
id: second,
|
|
1319
|
-
|
|
1498
|
+
data: toRecord(third),
|
|
1499
|
+
id: getStringArg(second, "sheet id"),
|
|
1500
|
+
type: getStringArg(first, "sheet type")
|
|
1320
1501
|
};
|
|
1321
|
-
}
|
|
1502
|
+
};
|
|
1322
1503
|
//#endregion
|
|
1323
|
-
//#region src/Store/
|
|
1504
|
+
//#region src/Store/create-sheet-store.ts
|
|
1505
|
+
const createItem = ({ type, id, data, ariaLabel }) => ({
|
|
1506
|
+
ariaLabel,
|
|
1507
|
+
data,
|
|
1508
|
+
id,
|
|
1509
|
+
type
|
|
1510
|
+
});
|
|
1511
|
+
const createResolvedWriters = (set, config) => ({
|
|
1512
|
+
openResolved: (resolved) => {
|
|
1513
|
+
set({
|
|
1514
|
+
isOpen: true,
|
|
1515
|
+
stack: [createItem(resolved)]
|
|
1516
|
+
});
|
|
1517
|
+
},
|
|
1518
|
+
pushResolved: (resolved) => {
|
|
1519
|
+
set((state) => {
|
|
1520
|
+
const item = createItem(resolved);
|
|
1521
|
+
if (Number.isFinite(config.maxDepth) && state.stack.length >= config.maxDepth) return {
|
|
1522
|
+
isOpen: true,
|
|
1523
|
+
stack: [...state.stack.slice(0, -1), item]
|
|
1524
|
+
};
|
|
1525
|
+
return {
|
|
1526
|
+
isOpen: true,
|
|
1527
|
+
stack: [...state.stack, item]
|
|
1528
|
+
};
|
|
1529
|
+
});
|
|
1530
|
+
},
|
|
1531
|
+
replaceResolved: (resolved) => {
|
|
1532
|
+
set((state) => {
|
|
1533
|
+
const item = createItem(resolved);
|
|
1534
|
+
if (state.stack.length === 0) return {
|
|
1535
|
+
isOpen: true,
|
|
1536
|
+
stack: [item]
|
|
1537
|
+
};
|
|
1538
|
+
return {
|
|
1539
|
+
isOpen: true,
|
|
1540
|
+
stack: [...state.stack.slice(0, -1), item]
|
|
1541
|
+
};
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
const createNavigate = (componentMap, get, resolve, writers) => (first, second, third, fourth) => {
|
|
1546
|
+
const resolved = resolve(first, second, third, fourth);
|
|
1547
|
+
const { stack } = get();
|
|
1548
|
+
const top = stack.at(-1);
|
|
1549
|
+
if (stack.length === 0) {
|
|
1550
|
+
writers.openResolved(resolved);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
let isSameType = top?.type === resolved.type;
|
|
1554
|
+
if (!isSameType && isComponent(first)) isSameType = componentMap.get(top?.type ?? "") === first;
|
|
1555
|
+
if (isSameType) {
|
|
1556
|
+
writers.replaceResolved(resolved);
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
writers.pushResolved(resolved);
|
|
1560
|
+
};
|
|
1561
|
+
const createPop = (set, pruneRegistry) => () => {
|
|
1562
|
+
set((state) => {
|
|
1563
|
+
if (state.stack.length <= 1) {
|
|
1564
|
+
pruneRegistry([]);
|
|
1565
|
+
return {
|
|
1566
|
+
isOpen: false,
|
|
1567
|
+
stack: []
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
const next = state.stack.slice(0, -1);
|
|
1571
|
+
pruneRegistry(next);
|
|
1572
|
+
return {
|
|
1573
|
+
isOpen: true,
|
|
1574
|
+
stack: next
|
|
1575
|
+
};
|
|
1576
|
+
});
|
|
1577
|
+
};
|
|
1578
|
+
const createRemove = (set, pruneRegistry) => (id) => {
|
|
1579
|
+
set((state) => {
|
|
1580
|
+
const next = state.stack.filter((item) => item.id !== id);
|
|
1581
|
+
if (next.length === state.stack.length) return state;
|
|
1582
|
+
pruneRegistry(next);
|
|
1583
|
+
return next.length === 0 ? {
|
|
1584
|
+
isOpen: false,
|
|
1585
|
+
stack: []
|
|
1586
|
+
} : { stack: next };
|
|
1587
|
+
});
|
|
1588
|
+
};
|
|
1589
|
+
const createSetData = (set, resolve) => (first, second, third) => {
|
|
1590
|
+
const { id, data } = resolve(first, second, third);
|
|
1591
|
+
set((state) => {
|
|
1592
|
+
const index = state.stack.findIndex((item) => item.id === id);
|
|
1593
|
+
const existing = state.stack[index];
|
|
1594
|
+
if (existing === void 0) return state;
|
|
1595
|
+
const updated = [...state.stack];
|
|
1596
|
+
updated[index] = {
|
|
1597
|
+
...existing,
|
|
1598
|
+
data
|
|
1599
|
+
};
|
|
1600
|
+
return { stack: updated };
|
|
1601
|
+
});
|
|
1602
|
+
};
|
|
1603
|
+
const createSwap = (set, internals) => (first, second, third) => {
|
|
1604
|
+
const type = isComponent(first) ? registerComponent(first, internals.componentRegistry, internals.componentMap, internals.getNextKey, internals.warnedNames) : getStringArg(first, "sheet type");
|
|
1605
|
+
const data = toRecord(second);
|
|
1606
|
+
const ariaLabel = resolvePresentationOptions(third)?.ariaLabel;
|
|
1607
|
+
set((state) => {
|
|
1608
|
+
const top = state.stack.at(-1);
|
|
1609
|
+
if (top === void 0) return state;
|
|
1610
|
+
const newStack = [...state.stack];
|
|
1611
|
+
newStack[newStack.length - 1] = {
|
|
1612
|
+
ariaLabel: ariaLabel ?? top.ariaLabel,
|
|
1613
|
+
data,
|
|
1614
|
+
id: top.id,
|
|
1615
|
+
type
|
|
1616
|
+
};
|
|
1617
|
+
return { stack: newStack };
|
|
1618
|
+
});
|
|
1619
|
+
};
|
|
1620
|
+
const createStoreState = ({ componentMap, componentRegistry, config, getNextKey, pruneRegistry, resolve, warnedNames }) => (set, get) => {
|
|
1621
|
+
const writers = createResolvedWriters(set, config);
|
|
1622
|
+
return {
|
|
1623
|
+
close: () => {
|
|
1624
|
+
pruneRegistry([]);
|
|
1625
|
+
set({
|
|
1626
|
+
isOpen: false,
|
|
1627
|
+
stack: []
|
|
1628
|
+
});
|
|
1629
|
+
},
|
|
1630
|
+
isOpen: false,
|
|
1631
|
+
navigate: createNavigate(componentMap, get, resolve, writers),
|
|
1632
|
+
open: (first, second, third, fourth) => {
|
|
1633
|
+
writers.openResolved(resolve(first, second, third, fourth));
|
|
1634
|
+
},
|
|
1635
|
+
pop: createPop(set, pruneRegistry),
|
|
1636
|
+
push: (first, second, third, fourth) => {
|
|
1637
|
+
writers.pushResolved(resolve(first, second, third, fourth));
|
|
1638
|
+
},
|
|
1639
|
+
remove: createRemove(set, pruneRegistry),
|
|
1640
|
+
replace: (first, second, third, fourth) => {
|
|
1641
|
+
writers.replaceResolved(resolve(first, second, third, fourth));
|
|
1642
|
+
},
|
|
1643
|
+
setData: createSetData(set, resolve),
|
|
1644
|
+
stack: [],
|
|
1645
|
+
swap: createSwap(set, {
|
|
1646
|
+
componentMap,
|
|
1647
|
+
componentRegistry,
|
|
1648
|
+
getNextKey,
|
|
1649
|
+
warnedNames
|
|
1650
|
+
})
|
|
1651
|
+
};
|
|
1652
|
+
};
|
|
1324
1653
|
/**
|
|
1325
1654
|
* Create an isolated Zustand store for a sheet stack instance.
|
|
1326
1655
|
*
|
|
@@ -1332,189 +1661,39 @@ function resolveArgs(componentRegistry, componentMap, getNextKey, warnedNames, f
|
|
|
1332
1661
|
* The ad-hoc counter is scoped per instance to prevent identity leaks across
|
|
1333
1662
|
* multiple `createStacksheet()` calls or test runs.
|
|
1334
1663
|
*/
|
|
1335
|
-
|
|
1664
|
+
const createSheetStore = (config) => {
|
|
1336
1665
|
const componentRegistry = /* @__PURE__ */ new Map();
|
|
1337
1666
|
const componentMap = /* @__PURE__ */ new Map();
|
|
1338
1667
|
let adhocCounter = 0;
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1668
|
+
const getNextKey = () => {
|
|
1669
|
+
const key = `__adhoc_${adhocCounter}`;
|
|
1670
|
+
adhocCounter += 1;
|
|
1671
|
+
return key;
|
|
1672
|
+
};
|
|
1342
1673
|
const warnedNames = /* @__PURE__ */ new Set();
|
|
1343
|
-
|
|
1344
|
-
return resolveArgs(componentRegistry, componentMap, getNextKey, warnedNames, first, second, third, fourth);
|
|
1345
|
-
}
|
|
1674
|
+
const resolve = (first, second, third, fourth) => resolveArgs(componentRegistry, componentMap, getNextKey, warnedNames, first, second, third, fourth);
|
|
1346
1675
|
/** Remove registry entries for type keys no longer in the stack */
|
|
1347
|
-
|
|
1676
|
+
const pruneRegistry = (remainingStack) => {
|
|
1348
1677
|
const usedTypes = new Set(remainingStack.map((item) => item.type));
|
|
1349
1678
|
for (const [component, typeKey] of componentRegistry) if (!usedTypes.has(typeKey)) {
|
|
1350
1679
|
componentRegistry.delete(component);
|
|
1351
1680
|
componentMap.delete(typeKey);
|
|
1352
1681
|
}
|
|
1353
|
-
}
|
|
1682
|
+
};
|
|
1354
1683
|
return {
|
|
1355
|
-
|
|
1356
|
-
function _openResolved({ type, id, data, ariaLabel }) {
|
|
1357
|
-
set({
|
|
1358
|
-
stack: [{
|
|
1359
|
-
id,
|
|
1360
|
-
type,
|
|
1361
|
-
data,
|
|
1362
|
-
ariaLabel
|
|
1363
|
-
}],
|
|
1364
|
-
isOpen: true
|
|
1365
|
-
});
|
|
1366
|
-
}
|
|
1367
|
-
function _pushResolved({ type, id, data, ariaLabel }) {
|
|
1368
|
-
set((state) => {
|
|
1369
|
-
const item = {
|
|
1370
|
-
id,
|
|
1371
|
-
type,
|
|
1372
|
-
data,
|
|
1373
|
-
ariaLabel
|
|
1374
|
-
};
|
|
1375
|
-
if (Number.isFinite(config.maxDepth) && state.stack.length >= config.maxDepth) return {
|
|
1376
|
-
stack: [...state.stack.slice(0, -1), item],
|
|
1377
|
-
isOpen: true
|
|
1378
|
-
};
|
|
1379
|
-
return {
|
|
1380
|
-
stack: [...state.stack, item],
|
|
1381
|
-
isOpen: true
|
|
1382
|
-
};
|
|
1383
|
-
});
|
|
1384
|
-
}
|
|
1385
|
-
function _replaceResolved({ type, id, data, ariaLabel }) {
|
|
1386
|
-
set((state) => {
|
|
1387
|
-
const item = {
|
|
1388
|
-
id,
|
|
1389
|
-
type,
|
|
1390
|
-
data,
|
|
1391
|
-
ariaLabel
|
|
1392
|
-
};
|
|
1393
|
-
if (state.stack.length === 0) return {
|
|
1394
|
-
stack: [item],
|
|
1395
|
-
isOpen: true
|
|
1396
|
-
};
|
|
1397
|
-
return {
|
|
1398
|
-
stack: [...state.stack.slice(0, -1), item],
|
|
1399
|
-
isOpen: true
|
|
1400
|
-
};
|
|
1401
|
-
});
|
|
1402
|
-
}
|
|
1403
|
-
return {
|
|
1404
|
-
stack: [],
|
|
1405
|
-
isOpen: false,
|
|
1406
|
-
open(first, second, third, fourth) {
|
|
1407
|
-
_openResolved(resolve(first, second, third, fourth));
|
|
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 };
|
|
1445
|
-
});
|
|
1446
|
-
},
|
|
1447
|
-
navigate(first, second, third, fourth) {
|
|
1448
|
-
const resolved = resolve(first, second, third, fourth);
|
|
1449
|
-
const { stack } = get();
|
|
1450
|
-
const top = stack.at(-1);
|
|
1451
|
-
if (stack.length === 0) {
|
|
1452
|
-
_openResolved(resolved);
|
|
1453
|
-
return;
|
|
1454
|
-
}
|
|
1455
|
-
let isSameType = top?.type === resolved.type;
|
|
1456
|
-
if (!isSameType && typeof first === "function") isSameType = componentMap.get(top?.type ?? "") === first;
|
|
1457
|
-
if (isSameType) {
|
|
1458
|
-
_replaceResolved(resolved);
|
|
1459
|
-
return;
|
|
1460
|
-
}
|
|
1461
|
-
_pushResolved(resolved);
|
|
1462
|
-
},
|
|
1463
|
-
setData(first, second, third) {
|
|
1464
|
-
const { id, data } = resolve(first, second, third);
|
|
1465
|
-
set((state) => {
|
|
1466
|
-
const idx = state.stack.findIndex((item) => item.id === id);
|
|
1467
|
-
if (idx === -1) return state;
|
|
1468
|
-
const updated = [...state.stack];
|
|
1469
|
-
updated[idx] = {
|
|
1470
|
-
...updated[idx],
|
|
1471
|
-
data
|
|
1472
|
-
};
|
|
1473
|
-
return { stack: updated };
|
|
1474
|
-
});
|
|
1475
|
-
},
|
|
1476
|
-
remove(id) {
|
|
1477
|
-
set((state) => {
|
|
1478
|
-
const next = state.stack.filter((item) => item.id !== id);
|
|
1479
|
-
if (next.length === state.stack.length) return state;
|
|
1480
|
-
pruneRegistry(next);
|
|
1481
|
-
if (next.length === 0) return {
|
|
1482
|
-
stack: [],
|
|
1483
|
-
isOpen: false
|
|
1484
|
-
};
|
|
1485
|
-
return { stack: next };
|
|
1486
|
-
});
|
|
1487
|
-
},
|
|
1488
|
-
pop() {
|
|
1489
|
-
set((state) => {
|
|
1490
|
-
if (state.stack.length <= 1) {
|
|
1491
|
-
pruneRegistry([]);
|
|
1492
|
-
return {
|
|
1493
|
-
stack: [],
|
|
1494
|
-
isOpen: false
|
|
1495
|
-
};
|
|
1496
|
-
}
|
|
1497
|
-
const next = state.stack.slice(0, -1);
|
|
1498
|
-
pruneRegistry(next);
|
|
1499
|
-
return {
|
|
1500
|
-
stack: next,
|
|
1501
|
-
isOpen: true
|
|
1502
|
-
};
|
|
1503
|
-
});
|
|
1504
|
-
},
|
|
1505
|
-
close() {
|
|
1506
|
-
pruneRegistry([]);
|
|
1507
|
-
set({
|
|
1508
|
-
stack: [],
|
|
1509
|
-
isOpen: false
|
|
1510
|
-
});
|
|
1511
|
-
}
|
|
1512
|
-
};
|
|
1513
|
-
}),
|
|
1684
|
+
componentMap,
|
|
1514
1685
|
componentRegistry,
|
|
1515
|
-
|
|
1686
|
+
store: createStore()(createStoreState({
|
|
1687
|
+
componentMap,
|
|
1688
|
+
componentRegistry,
|
|
1689
|
+
config,
|
|
1690
|
+
getNextKey,
|
|
1691
|
+
pruneRegistry,
|
|
1692
|
+
resolve,
|
|
1693
|
+
warnedNames
|
|
1694
|
+
}))
|
|
1516
1695
|
};
|
|
1517
|
-
}
|
|
1696
|
+
};
|
|
1518
1697
|
//#endregion
|
|
1519
1698
|
//#region src/create.tsx
|
|
1520
1699
|
/**
|
|
@@ -1547,86 +1726,82 @@ function createSheetStore(config) {
|
|
|
1547
1726
|
*
|
|
1548
1727
|
* Use `useSheetPanel()` inside content components to access `close()` and `back()`.
|
|
1549
1728
|
*/
|
|
1550
|
-
|
|
1729
|
+
const createStacksheet = (config) => {
|
|
1551
1730
|
const resolved = resolveConfig(config);
|
|
1552
1731
|
const { store, componentMap } = createSheetStore(resolved);
|
|
1553
1732
|
const StoreContext = createContext(null);
|
|
1554
|
-
|
|
1555
|
-
const ctx =
|
|
1733
|
+
const useStoreContext = () => {
|
|
1734
|
+
const ctx = use(StoreContext);
|
|
1556
1735
|
if (!ctx) throw new Error("useSheet/useStacksheetState must be used within <StacksheetProvider>");
|
|
1557
1736
|
return ctx;
|
|
1558
|
-
}
|
|
1559
|
-
const
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
children:
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
});
|
|
1580
|
-
}
|
|
1581
|
-
function useSheet() {
|
|
1737
|
+
};
|
|
1738
|
+
const providerValue = {
|
|
1739
|
+
config: resolved,
|
|
1740
|
+
store
|
|
1741
|
+
};
|
|
1742
|
+
const StacksheetProvider = ({ sheets, children, classNames, layout, renderHeader }) => /* @__PURE__ */ jsxs(StoreContext.Provider, {
|
|
1743
|
+
value: providerValue,
|
|
1744
|
+
children: [children, /* @__PURE__ */ jsx(Portal, {
|
|
1745
|
+
asChild: false,
|
|
1746
|
+
children: /* @__PURE__ */ jsx(SheetRenderer, {
|
|
1747
|
+
classNames,
|
|
1748
|
+
componentMap,
|
|
1749
|
+
config: resolved,
|
|
1750
|
+
layout,
|
|
1751
|
+
renderHeader,
|
|
1752
|
+
sheets,
|
|
1753
|
+
store
|
|
1754
|
+
})
|
|
1755
|
+
})]
|
|
1756
|
+
});
|
|
1757
|
+
const useSheet = () => {
|
|
1582
1758
|
const { store: s } = useStoreContext();
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
}
|
|
1598
|
-
function useStacksheetState() {
|
|
1759
|
+
const state = s.getState();
|
|
1760
|
+
return {
|
|
1761
|
+
close: state.close,
|
|
1762
|
+
navigate: state.navigate,
|
|
1763
|
+
open: state.open,
|
|
1764
|
+
pop: state.pop,
|
|
1765
|
+
push: state.push,
|
|
1766
|
+
remove: state.remove,
|
|
1767
|
+
replace: state.replace,
|
|
1768
|
+
setData: state.setData,
|
|
1769
|
+
swap: state.swap
|
|
1770
|
+
};
|
|
1771
|
+
};
|
|
1772
|
+
const useStacksheetState = () => {
|
|
1599
1773
|
const { store: s } = useStoreContext();
|
|
1600
1774
|
return useStore(s, useShallow((state) => ({
|
|
1601
|
-
|
|
1602
|
-
|
|
1775
|
+
isOpen: state.isOpen,
|
|
1776
|
+
stack: state.stack
|
|
1603
1777
|
})));
|
|
1604
|
-
}
|
|
1778
|
+
};
|
|
1605
1779
|
return {
|
|
1606
1780
|
StacksheetProvider,
|
|
1781
|
+
store,
|
|
1607
1782
|
useSheet,
|
|
1608
|
-
useStacksheetState
|
|
1609
|
-
store
|
|
1783
|
+
useStacksheetState
|
|
1610
1784
|
};
|
|
1611
|
-
}
|
|
1785
|
+
};
|
|
1612
1786
|
//#endregion
|
|
1613
|
-
//#region src/SheetParts/
|
|
1614
|
-
|
|
1787
|
+
//#region src/SheetParts/sheet-back.tsx
|
|
1788
|
+
const SheetBack = ({ asChild, className, style, children }) => {
|
|
1615
1789
|
const { back, isNested } = useSheetPanel();
|
|
1616
1790
|
if (!isNested) return null;
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1791
|
+
const isAsChild = asChild === true;
|
|
1792
|
+
return /* @__PURE__ */ jsx(isAsChild ? Slot : "button", {
|
|
1793
|
+
"aria-label": children === void 0 || children === null ? "Back" : void 0,
|
|
1794
|
+
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),
|
|
1620
1795
|
onClick: back,
|
|
1621
1796
|
style,
|
|
1622
|
-
type:
|
|
1797
|
+
type: isAsChild ? void 0 : "button",
|
|
1623
1798
|
children: children ?? /* @__PURE__ */ jsx(ArrowLeftIcon, {})
|
|
1624
1799
|
});
|
|
1625
|
-
}
|
|
1800
|
+
};
|
|
1626
1801
|
//#endregion
|
|
1627
|
-
//#region src/SheetParts/
|
|
1628
|
-
|
|
1629
|
-
if (asChild) return /* @__PURE__ */ jsx(Slot, {
|
|
1802
|
+
//#region src/SheetParts/sheet-body.tsx
|
|
1803
|
+
const SheetBody = ({ asChild, className, style, children }) => {
|
|
1804
|
+
if (asChild === true) return /* @__PURE__ */ jsx(Slot, {
|
|
1630
1805
|
className: joinClassNames("relative min-h-0 flex-1", className),
|
|
1631
1806
|
"data-stacksheet-no-drag": "",
|
|
1632
1807
|
style,
|
|
@@ -1645,88 +1820,86 @@ function SheetBody({ asChild, className, style, children }) {
|
|
|
1645
1820
|
children: /* @__PURE__ */ jsx(Thumb, { className: "relative flex-1 rounded bg-current/15" })
|
|
1646
1821
|
})]
|
|
1647
1822
|
});
|
|
1648
|
-
}
|
|
1823
|
+
};
|
|
1649
1824
|
//#endregion
|
|
1650
|
-
//#region src/SheetParts/
|
|
1651
|
-
|
|
1825
|
+
//#region src/SheetParts/sheet-close.tsx
|
|
1826
|
+
const SheetClose = ({ asChild, className, style, children }) => {
|
|
1652
1827
|
const { close } = useSheetPanel();
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1828
|
+
const isAsChild = asChild === true;
|
|
1829
|
+
return /* @__PURE__ */ jsx(isAsChild ? Slot : "button", {
|
|
1830
|
+
"aria-label": children === void 0 || children === null ? "Close" : void 0,
|
|
1831
|
+
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),
|
|
1656
1832
|
onClick: close,
|
|
1657
1833
|
style,
|
|
1658
|
-
type:
|
|
1834
|
+
type: isAsChild ? void 0 : "button",
|
|
1659
1835
|
children: children ?? /* @__PURE__ */ jsx(XIcon, {})
|
|
1660
1836
|
});
|
|
1661
|
-
}
|
|
1837
|
+
};
|
|
1662
1838
|
//#endregion
|
|
1663
|
-
//#region src/SheetParts/
|
|
1664
|
-
|
|
1839
|
+
//#region src/SheetParts/sheet-description.tsx
|
|
1840
|
+
const SheetDescription = ({ asChild, className, style, children }) => {
|
|
1665
1841
|
const { panelId, registerDescription } = useSheetPanel();
|
|
1666
1842
|
useEffect(() => registerDescription(), [registerDescription]);
|
|
1667
|
-
return /* @__PURE__ */ jsx(asChild ? Slot : "p", {
|
|
1843
|
+
return /* @__PURE__ */ jsx(asChild === true ? Slot : "p", {
|
|
1668
1844
|
className,
|
|
1669
1845
|
id: `${panelId}-desc`,
|
|
1670
1846
|
style,
|
|
1671
1847
|
children
|
|
1672
1848
|
});
|
|
1673
|
-
}
|
|
1849
|
+
};
|
|
1674
1850
|
//#endregion
|
|
1675
|
-
//#region src/SheetParts/
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1851
|
+
//#region src/SheetParts/sheet-footer.tsx
|
|
1852
|
+
const SheetFooter = ({ asChild, className, style, children }) => {
|
|
1853
|
+
const isAsChild = asChild === true;
|
|
1854
|
+
return /* @__PURE__ */ jsx(isAsChild ? Slot : "footer", {
|
|
1855
|
+
className: joinClassNames(isAsChild ? "shrink-0" : "flex shrink-0 items-center gap-2 border-t px-6 py-3", className),
|
|
1679
1856
|
style,
|
|
1680
1857
|
children
|
|
1681
1858
|
});
|
|
1682
|
-
}
|
|
1859
|
+
};
|
|
1683
1860
|
//#endregion
|
|
1684
|
-
//#region src/SheetParts/
|
|
1685
|
-
|
|
1861
|
+
//#region src/SheetParts/sheet-handle.tsx
|
|
1862
|
+
const SheetHandle = ({ asChild, className, style, children }) => {
|
|
1686
1863
|
const { close, back, isNested } = useSheetPanel();
|
|
1687
1864
|
const dismiss = isNested ? back : close;
|
|
1688
|
-
|
|
1865
|
+
const isAsChild = asChild === true;
|
|
1866
|
+
return /* @__PURE__ */ jsx(isAsChild ? Slot : "button", {
|
|
1689
1867
|
"aria-label": "Dismiss",
|
|
1690
|
-
className: joinClassNames(
|
|
1868
|
+
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),
|
|
1691
1869
|
"data-stacksheet-handle": "",
|
|
1692
1870
|
onClick: dismiss,
|
|
1693
|
-
onKeyDown: (e) => {
|
|
1694
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
1695
|
-
e.preventDefault();
|
|
1696
|
-
dismiss();
|
|
1697
|
-
}
|
|
1698
|
-
},
|
|
1699
|
-
role: "button",
|
|
1700
1871
|
style,
|
|
1701
|
-
|
|
1872
|
+
type: isAsChild ? void 0 : "button",
|
|
1702
1873
|
children: children ?? /* @__PURE__ */ jsx("div", {
|
|
1703
1874
|
"aria-hidden": "true",
|
|
1704
1875
|
className: "h-1 w-9 rounded-sm bg-current/25"
|
|
1705
1876
|
})
|
|
1706
1877
|
});
|
|
1707
|
-
}
|
|
1878
|
+
};
|
|
1708
1879
|
//#endregion
|
|
1709
|
-
//#region src/SheetParts/
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1880
|
+
//#region src/SheetParts/sheet-header.tsx
|
|
1881
|
+
const SheetHeader = ({ asChild, className, style, children }) => {
|
|
1882
|
+
const isAsChild = asChild === true;
|
|
1883
|
+
return /* @__PURE__ */ jsx(isAsChild ? Slot : "header", {
|
|
1884
|
+
className: joinClassNames(isAsChild ? "shrink-0" : "flex h-14 shrink-0 items-center justify-between border-b px-6", className),
|
|
1713
1885
|
style,
|
|
1714
1886
|
children
|
|
1715
1887
|
});
|
|
1716
|
-
}
|
|
1888
|
+
};
|
|
1717
1889
|
//#endregion
|
|
1718
|
-
//#region src/SheetParts/
|
|
1719
|
-
|
|
1890
|
+
//#region src/SheetParts/sheet-title.tsx
|
|
1891
|
+
const SheetTitle = ({ asChild, className, style, children }) => {
|
|
1720
1892
|
const { panelId } = useSheetPanel();
|
|
1721
|
-
|
|
1722
|
-
|
|
1893
|
+
const isAsChild = asChild === true;
|
|
1894
|
+
return /* @__PURE__ */ jsx(isAsChild ? Slot : "h2", {
|
|
1895
|
+
className: joinClassNames(isAsChild ? void 0 : "font-semibold text-sm", className),
|
|
1723
1896
|
id: `${panelId}-title`,
|
|
1724
1897
|
style,
|
|
1725
1898
|
children
|
|
1726
1899
|
});
|
|
1727
|
-
}
|
|
1900
|
+
};
|
|
1728
1901
|
//#endregion
|
|
1729
|
-
//#region src/SheetParts/
|
|
1902
|
+
//#region src/SheetParts/sheet-parts.ts
|
|
1730
1903
|
/**
|
|
1731
1904
|
* Composable Sheet Parts for building custom Sheet layouts.
|
|
1732
1905
|
*
|