@howells/stacksheet 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # Stacksheet
2
+
3
+ A typed, animated sheet stack for React. Powered by Zustand and Motion.
4
+
5
+ ```
6
+ npm install @howells/stacksheet
7
+ ```
8
+
9
+ Peer dependencies: `react >= 18`, `react-dom >= 18`.
10
+
11
+ ## Quick start
12
+
13
+ ```tsx
14
+ import { createStacksheet } from "@howells/stacksheet";
15
+
16
+ const { StacksheetProvider, useSheet } = createStacksheet();
17
+
18
+ function UserProfile({ userId }: { userId: string }) {
19
+ const { close } = useSheet();
20
+ return (
21
+ <div>
22
+ <h2>User {userId}</h2>
23
+ <button onClick={close}>Done</button>
24
+ </div>
25
+ );
26
+ }
27
+
28
+ function App() {
29
+ return (
30
+ <StacksheetProvider>
31
+ <YourApp />
32
+ </StacksheetProvider>
33
+ );
34
+ }
35
+ ```
36
+
37
+ Open a sheet from any component:
38
+
39
+ ```tsx
40
+ const { open } = useSheet();
41
+ open(UserProfile, { userId: "u_abc" });
42
+ ```
43
+
44
+ No registration, no type map, no config. The sheet slides in from the right on desktop, bottom on mobile, with focus trapping, scroll lock, drag-to-dismiss, and keyboard navigation built in.
45
+
46
+ ## Documentation
47
+
48
+ Full docs, interactive playground, and API reference:
49
+
50
+ **[stacksheet.danielhowells.com](https://stacksheet.danielhowells.com)**
51
+
52
+ - [Getting Started](https://stacksheet.danielhowells.com/docs)
53
+ - [Configuration](https://stacksheet.danielhowells.com/docs/config)
54
+ - [Composable Parts](https://stacksheet.danielhowells.com/docs/composable-parts)
55
+ - [Drag to Dismiss](https://stacksheet.danielhowells.com/docs/drag-to-dismiss)
56
+ - [Styling](https://stacksheet.danielhowells.com/docs/styling)
57
+ - [Type Registry](https://stacksheet.danielhowells.com/docs/type-registry)
58
+
59
+ ## License
60
+
61
+ MIT
package/dist/index.d.ts CHANGED
@@ -1,6 +1,381 @@
1
- export { createSheetStack } from "./create.js";
2
- export { resolveConfig } from "./config.js";
3
- export { getStackTransform, getSlideFrom, getPanelStyles } from "./stacking.js";
4
- export { useIsMobile, useResolvedSide } from "./media.js";
5
- export type { SheetItem, Side, ResponsiveSide, SideConfig, StackingConfig, SpringConfig, SheetStackConfig, ResolvedConfig, SheetContentComponent, ContentMap, SheetSnapshot, SheetActions, SheetStackInstance, } from "./types.js";
6
- //# sourceMappingURL=index.d.ts.map
1
+ import * as zustand from 'zustand';
2
+ import { ComponentType, ReactNode, CSSProperties } from 'react';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+
5
+ /**
6
+ * Spring presets inspired by iOS animation feel.
7
+ *
8
+ * - `soft` — Very gentle, slow settle. Loaders, radial pickers.
9
+ * - `subtle` — Barely noticeable bounce, professional.
10
+ * - `natural` — Balanced, general-purpose default.
11
+ * - `snappy` — Quick, responsive for interactions.
12
+ * - `stiff` — Very quick, controlled. Panels, drawers. **(default)**
13
+ */
14
+ declare const springs: {
15
+ readonly soft: {
16
+ readonly stiffness: 120;
17
+ readonly damping: 18;
18
+ readonly mass: 1;
19
+ };
20
+ readonly subtle: {
21
+ readonly stiffness: 300;
22
+ readonly damping: 30;
23
+ readonly mass: 1;
24
+ };
25
+ readonly natural: {
26
+ readonly stiffness: 200;
27
+ readonly damping: 20;
28
+ readonly mass: 1;
29
+ };
30
+ readonly snappy: {
31
+ readonly stiffness: 400;
32
+ readonly damping: 28;
33
+ readonly mass: 0.8;
34
+ };
35
+ readonly stiff: {
36
+ readonly stiffness: 400;
37
+ readonly damping: 40;
38
+ readonly mass: 1;
39
+ };
40
+ };
41
+ type SpringPreset = keyof typeof springs;
42
+
43
+ interface StacksheetClassNames {
44
+ /** Applied to backdrop overlay */
45
+ backdrop?: string;
46
+ /** Applied to each panel container */
47
+ panel?: string;
48
+ /** Applied to the header bar */
49
+ header?: string;
50
+ }
51
+ interface HeaderRenderProps {
52
+ /** `true` when the stack has more than one sheet */
53
+ isNested: boolean;
54
+ /** Pop the top sheet (go back one level) */
55
+ onBack: () => void;
56
+ /** Close the entire sheet stack */
57
+ onClose: () => void;
58
+ /** Current resolved side (left/right/bottom) */
59
+ side: Side;
60
+ }
61
+ interface SheetItem<TType extends string = string> {
62
+ /** Unique identifier for this sheet instance */
63
+ id: string;
64
+ /** Sheet type key from the TMap */
65
+ type: TType;
66
+ /** Data payload passed when opening the sheet */
67
+ data: Record<string, unknown>;
68
+ }
69
+ type Side = "left" | "right" | "bottom";
70
+ interface ResponsiveSide {
71
+ desktop: Side;
72
+ mobile: Side;
73
+ }
74
+ type SideConfig = Side | ResponsiveSide;
75
+ interface StackingConfig {
76
+ /** Scale reduction per depth level (default: 0.04) */
77
+ scaleStep: number;
78
+ /** Horizontal/vertical offset per depth level in px (default: 24) */
79
+ offsetStep: number;
80
+ /** Opacity reduction per depth level (default: 0) */
81
+ opacityStep: number;
82
+ /** Border radius applied to stacked panels in px (default: 12) */
83
+ radius: number;
84
+ /** Max depth before content stops rendering (default: 5) */
85
+ renderThreshold: number;
86
+ }
87
+ interface SpringConfig {
88
+ /** Damping — higher = less oscillation (default: 30) */
89
+ damping: number;
90
+ /** Stiffness — higher = snappier (default: 170) */
91
+ stiffness: number;
92
+ /** Mass — higher = more momentum (default: 0.8) */
93
+ mass: number;
94
+ }
95
+ interface StacksheetConfig {
96
+ /** Maximum stack depth. Default: Infinity (unlimited) */
97
+ maxDepth?: number;
98
+ /** Close on ESC key. Default: true */
99
+ closeOnEscape?: boolean;
100
+ /** Close on backdrop click. Default: true */
101
+ closeOnBackdrop?: boolean;
102
+ /** Show backdrop overlay when open. Default: true */
103
+ showOverlay?: boolean;
104
+ /** Lock body scroll when open. Default: true */
105
+ lockScroll?: boolean;
106
+ /** Panel width in px. Default: 420 */
107
+ width?: number;
108
+ /** Maximum panel width as CSS value. Default: "90vw" */
109
+ maxWidth?: string;
110
+ /** Mobile breakpoint in px. Default: 768 */
111
+ breakpoint?: number;
112
+ /** Sheet slide-from side. Default: { desktop: "right", mobile: "bottom" } */
113
+ side?: SideConfig;
114
+ /** Stacking visual parameters */
115
+ stacking?: Partial<StackingConfig>;
116
+ /** Spring animation parameters — preset name or custom config */
117
+ spring?: SpringPreset | Partial<SpringConfig>;
118
+ /** Base z-index. Default: 100 */
119
+ zIndex?: number;
120
+ /** Default aria-label for dialog panels. Default: "Sheet dialog" */
121
+ ariaLabel?: string;
122
+ /** Called when the top panel's entrance animation completes */
123
+ onOpenComplete?: () => void;
124
+ /** Called when the last panel's exit animation completes (stack fully closed) */
125
+ onCloseComplete?: () => void;
126
+ /** Enable drag-to-dismiss. Default: true */
127
+ drag?: boolean;
128
+ /** Fraction of panel dimension to trigger close (0-1). Default: 0.25 */
129
+ closeThreshold?: number;
130
+ /** Velocity threshold (px/ms) to trigger close. Default: 0.5 */
131
+ velocityThreshold?: number;
132
+ /** Allow any form of dismissal (drag, backdrop, escape). Default: true */
133
+ dismissible?: boolean;
134
+ /** Modal mode — overlay + scroll lock + focus trap. Default: true */
135
+ modal?: boolean;
136
+ /** Scale down [data-stacksheet-wrapper] when sheets open. Default: false */
137
+ shouldScaleBackground?: boolean;
138
+ /** Scale factor applied to background (0-1). Default: 0.97 */
139
+ scaleBackgroundAmount?: number;
140
+ }
141
+ /** Fully resolved config — all fields required */
142
+ interface ResolvedConfig {
143
+ maxDepth: number;
144
+ closeOnEscape: boolean;
145
+ closeOnBackdrop: boolean;
146
+ showOverlay: boolean;
147
+ lockScroll: boolean;
148
+ width: number;
149
+ maxWidth: string;
150
+ breakpoint: number;
151
+ side: ResponsiveSide;
152
+ stacking: StackingConfig;
153
+ spring: SpringConfig;
154
+ zIndex: number;
155
+ ariaLabel: string;
156
+ onOpenComplete?: () => void;
157
+ onCloseComplete?: () => void;
158
+ drag: boolean;
159
+ closeThreshold: number;
160
+ velocityThreshold: number;
161
+ dismissible: boolean;
162
+ modal: boolean;
163
+ shouldScaleBackground: boolean;
164
+ scaleBackgroundAmount: number;
165
+ }
166
+ /** Component rendered inside a sheet panel — receives data as spread props */
167
+ type SheetContentComponent<TData = unknown> = ComponentType<TData extends Record<string, unknown> ? TData : Record<string, unknown>>;
168
+ /** Map of sheet type → content component */
169
+ type ContentMap<TMap extends Record<string, unknown>> = {
170
+ [K in keyof TMap]: SheetContentComponent<TMap[K]>;
171
+ };
172
+ interface StacksheetSnapshot<TMap extends Record<string, unknown>> {
173
+ /** Current sheet stack, ordered bottom to top */
174
+ stack: SheetItem<Extract<keyof TMap, string>>[];
175
+ /** Whether any sheets are currently visible */
176
+ isOpen: boolean;
177
+ }
178
+ interface SheetActions<TMap extends Record<string, unknown>> {
179
+ /** Replace stack with a single item */
180
+ open<K extends Extract<keyof TMap, string>>(type: K, id: string, data: TMap[K]): void;
181
+ /** Replace stack with an ad-hoc component */
182
+ open<TData extends Record<string, unknown>>(component: ComponentType<TData>, data: TData): void;
183
+ /** Replace stack with an ad-hoc component (explicit id) */
184
+ open<TData extends Record<string, unknown>>(component: ComponentType<TData>, id: string, data: TData): void;
185
+ /** Push onto stack (replaces top at maxDepth) */
186
+ push<K extends Extract<keyof TMap, string>>(type: K, id: string, data: TMap[K]): void;
187
+ /** Push an ad-hoc component onto the stack */
188
+ push<TData extends Record<string, unknown>>(component: ComponentType<TData>, data: TData): void;
189
+ /** Push an ad-hoc component onto the stack (explicit id) */
190
+ push<TData extends Record<string, unknown>>(component: ComponentType<TData>, id: string, data: TData): void;
191
+ /** Swap the top item */
192
+ replace<K extends Extract<keyof TMap, string>>(type: K, id: string, data: TMap[K]): void;
193
+ /** Swap the top item with an ad-hoc component */
194
+ replace<TData extends Record<string, unknown>>(component: ComponentType<TData>, data: TData): void;
195
+ /** Swap the top item with an ad-hoc component (explicit id) */
196
+ replace<TData extends Record<string, unknown>>(component: ComponentType<TData>, id: string, data: TData): void;
197
+ /** Swap the top item's content in place (no animation) */
198
+ swap<K extends Extract<keyof TMap, string>>(type: K, data: TMap[K]): void;
199
+ /** Swap the top item's content with an ad-hoc component (no animation) */
200
+ swap<TData extends Record<string, unknown>>(component: ComponentType<TData>, data: TData): void;
201
+ /** Smart: empty→open, same type on top→replace, different→push */
202
+ navigate<K extends Extract<keyof TMap, string>>(type: K, id: string, data: TMap[K]): void;
203
+ /** Smart navigate with an ad-hoc component */
204
+ navigate<TData extends Record<string, unknown>>(component: ComponentType<TData>, data: TData): void;
205
+ /** Smart navigate with an ad-hoc component (explicit id) */
206
+ navigate<TData extends Record<string, unknown>>(component: ComponentType<TData>, id: string, data: TData): void;
207
+ /** Update data on a sheet by id (no animation) */
208
+ setData<K extends Extract<keyof TMap, string>>(type: K, id: string, data: TMap[K]): void;
209
+ /** Update data on an ad-hoc sheet by id */
210
+ setData<TData extends Record<string, unknown>>(component: ComponentType<TData>, id: string, data: TData): void;
211
+ /** Remove a specific sheet by id; close if last */
212
+ remove(id: string): void;
213
+ /** Pop top item; close if last */
214
+ pop(): void;
215
+ /** Clear entire stack */
216
+ close(): void;
217
+ }
218
+ interface StacksheetProviderProps<TMap extends Record<string, unknown>> {
219
+ /** Map of sheet type keys to content components (optional — only needed for type registry pattern) */
220
+ sheets?: ContentMap<TMap>;
221
+ /** Your application content */
222
+ children: ReactNode;
223
+ /** CSS class overrides for backdrop, panel, and header */
224
+ classNames?: StacksheetClassNames;
225
+ /**
226
+ * Controls the panel header rendering mode.
227
+ * - `undefined` — renders the default close/back header (classic mode)
228
+ * - `function` — custom header renderer (classic mode with custom header)
229
+ * - `false` — no auto header, no auto scroll wrapper (composable mode — use Sheet.* parts)
230
+ */
231
+ renderHeader?: false | ((props: HeaderRenderProps) => ReactNode);
232
+ }
233
+ interface StacksheetInstance<TMap extends Record<string, unknown>> {
234
+ /** Provider component — wrap your app, pass sheets map */
235
+ StacksheetProvider: ComponentType<StacksheetProviderProps<TMap>>;
236
+ /** Hook returning sheet actions */
237
+ useSheet: () => SheetActions<TMap>;
238
+ /** Hook returning sheet state (stack, isOpen) */
239
+ useStacksheetState: () => StacksheetSnapshot<TMap>;
240
+ /** Raw Zustand store for advanced use */
241
+ store: zustand.StoreApi<StacksheetSnapshot<TMap> & SheetActions<TMap>>;
242
+ }
243
+
244
+ declare function resolveConfig(config?: StacksheetConfig): ResolvedConfig;
245
+
246
+ /**
247
+ * Create an isolated sheet stack instance with typed store, hooks, and provider.
248
+ *
249
+ * ```ts
250
+ * const { StacksheetProvider, useSheet, useStacksheetState } = createStacksheet<{
251
+ * "bucket-create": { onCreated?: (b: Bucket) => void };
252
+ * "bucket-edit": { bucket: Bucket };
253
+ * }>();
254
+ * ```
255
+ */
256
+ declare function createStacksheet<TMap extends Record<string, unknown>>(config?: StacksheetConfig): StacksheetInstance<TMap>;
257
+
258
+ /**
259
+ * Returns true when viewport width is at or below the breakpoint.
260
+ * SSR-safe: defaults to false (desktop).
261
+ */
262
+ declare function useIsMobile(breakpoint: number): boolean;
263
+ /** Resolve the current side from config + viewport. */
264
+ declare function useResolvedSide(config: ResolvedConfig): Side;
265
+
266
+ interface SheetPanelContextValue {
267
+ /** Close the entire sheet stack */
268
+ close: () => void;
269
+ /** Pop the top sheet (go back one level) */
270
+ back: () => void;
271
+ /** Whether the stack has more than one sheet */
272
+ isNested: boolean;
273
+ /** Whether this is the top (active) sheet */
274
+ isTop: boolean;
275
+ /** Unique ID prefix for this panel (for aria-labelledby linking) */
276
+ panelId: string;
277
+ /** Current resolved side (left/right/bottom) */
278
+ side: Side;
279
+ }
280
+ /**
281
+ * Access the current sheet panel's context.
282
+ * Must be called inside a component rendered by the sheet stack.
283
+ */
284
+ declare function useSheetPanel(): SheetPanelContextValue;
285
+
286
+ interface SheetHandleProps {
287
+ /** Render as child element, merging props */
288
+ asChild?: boolean;
289
+ className?: string;
290
+ style?: CSSProperties;
291
+ /** Custom handle content. Defaults to a centered grab bar. */
292
+ children?: ReactNode;
293
+ }
294
+ declare function SheetHandle({ asChild, className, style, children, }: SheetHandleProps): react_jsx_runtime.JSX.Element;
295
+ interface SheetHeaderProps {
296
+ asChild?: boolean;
297
+ className?: string;
298
+ style?: CSSProperties;
299
+ children: ReactNode;
300
+ }
301
+ declare function SheetHeader({ asChild, className, style, children, }: SheetHeaderProps): react_jsx_runtime.JSX.Element;
302
+ interface SheetTitleProps {
303
+ asChild?: boolean;
304
+ className?: string;
305
+ style?: CSSProperties;
306
+ children: ReactNode;
307
+ }
308
+ declare function SheetTitle({ asChild, className, style, children }: SheetTitleProps): react_jsx_runtime.JSX.Element;
309
+ interface SheetDescriptionProps {
310
+ asChild?: boolean;
311
+ className?: string;
312
+ style?: CSSProperties;
313
+ children: ReactNode;
314
+ }
315
+ declare function SheetDescription({ asChild, className, style, children, }: SheetDescriptionProps): react_jsx_runtime.JSX.Element;
316
+ interface SheetBodyProps {
317
+ /** When true, renders child element directly instead of ScrollArea */
318
+ asChild?: boolean;
319
+ className?: string;
320
+ style?: CSSProperties;
321
+ children: ReactNode;
322
+ }
323
+ declare function SheetBody({ asChild, className, style, children }: SheetBodyProps): react_jsx_runtime.JSX.Element;
324
+ interface SheetFooterProps {
325
+ asChild?: boolean;
326
+ className?: string;
327
+ style?: CSSProperties;
328
+ children: ReactNode;
329
+ }
330
+ declare function SheetFooter({ asChild, className, style, children, }: SheetFooterProps): react_jsx_runtime.JSX.Element;
331
+ interface SheetCloseProps {
332
+ asChild?: boolean;
333
+ className?: string;
334
+ style?: CSSProperties;
335
+ /** Custom content. Defaults to an X icon. */
336
+ children?: ReactNode;
337
+ }
338
+ declare function SheetClose({ asChild, className, style, children }: SheetCloseProps): react_jsx_runtime.JSX.Element;
339
+ interface SheetBackProps {
340
+ asChild?: boolean;
341
+ className?: string;
342
+ style?: CSSProperties;
343
+ /** Custom content. Defaults to an arrow-left icon. */
344
+ children?: ReactNode;
345
+ }
346
+ declare function SheetBack({ asChild, className, style, children }: SheetBackProps): react_jsx_runtime.JSX.Element | null;
347
+ declare const Sheet: {
348
+ readonly Handle: typeof SheetHandle;
349
+ readonly Header: typeof SheetHeader;
350
+ readonly Title: typeof SheetTitle;
351
+ readonly Description: typeof SheetDescription;
352
+ readonly Body: typeof SheetBody;
353
+ readonly Footer: typeof SheetFooter;
354
+ readonly Close: typeof SheetClose;
355
+ readonly Back: typeof SheetBack;
356
+ };
357
+
358
+ interface StackTransform {
359
+ scale: number;
360
+ offset: number;
361
+ opacity: number;
362
+ borderRadius: number;
363
+ }
364
+ /**
365
+ * Compute visual transforms for a panel at a given depth.
366
+ * depth=0 is the top (foreground) panel.
367
+ * Panels beyond renderThreshold are clamped to the edge position and faded out.
368
+ */
369
+ declare function getStackTransform(depth: number, stacking: StackingConfig): StackTransform;
370
+ interface SlideValues {
371
+ x?: string | number;
372
+ y?: string | number;
373
+ }
374
+ /** Motion initial/exit values for sliding from the given side. */
375
+ declare function getSlideFrom(side: Side): SlideValues;
376
+ /**
377
+ * Fixed-position styles for a panel, accounting for side, width, and depth.
378
+ */
379
+ declare function getPanelStyles(side: Side, config: ResolvedConfig, _depth: number, index: number): CSSProperties;
380
+
381
+ export { type ContentMap, type HeaderRenderProps, type ResolvedConfig, type ResponsiveSide, Sheet, type SheetActions, type SheetBackProps, type SheetBodyProps, type SheetCloseProps, type SheetContentComponent, type SheetDescriptionProps, type SheetFooterProps, type SheetHandleProps, type SheetHeaderProps, type SheetItem, type SheetPanelContextValue, type SheetTitleProps, type Side, type SideConfig, type SpringConfig, type SpringPreset, type StackingConfig, type StacksheetClassNames, type StacksheetConfig, type StacksheetInstance, type StacksheetProviderProps, type StacksheetSnapshot, createStacksheet, getPanelStyles, getSlideFrom, getStackTransform, resolveConfig, springs, useIsMobile, useResolvedSide, useSheetPanel };