@slithy/modal-spring 0.1.2

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,102 @@
1
+ # @slithy/modal-spring
2
+
3
+ Animated modal adapter for `@slithy/modal-kit`, built on react-spring and @use-gesture/react.
4
+
5
+ Provides animated enter/leave transitions, an animated backdrop, and drag-to-close gesture support. For full usage documentation see [`docs/modal-implementation-guide.md`](../../docs/modal-implementation-guide.md).
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pnpm add @slithy/modal-core @slithy/modal-kit @slithy/modal-spring
13
+ pnpm add @react-spring/web @use-gesture/react
14
+ ```
15
+
16
+ `@react-spring/web` and `@use-gesture/react` are peer dependencies.
17
+
18
+ ---
19
+
20
+ ## Setup
21
+
22
+ ```tsx
23
+ import { LayerProvider, LayerStackPriority } from '@slithy/layers'
24
+ import { Portal } from '@slithy/portal'
25
+ import { SpringModalRenderer } from '@slithy/modal-spring'
26
+
27
+ export function App() {
28
+ return (
29
+ <LayerProvider id="app" zIndex={LayerStackPriority.Base}>
30
+ <main>{/* your app */}</main>
31
+ <SpringModalRenderer
32
+ renderLayer={(children) => (
33
+ <LayerProvider id="modal" zIndex={LayerStackPriority.Modal}>
34
+ {children}
35
+ </LayerProvider>
36
+ )}
37
+ renderPortal={(children) => <Portal>{children}</Portal>}
38
+ />
39
+ </LayerProvider>
40
+ )
41
+ }
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Usage
47
+
48
+ ```tsx
49
+ import { useModalStore } from '@slithy/modal-core'
50
+ import { SpringModal } from '@slithy/modal-spring'
51
+
52
+ function MyButton() {
53
+ const open = (event: React.MouseEvent) => {
54
+ useModalStore.getState().openModal(
55
+ <SpringModal aria-label="My Modal">
56
+ <p>Content</p>
57
+ </SpringModal>,
58
+ { triggerEvent: event }
59
+ )
60
+ }
61
+ return <button onClick={open}>Open</button>
62
+ }
63
+ ```
64
+
65
+ ---
66
+
67
+ ## SpringModal Props
68
+
69
+ | Prop | Type | Default | Description |
70
+ |---|---|---|---|
71
+ | `aria-label` | `string` | — | Accessible name for the dialog |
72
+ | `alignX` | `'center' \| 'left' \| 'right'` | `'center'` | Horizontal position |
73
+ | `alignY` | `'middle' \| 'top' \| 'bottom'` | `'middle'` | Vertical position |
74
+ | `dismissible` | `boolean` | `true` | Allow Escape and backdrop-click to close |
75
+ | `contentClassName` | `string` | — | Class on the `<dialog>` element |
76
+ | `contentStyle` | `CSSProperties` | — | Static styles on the `<dialog>` element |
77
+ | `contentTransitions` | `{ from, enter, leave }` | — | Spring transition values |
78
+ | `disableOpacityTransition` | `boolean` | — | Skip the default opacity fade |
79
+ | `dragDirection` | `DragDirection` | — | Enable drag-to-close |
80
+ | `containerScrolling` | `boolean` | `true` | Allow container scroll |
81
+ | `layerIsActive` | `boolean` | `true` | Pass from `useLayerState` for layer coordination |
82
+ | `springConfig` | `SpringConfig` | — | Override the default spring config |
83
+ | `afterOpen` | `() => void` | — | Fires after enter animation completes |
84
+ | `afterClose` | `() => void` | — | Fires after modal is removed |
85
+
86
+ ---
87
+
88
+ ## Exports
89
+
90
+ | Export | Description |
91
+ |---|---|
92
+ | `SpringModal` | Animated modal component |
93
+ | `SpringModalRenderer` | Renders all open modals with animated backdrop |
94
+ | `DragHandle` | Drag-to-close gesture wrapper |
95
+ | `useModalDrag` | Low-level drag hook |
96
+ | `Backdrop` | Animated backdrop (used by `SpringModalRenderer`) |
97
+ | `defaultSpring` | Default spring config |
98
+ | `iosSheetSpring` | iOS-feel spring config |
99
+ | `useModalDragging` | Internal drag spring hook |
100
+ | `DragDirection` | `'up' \| 'down' \| 'left' \| 'right'` |
101
+ | `DragStyles` | Spring transform values from drag |
102
+ | `SpringModalProps` | — |
@@ -0,0 +1,111 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { SpringValues, SpringConfig, SpringValue, UseTransitionProps } from '@react-spring/web';
3
+ import { CSSProperties, RefObject, DOMAttributes } from 'react';
4
+ import { ModalProps as ModalProps$1, ModalRendererProps as ModalRendererProps$1 } from '@slithy/modal-kit';
5
+ import { ModalElement } from '@slithy/modal-core';
6
+
7
+ type BackdropProps = {
8
+ className?: string;
9
+ onClick?: () => void;
10
+ /** Static CSS overrides applied alongside the animated spring styles. */
11
+ style?: CSSProperties;
12
+ /** Animated spring values (or plain CSSProperties for static use). */
13
+ styles?: SpringValues<CSSProperties> | CSSProperties;
14
+ };
15
+ declare const ModalBackdrop: ({ className, onClick, style, styles, }: BackdropProps) => react_jsx_runtime.JSX.Element;
16
+
17
+ /** Default spring for modal enter/leave animations. */
18
+ declare const defaultSpring: SpringConfig;
19
+ /**
20
+ * iOS Sheet / Ionic Framework easing curve.
21
+ * 500ms duration — well-suited for bottom-sheet style drawers.
22
+ * cubic-bezier(0.32, 0.72, 0, 1)
23
+ */
24
+ declare const iosSheetSpring: SpringConfig;
25
+
26
+ type DragDirection = 'down' | 'up' | 'left' | 'right';
27
+ type DragStyles = {
28
+ x?: SpringValue<number>;
29
+ y?: SpringValue<number>;
30
+ };
31
+ type UseModalDraggingResult = {
32
+ bind: () => DOMAttributes<HTMLElement>;
33
+ dragStyles: DragStyles;
34
+ };
35
+ declare const useModalDragging: (modalId: ModalElement["id"] | undefined, dragDirection?: DragDirection, cardRef?: RefObject<HTMLElement | null>, dragDismissedRef?: RefObject<boolean>, onFlyOutRest?: () => void) => UseModalDraggingResult;
36
+
37
+ type DragContextValue = {
38
+ bind: () => DOMAttributes<HTMLElement>;
39
+ active: boolean;
40
+ };
41
+ /**
42
+ * Returns the drag bind function for attaching drag-to-close to a handle
43
+ * element within a SpringModal.
44
+ *
45
+ * @example
46
+ * const { bind } = useModalDrag()
47
+ * return <div {...bind()}>Drag handle</div>
48
+ */
49
+ declare function useModalDrag(): DragContextValue;
50
+ /**
51
+ * A wrapper component that applies drag-to-close behavior to its children.
52
+ * Must be rendered inside SpringModal's children.
53
+ *
54
+ * @example
55
+ * <SpringModal>
56
+ * <DragHandle>
57
+ * <Header>Title</Header>
58
+ * </DragHandle>
59
+ * </SpringModal>
60
+ */
61
+ declare const DragHandle: ({ children, className, enabled, }: {
62
+ children?: React.ReactNode;
63
+ className?: string;
64
+ enabled?: boolean;
65
+ }) => react_jsx_runtime.JSX.Element;
66
+
67
+ type ModalProps = {
68
+ "aria-label"?: string;
69
+ afterClose?: () => void;
70
+ afterOpen?: () => void;
71
+ alignX?: ModalProps$1["alignX"];
72
+ alignY?: ModalProps$1["alignY"];
73
+ children?: React.ReactNode;
74
+ containerScrolling?: boolean;
75
+ contentClassName?: string;
76
+ contentStyle?: CSSProperties;
77
+ /**
78
+ * Spring transition values for the enter/from/leave animation phases.
79
+ *
80
+ * This prop accepts an object, so define it outside the component (or with
81
+ * `useMemo`) to keep the reference stable across renders.
82
+ *
83
+ * @example
84
+ * const transitions = { from: { y: '100%' }, enter: { y: '0%' }, leave: { y: '100%' } }
85
+ * <SpringModal contentTransitions={transitions} ... />
86
+ */
87
+ contentTransitions?: Pick<UseTransitionProps, "from" | "enter" | "leave">;
88
+ disableOpacityTransition?: boolean;
89
+ dismissible?: boolean;
90
+ dragDirection?: DragDirection;
91
+ /**
92
+ * Whether the modal's layer is currently active. Pass a value from
93
+ * `useLayerState` when using `@slithy/layers` for full layer-stack
94
+ * coordination. Defaults to `true`.
95
+ */
96
+ layerIsActive?: boolean;
97
+ springConfig?: SpringConfig;
98
+ };
99
+ declare const Modal: ({ "aria-label": ariaLabel, afterClose, afterOpen, alignX, alignY, children, containerScrolling, contentClassName, contentStyle, contentTransitions, disableOpacityTransition, dismissible, dragDirection, layerIsActive: layerIsActiveProp, springConfig, }: ModalProps) => JSX.Element;
100
+
101
+ type ModalRendererProps = Pick<ModalRendererProps$1, "renderLayer" | "renderPortal"> & {
102
+ /**
103
+ * @deprecated `ModalRenderer` manages its own animated backdrop internally.
104
+ * This prop is accepted for interface parity with `@slithy/modal-kit`'s
105
+ * `ModalRenderer` but has no effect.
106
+ */
107
+ renderBackdrop?: ModalRendererProps$1["renderBackdrop"];
108
+ };
109
+ declare const ModalRenderer: ({ renderLayer, renderPortal, }?: ModalRendererProps) => react_jsx_runtime.JSX.Element;
110
+
111
+ export { type DragDirection, DragHandle, type DragStyles, Modal, ModalBackdrop, type ModalProps, ModalRenderer, defaultSpring, iosSheetSpring, useModalDrag, useModalDragging };
package/dist/index.js ADDED
@@ -0,0 +1,600 @@
1
+ // src/animated.tsx
2
+ import { animated } from "@react-spring/web";
3
+ import { ModalBackdrop, ModalContent, ModalDialog } from "@slithy/modal-kit";
4
+ import { forwardRef } from "react";
5
+ import { jsx } from "react/jsx-runtime";
6
+ var DivBase = forwardRef(
7
+ (props, ref) => /* @__PURE__ */ jsx("div", { ref, ...props })
8
+ );
9
+ var AnimatedDiv = animated(DivBase);
10
+ var AnimatedBackdrop = animated(ModalBackdrop);
11
+ var AnimatedModalContent = animated(ModalContent);
12
+ var ModalDialogBase = forwardRef(
13
+ (props, ref) => /* @__PURE__ */ jsx(ModalDialog, { ref, ...props })
14
+ );
15
+ var AnimatedModalDialog = animated(ModalDialogBase);
16
+
17
+ // src/ModalBackdrop.tsx
18
+ import { jsx as jsx2 } from "react/jsx-runtime";
19
+ var ModalBackdrop2 = ({
20
+ className,
21
+ onClick,
22
+ style,
23
+ styles
24
+ }) => /* @__PURE__ */ jsx2(
25
+ AnimatedBackdrop,
26
+ {
27
+ className,
28
+ onClick,
29
+ style: { ...style, ...styles }
30
+ }
31
+ );
32
+
33
+ // src/spring-configs.ts
34
+ function cubicBezier(x1, y1, x2, y2) {
35
+ const cx = 3 * x1;
36
+ const bx = 3 * (x2 - x1) - cx;
37
+ const ax = 1 - cx - bx;
38
+ const cy = 3 * y1;
39
+ const by = 3 * (y2 - y1) - cy;
40
+ const ay = 1 - cy - by;
41
+ const sampleX = (t) => ((ax * t + bx) * t + cx) * t;
42
+ const sampleY = (t) => ((ay * t + by) * t + cy) * t;
43
+ const sampleDerivativeX = (t) => (3 * ax * t + 2 * bx) * t + cx;
44
+ const solve = (x) => {
45
+ let t = x;
46
+ for (let i = 0; i < 8; i++) {
47
+ const dx = sampleX(t) - x;
48
+ if (Math.abs(dx) < 1e-7) return t;
49
+ const d = sampleDerivativeX(t);
50
+ if (Math.abs(d) < 1e-6) break;
51
+ t -= dx / d;
52
+ }
53
+ return t;
54
+ };
55
+ return (x) => sampleY(solve(x));
56
+ }
57
+ var defaultSpring = {
58
+ friction: 200,
59
+ mass: 5,
60
+ tension: 2e3
61
+ };
62
+ var iosSheetSpring = {
63
+ duration: 500,
64
+ easing: cubicBezier(0.32, 0.72, 0, 1)
65
+ };
66
+
67
+ // src/DragContext.tsx
68
+ import { createContext, useContext, useMemo as useMemo2 } from "react";
69
+
70
+ // src/useModalDragging.ts
71
+ import { useMemo, useRef } from "react";
72
+ import { useSpring } from "@react-spring/web";
73
+ import { useDrag } from "@use-gesture/react";
74
+ import { useModalStore } from "@slithy/modal-core";
75
+ var snapBackSpring = { friction: 40, tension: 300, mass: 1 };
76
+ var flyOutSpring = { friction: 20, tension: 150, mass: 1, clamp: true };
77
+ var noopModalDragging = {
78
+ bind: () => ({}),
79
+ dragStyles: {}
80
+ };
81
+ var useModalDragging = (modalId, dragDirection = "down", cardRef, dragDismissedRef, onFlyOutRest) => {
82
+ const closeModal = useModalStore((state) => state.closeModal);
83
+ const isXAxis = dragDirection === "left" || dragDirection === "right";
84
+ const axis = isXAxis ? "x" : "y";
85
+ const bounds = useMemo(
86
+ () => dragDirection === "down" ? { top: 0 } : dragDirection === "up" ? { bottom: 0 } : dragDirection === "right" ? { left: 0 } : { right: 0 },
87
+ [dragDirection]
88
+ );
89
+ const [dragStyles, api] = useSpring(() => ({
90
+ config: snapBackSpring,
91
+ x: 0,
92
+ y: 0
93
+ }));
94
+ const multiTouchCancelledRef = useRef(false);
95
+ const bind = useDrag(
96
+ ({ down, touches, offset: [offsetX, offsetY], velocity: [velocityX, velocityY], direction: [dirX, dirY] }) => {
97
+ if (!modalId) return;
98
+ if (touches > 1) {
99
+ multiTouchCancelledRef.current = true;
100
+ api.start({ config: snapBackSpring, immediate: true, x: 0, y: 0 });
101
+ return;
102
+ }
103
+ if (multiTouchCancelledRef.current) {
104
+ if (!down) multiTouchCancelledRef.current = false;
105
+ return;
106
+ }
107
+ const offset = isXAxis ? offsetX : offsetY;
108
+ const velocity = isXAxis ? velocityX : velocityY;
109
+ const dir = isXAxis ? dirX : dirY;
110
+ const isOutward = offset > 0 ? dragDirection === "down" || dragDirection === "right" : offset < 0 ? dragDirection === "up" || dragDirection === "left" : false;
111
+ const isMovingOutward = dragDirection === "down" ? dir > 0 : dragDirection === "up" ? dir < 0 : dragDirection === "right" ? dir > 0 : dir < 0;
112
+ const cardDimension = isXAxis ? cardRef?.current?.offsetWidth ?? 0 : cardRef?.current?.offsetHeight ?? 0;
113
+ const isDismissing = !down && isOutward && (isMovingOutward && velocity >= 0.5 || cardDimension > 0 && Math.abs(offset) >= cardDimension * 0.5);
114
+ if (isDismissing) {
115
+ if (dragDismissedRef) dragDismissedRef.current = true;
116
+ closeModal(modalId);
117
+ api.start({
118
+ config: flyOutSpring,
119
+ x: dragDirection === "right" ? window.innerWidth : dragDirection === "left" ? -window.innerWidth : 0,
120
+ y: dragDirection === "down" ? window.innerHeight : dragDirection === "up" ? -window.innerHeight : 0,
121
+ onRest: onFlyOutRest
122
+ });
123
+ } else {
124
+ api.start({
125
+ config: snapBackSpring,
126
+ immediate: down,
127
+ x: isXAxis ? down ? offsetX : 0 : 0,
128
+ y: !isXAxis ? down ? offsetY : 0 : 0
129
+ });
130
+ }
131
+ },
132
+ { axis, bounds, filterTaps: true, from: () => [dragStyles.x.get(), dragStyles.y.get()], rubberband: 0 }
133
+ );
134
+ return useMemo(
135
+ () => modalId ? { bind, dragStyles } : noopModalDragging,
136
+ [modalId, bind, dragStyles]
137
+ );
138
+ };
139
+
140
+ // #style-inject:#style-inject
141
+ function styleInject(css, { insertAt } = {}) {
142
+ if (!css || typeof document === "undefined") return;
143
+ const head = document.head || document.getElementsByTagName("head")[0];
144
+ const style = document.createElement("style");
145
+ style.type = "text/css";
146
+ if (insertAt === "top") {
147
+ if (head.firstChild) {
148
+ head.insertBefore(style, head.firstChild);
149
+ } else {
150
+ head.appendChild(style);
151
+ }
152
+ } else {
153
+ head.appendChild(style);
154
+ }
155
+ if (style.styleSheet) {
156
+ style.styleSheet.cssText = css;
157
+ } else {
158
+ style.appendChild(document.createTextNode(css));
159
+ }
160
+ }
161
+
162
+ // src/DragHandle.css
163
+ styleInject("[data-slithy=drag-handle][data-enabled] {\n cursor: grab;\n user-select: none;\n -webkit-user-select: none;\n}\n");
164
+
165
+ // src/DragContext.tsx
166
+ import { jsx as jsx3 } from "react/jsx-runtime";
167
+ var DragContext = createContext({ bind: () => ({}), active: false });
168
+ var ModalDraggingSlot = ({
169
+ modalId,
170
+ dragDirection,
171
+ cardRef,
172
+ dragDismissedRef,
173
+ onFlyOutRest,
174
+ children
175
+ }) => {
176
+ const { bind, dragStyles } = useModalDragging(modalId, dragDirection, cardRef, dragDismissedRef, onFlyOutRest);
177
+ const contextValue = useMemo2(() => ({ bind, active: true }), [bind]);
178
+ return /* @__PURE__ */ jsx3(DragContext.Provider, { value: contextValue, children: children({ dragStyles }) });
179
+ };
180
+ function useModalDrag() {
181
+ return useContext(DragContext);
182
+ }
183
+ var DragHandle = ({
184
+ children,
185
+ className,
186
+ enabled = true
187
+ }) => {
188
+ const { bind, active } = useModalDrag();
189
+ const isActive = enabled && active;
190
+ return /* @__PURE__ */ jsx3(
191
+ "div",
192
+ {
193
+ className,
194
+ "data-slithy": "drag-handle",
195
+ "data-enabled": enabled || void 0,
196
+ ...isActive ? bind() : {},
197
+ children
198
+ }
199
+ );
200
+ };
201
+
202
+ // ../../node_modules/.pnpm/@react-spring+rafz@9.7.5/node_modules/@react-spring/rafz/dist/react-spring_rafz.modern.mjs
203
+ var updateQueue = makeQueue();
204
+ var raf = (fn) => schedule(fn, updateQueue);
205
+ var writeQueue = makeQueue();
206
+ raf.write = (fn) => schedule(fn, writeQueue);
207
+ var onStartQueue = makeQueue();
208
+ raf.onStart = (fn) => schedule(fn, onStartQueue);
209
+ var onFrameQueue = makeQueue();
210
+ raf.onFrame = (fn) => schedule(fn, onFrameQueue);
211
+ var onFinishQueue = makeQueue();
212
+ raf.onFinish = (fn) => schedule(fn, onFinishQueue);
213
+ var timeouts = [];
214
+ raf.setTimeout = (handler, ms) => {
215
+ const time = raf.now() + ms;
216
+ const cancel = () => {
217
+ const i = timeouts.findIndex((t) => t.cancel == cancel);
218
+ if (~i)
219
+ timeouts.splice(i, 1);
220
+ pendingCount -= ~i ? 1 : 0;
221
+ };
222
+ const timeout = { time, handler, cancel };
223
+ timeouts.splice(findTimeout(time), 0, timeout);
224
+ pendingCount += 1;
225
+ start();
226
+ return timeout;
227
+ };
228
+ var findTimeout = (time) => ~(~timeouts.findIndex((t) => t.time > time) || ~timeouts.length);
229
+ raf.cancel = (fn) => {
230
+ onStartQueue.delete(fn);
231
+ onFrameQueue.delete(fn);
232
+ onFinishQueue.delete(fn);
233
+ updateQueue.delete(fn);
234
+ writeQueue.delete(fn);
235
+ };
236
+ raf.sync = (fn) => {
237
+ sync = true;
238
+ raf.batchedUpdates(fn);
239
+ sync = false;
240
+ };
241
+ raf.throttle = (fn) => {
242
+ let lastArgs;
243
+ function queuedFn() {
244
+ try {
245
+ fn(...lastArgs);
246
+ } finally {
247
+ lastArgs = null;
248
+ }
249
+ }
250
+ function throttled(...args) {
251
+ lastArgs = args;
252
+ raf.onStart(queuedFn);
253
+ }
254
+ throttled.handler = fn;
255
+ throttled.cancel = () => {
256
+ onStartQueue.delete(queuedFn);
257
+ lastArgs = null;
258
+ };
259
+ return throttled;
260
+ };
261
+ var nativeRaf = typeof window != "undefined" ? window.requestAnimationFrame : (
262
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
263
+ (() => {
264
+ })
265
+ );
266
+ raf.use = (impl) => nativeRaf = impl;
267
+ raf.now = typeof performance != "undefined" ? () => performance.now() : Date.now;
268
+ raf.batchedUpdates = (fn) => fn();
269
+ raf.catch = console.error;
270
+ raf.frameLoop = "always";
271
+ raf.advance = () => {
272
+ if (raf.frameLoop !== "demand") {
273
+ console.warn(
274
+ "Cannot call the manual advancement of rafz whilst frameLoop is not set as demand"
275
+ );
276
+ } else {
277
+ update();
278
+ }
279
+ };
280
+ var ts = -1;
281
+ var pendingCount = 0;
282
+ var sync = false;
283
+ function schedule(fn, queue) {
284
+ if (sync) {
285
+ queue.delete(fn);
286
+ fn(0);
287
+ } else {
288
+ queue.add(fn);
289
+ start();
290
+ }
291
+ }
292
+ function start() {
293
+ if (ts < 0) {
294
+ ts = 0;
295
+ if (raf.frameLoop !== "demand") {
296
+ nativeRaf(loop);
297
+ }
298
+ }
299
+ }
300
+ function stop() {
301
+ ts = -1;
302
+ }
303
+ function loop() {
304
+ if (~ts) {
305
+ nativeRaf(loop);
306
+ raf.batchedUpdates(update);
307
+ }
308
+ }
309
+ function update() {
310
+ const prevTs = ts;
311
+ ts = raf.now();
312
+ const count = findTimeout(ts);
313
+ if (count) {
314
+ eachSafely(timeouts.splice(0, count), (t) => t.handler());
315
+ pendingCount -= count;
316
+ }
317
+ if (!pendingCount) {
318
+ stop();
319
+ return;
320
+ }
321
+ onStartQueue.flush();
322
+ updateQueue.flush(prevTs ? Math.min(64, ts - prevTs) : 16.667);
323
+ onFrameQueue.flush();
324
+ writeQueue.flush();
325
+ onFinishQueue.flush();
326
+ }
327
+ function makeQueue() {
328
+ let next = /* @__PURE__ */ new Set();
329
+ let current = next;
330
+ return {
331
+ add(fn) {
332
+ pendingCount += current == next && !next.has(fn) ? 1 : 0;
333
+ next.add(fn);
334
+ },
335
+ delete(fn) {
336
+ pendingCount -= current == next && next.has(fn) ? 1 : 0;
337
+ return next.delete(fn);
338
+ },
339
+ flush(arg) {
340
+ if (current.size) {
341
+ next = /* @__PURE__ */ new Set();
342
+ pendingCount -= current.size;
343
+ eachSafely(current, (fn) => fn(arg) && next.add(fn));
344
+ pendingCount += next.size;
345
+ current = next;
346
+ }
347
+ }
348
+ };
349
+ }
350
+ function eachSafely(values, each) {
351
+ values.forEach((value) => {
352
+ try {
353
+ each(value);
354
+ } catch (e) {
355
+ raf.catch(e);
356
+ }
357
+ });
358
+ }
359
+
360
+ // src/Modal.tsx
361
+ import { useTransition as useTransition2 } from "@react-spring/web";
362
+ import { useCallback, useRef as useRef2, useState } from "react";
363
+ import {
364
+ ModalContainer,
365
+ useDialogKeyDown,
366
+ useModalLogic
367
+ } from "@slithy/modal-kit";
368
+ import { useMountEffect, useTrapFocus } from "@slithy/utils";
369
+
370
+ // src/Veil.tsx
371
+ import { useTransition } from "@react-spring/web";
372
+ import { useModalStore as useModalStore2 } from "@slithy/modal-core";
373
+
374
+ // src/Veil.css
375
+ styleInject("[data-slithy=modal-veil] {\n background: rgba(0, 0, 0, 0.25);\n inset: 0;\n position: absolute;\n user-select: none;\n -webkit-user-select: none;\n z-index: 100;\n}\n");
376
+
377
+ // src/Veil.tsx
378
+ import { jsx as jsx4 } from "react/jsx-runtime";
379
+ var opaque = 1;
380
+ var transparent = 0;
381
+ var veilTransitions = {
382
+ config: defaultSpring,
383
+ from: { opacity: transparent },
384
+ enter: { opacity: opaque },
385
+ leave: { opacity: transparent }
386
+ };
387
+ var Veil = ({ modalId, skipAnimation }) => {
388
+ const { backdropId, topModalId } = useModalStore2(
389
+ ({ backdropId: backdropId2, topModalId: topModalId2 }) => ({ backdropId: backdropId2, topModalId: topModalId2 })
390
+ );
391
+ const transitions = useTransition(
392
+ backdropId !== modalId && topModalId !== modalId,
393
+ {
394
+ ...veilTransitions,
395
+ immediate: !!skipAnimation
396
+ }
397
+ );
398
+ return transitions(
399
+ (springStyles, show) => show ? /* @__PURE__ */ jsx4(
400
+ AnimatedDiv,
401
+ {
402
+ "data-slithy": "modal-veil",
403
+ "data-testid": "modal-veil",
404
+ style: springStyles
405
+ }
406
+ ) : null
407
+ );
408
+ };
409
+
410
+ // src/Modal.tsx
411
+ import { jsx as jsx5, jsxs } from "react/jsx-runtime";
412
+ var opaque2 = 1;
413
+ var transparent2 = 0;
414
+ var Modal = ({
415
+ "aria-label": ariaLabel,
416
+ afterClose,
417
+ afterOpen,
418
+ alignX = "center",
419
+ alignY = "middle",
420
+ children,
421
+ containerScrolling = true,
422
+ contentClassName,
423
+ contentStyle,
424
+ contentTransitions,
425
+ disableOpacityTransition,
426
+ dismissible = true,
427
+ dragDirection,
428
+ layerIsActive: layerIsActiveProp,
429
+ springConfig
430
+ }) => {
431
+ const trapFocus = useTrapFocus();
432
+ const [springOpen, setSpringOpen] = useState(false);
433
+ useMountEffect(() => {
434
+ const cb = () => setSpringOpen(true);
435
+ raf(cb);
436
+ return () => raf.cancel(cb);
437
+ });
438
+ const [scrollable, setScrollable] = useState(false);
439
+ const doneRef = useRef2(null);
440
+ const dragDismissedRef = useRef2(false);
441
+ const onLeave = useCallback((done) => {
442
+ doneRef.current = done;
443
+ setScrollable(false);
444
+ setSpringOpen(false);
445
+ }, []);
446
+ const {
447
+ contentRef,
448
+ handleCloseModal,
449
+ layerIsActive,
450
+ skipAnimation,
451
+ isTopModal,
452
+ markAtRest,
453
+ modalId,
454
+ modalState
455
+ } = useModalLogic({
456
+ afterClose,
457
+ // afterOpen is fired from enter.onRest instead of the mount effect
458
+ afterOpen: void 0,
459
+ delayAtRest: true,
460
+ layerIsActive: layerIsActiveProp,
461
+ onLeave
462
+ });
463
+ const onKeyDown = useDialogKeyDown({
464
+ dismissible,
465
+ handleCloseModal,
466
+ isTopModal,
467
+ layerIsActive,
468
+ onKeyDown: trapFocus.onKeyDown
469
+ });
470
+ const transitions = useTransition2(springOpen, {
471
+ config: springConfig ?? defaultSpring,
472
+ from: { opacity: transparent2, ...contentTransitions?.from },
473
+ enter: {
474
+ opacity: opaque2,
475
+ ...contentTransitions?.enter,
476
+ onRest: () => {
477
+ if (springOpen) {
478
+ markAtRest();
479
+ afterOpen?.();
480
+ if (containerScrolling) setScrollable(true);
481
+ }
482
+ }
483
+ },
484
+ leave: {
485
+ opacity: transparent2,
486
+ ...dragDismissedRef.current ? { immediate: true } : contentTransitions?.leave,
487
+ onRest: () => {
488
+ if (!springOpen && doneRef.current) {
489
+ if (dragDismissedRef.current) return;
490
+ doneRef.current();
491
+ doneRef.current = null;
492
+ }
493
+ }
494
+ },
495
+ immediate: !!skipAnimation
496
+ });
497
+ const renderContent = (dragStyles, styles) => /* @__PURE__ */ jsx5(
498
+ ModalContainer,
499
+ {
500
+ alignX,
501
+ modalId,
502
+ onBackdropClick: dismissible ? handleCloseModal : void 0,
503
+ scrollable,
504
+ children: /* @__PURE__ */ jsx5(
505
+ AnimatedModalContent,
506
+ {
507
+ alignY,
508
+ disableOpacityTransition,
509
+ modalState,
510
+ style: styles,
511
+ children: /* @__PURE__ */ jsxs(
512
+ AnimatedModalDialog,
513
+ {
514
+ ref: contentRef,
515
+ "aria-label": ariaLabel,
516
+ className: contentClassName,
517
+ onKeyDown,
518
+ style: {
519
+ ...contentStyle,
520
+ x: dragStyles.x ?? 0,
521
+ y: dragStyles.y ?? 0
522
+ },
523
+ children: [
524
+ children,
525
+ /* @__PURE__ */ jsx5(Veil, { modalId, skipAnimation })
526
+ ]
527
+ }
528
+ )
529
+ }
530
+ )
531
+ }
532
+ );
533
+ return transitions(
534
+ (styles, show) => show ? dismissible ? /* @__PURE__ */ jsx5(
535
+ ModalDraggingSlot,
536
+ {
537
+ modalId,
538
+ dragDirection,
539
+ cardRef: contentRef,
540
+ dragDismissedRef,
541
+ onFlyOutRest: () => {
542
+ if (doneRef.current) {
543
+ doneRef.current();
544
+ doneRef.current = null;
545
+ }
546
+ },
547
+ children: ({ dragStyles }) => renderContent(dragStyles, styles)
548
+ }
549
+ ) : renderContent({}, styles) : null
550
+ );
551
+ };
552
+
553
+ // src/ModalRenderer.tsx
554
+ import { useTransition as useTransition3 } from "@react-spring/web";
555
+ import { useModalStore as useModalStore3 } from "@slithy/modal-core";
556
+ import { ModalRenderer as ModalKitRenderer } from "@slithy/modal-kit";
557
+ import { jsx as jsx6 } from "react/jsx-runtime";
558
+ var opaque3 = 1;
559
+ var transparent3 = 0;
560
+ var backdropTransitions = {
561
+ config: defaultSpring,
562
+ from: { opacity: transparent3 },
563
+ enter: { opacity: opaque3 },
564
+ leave: { opacity: transparent3 }
565
+ };
566
+ var ModalRenderer = ({
567
+ renderLayer,
568
+ renderPortal
569
+ } = {}) => {
570
+ const { backdropId, modals } = useModalStore3(({ backdropId: backdropId2, modals: modals2 }) => ({
571
+ backdropId: backdropId2,
572
+ modals: modals2
573
+ }));
574
+ const modalsExist = modals.length > 0;
575
+ const transitions = useTransition3(!!backdropId && modalsExist, {
576
+ ...backdropTransitions,
577
+ immediate: !!modals.at(-1)?.skipAnimation
578
+ });
579
+ const renderBackdrop = () => transitions(
580
+ (styles, show) => show ? /* @__PURE__ */ jsx6(ModalBackdrop2, { styles }) : null
581
+ );
582
+ return /* @__PURE__ */ jsx6(
583
+ ModalKitRenderer,
584
+ {
585
+ renderBackdrop,
586
+ renderLayer,
587
+ renderPortal
588
+ }
589
+ );
590
+ };
591
+ export {
592
+ DragHandle,
593
+ Modal,
594
+ ModalBackdrop2 as ModalBackdrop,
595
+ ModalRenderer,
596
+ defaultSpring,
597
+ iosSheetSpring,
598
+ useModalDrag,
599
+ useModalDragging
600
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@slithy/modal-spring",
3
+ "version": "0.1.2",
4
+ "description": "React Spring animation adapter for @slithy/modal-kit.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./dist/index.js",
9
+ "types": "./dist/index.d.ts"
10
+ }
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "sideEffects": [
16
+ "*.css"
17
+ ],
18
+ "dependencies": {
19
+ "@slithy/modal-kit": "0.1.2",
20
+ "@slithy/modal-core": "0.1.2",
21
+ "@slithy/utils": "0.3.0"
22
+ },
23
+ "peerDependencies": {
24
+ "@react-spring/web": ">=9",
25
+ "@use-gesture/react": ">=10",
26
+ "react": "^17 || ^18 || ^19"
27
+ },
28
+ "devDependencies": {
29
+ "@react-spring/rafz": "^9",
30
+ "@react-spring/web": "^9",
31
+ "@use-gesture/react": "^10.3.1",
32
+ "@testing-library/jest-dom": "^6",
33
+ "@testing-library/react": "^16",
34
+ "@types/react": "^19",
35
+ "@vitejs/plugin-react": "^6",
36
+ "@vitest/coverage-v8": "^4.1.2",
37
+ "jsdom": "^29.0.1",
38
+ "react": "^19",
39
+ "react-dom": "^19",
40
+ "tsup": "^8",
41
+ "typescript": "^5",
42
+ "vitest": "^4.1.2",
43
+ "@slithy/tsconfig": "0.0.0",
44
+ "@slithy/eslint-config": "0.0.0"
45
+ },
46
+ "author": "mjcampagna",
47
+ "license": "ISC",
48
+ "scripts": {
49
+ "clean": "rm -rf dist",
50
+ "build": "rm -rf dist && tsup",
51
+ "dev": "tsup --watch",
52
+ "typecheck": "tsc --noEmit",
53
+ "lint": "eslint .",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest"
56
+ }
57
+ }