entity-react-modals 1.0.0 → 1.1.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 +195 -16
- package/dist/index.cjs +189 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +189 -29
- package/dist/index.js.map +1 -1
- package/dist/styles.css +7 -2
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# entity-react-modals
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/entity-react-modals)
|
|
4
|
+
[](https://www.npmjs.com/package/entity-react-modals)
|
|
5
|
+
[](https://github.com/BazilSuhail/npm-react-modals/blob/main/LICENSE)
|
|
6
|
+
[](https://bundlephobia.com/package/entity-react-modals)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
|
|
3
9
|
Zero-dependency React modal library with spring physics animations powered by the Web Animations API.
|
|
4
10
|
|
|
5
|
-
-
|
|
11
|
+
- **7 animation variants** — scale, slide, fade, none
|
|
12
|
+
- **5 spring presets** — default, gentle, wobbly, stiff, slow
|
|
13
|
+
- Zero-config — styles auto-injected, no CSS import needed
|
|
6
14
|
- Compound component API for flexible composition
|
|
7
|
-
- 5 built-in spring presets + custom spring configs
|
|
8
15
|
- Controlled and uncontrolled modes
|
|
9
16
|
- Portal-rendered, accessible, and tree-shakable
|
|
10
17
|
|
|
@@ -26,7 +33,6 @@ import {
|
|
|
26
33
|
ModalFooter,
|
|
27
34
|
ModalClose,
|
|
28
35
|
} from 'entity-react-modals';
|
|
29
|
-
import 'entity-react-modals/styles.css';
|
|
30
36
|
|
|
31
37
|
function App() {
|
|
32
38
|
return (
|
|
@@ -74,7 +80,8 @@ function App() {
|
|
|
74
80
|
| `open` | `boolean` | — | Controlled open state. |
|
|
75
81
|
| `defaultOpen` | `boolean` | `false` | Initial open state for uncontrolled mode. |
|
|
76
82
|
| `onOpenChange` | `(open: boolean) => void` | — | Called when open state changes. |
|
|
77
|
-
| `
|
|
83
|
+
| `animation` | `AnimationVariant` | `'scale'` | Animation variant. |
|
|
84
|
+
| `spring` | `SpringPreset \| SpringConfig` | `'default'` | Spring physics config. |
|
|
78
85
|
| `backdropColor` | `string` | `'rgba(0,0,0,0.6)'` | Backdrop background color. |
|
|
79
86
|
| `backdropBlur` | `number` | `4` | Backdrop blur radius in px. |
|
|
80
87
|
| `size` | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Panel max-width preset. |
|
|
@@ -87,7 +94,9 @@ Props set on `ModalContent` override those set on `Modal` for that specific cont
|
|
|
87
94
|
| Prop | Type | Default | Description |
|
|
88
95
|
|---|---|---|---|
|
|
89
96
|
| `children` | `ReactNode` | — | Modal panel content. |
|
|
97
|
+
| `className` | `string` | — | Additional CSS class on the panel. |
|
|
90
98
|
| `size` | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Panel max-width. |
|
|
99
|
+
| `animation` | `AnimationVariant` | `'scale'` | Animation variant. |
|
|
91
100
|
| `closeOnBackdropClick` | `boolean` | `true` | Close when clicking the backdrop. |
|
|
92
101
|
| `closeOnEscape` | `boolean` | `true` | Close on Escape key press. |
|
|
93
102
|
| `backdropColor` | `string` | — | Override backdrop color. |
|
|
@@ -109,6 +118,184 @@ Props set on `ModalContent` override those set on `Modal` for that specific cont
|
|
|
109
118
|
| `children` | `ReactNode` | Section content. |
|
|
110
119
|
| `className` | `string` | Additional CSS class. |
|
|
111
120
|
|
|
121
|
+
## Animation Variants
|
|
122
|
+
|
|
123
|
+
| Variant | Effect |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `scale` | Scale from 0.95 to 1 (default) |
|
|
126
|
+
| `slide-bottom` | Slide up from below |
|
|
127
|
+
| `slide-top` | Slide down from above |
|
|
128
|
+
| `slide-left` | Slide in from left |
|
|
129
|
+
| `slide-right` | Slide in from right |
|
|
130
|
+
| `fade` | Opacity only, no transform |
|
|
131
|
+
| `none` | Instant, no animation |
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
// Set on Modal (applies to all content)
|
|
135
|
+
<Modal animation="slide-bottom">...</Modal>
|
|
136
|
+
|
|
137
|
+
// Set on ModalContent (overrides Modal)
|
|
138
|
+
<ModalContent animation="slide-left" spring="gentle">...</ModalContent>
|
|
139
|
+
|
|
140
|
+
// Disable animation entirely
|
|
141
|
+
<ModalContent animation="none">...</ModalContent>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Real-World Example: Notification Toast
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
<Modal>
|
|
148
|
+
<ModalTrigger>
|
|
149
|
+
<button>Show Notification</button>
|
|
150
|
+
</ModalTrigger>
|
|
151
|
+
<ModalContent size="sm" animation="slide-bottom" spring="stiff"
|
|
152
|
+
backdropColor="rgba(0, 0, 0, 0.3)" backdropBlur={4}>
|
|
153
|
+
<div style={{ padding: '24px', textAlign: 'center' }}>
|
|
154
|
+
<h3>Success!</h3>
|
|
155
|
+
<p>Your changes have been saved.</p>
|
|
156
|
+
<ModalClose>Done</ModalClose>
|
|
157
|
+
</div>
|
|
158
|
+
</ModalContent>
|
|
159
|
+
</Modal>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Real-World Example: Settings Panel
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
<Modal>
|
|
166
|
+
<ModalTrigger>
|
|
167
|
+
<button>Settings</button>
|
|
168
|
+
</ModalTrigger>
|
|
169
|
+
<ModalContent size="lg" animation="slide-left" spring="default"
|
|
170
|
+
backdropColor="rgba(0, 0, 0, 0.4)" backdropBlur={8}>
|
|
171
|
+
<ModalHeader>
|
|
172
|
+
<h2>Settings</h2>
|
|
173
|
+
<ModalClose />
|
|
174
|
+
</ModalHeader>
|
|
175
|
+
<ModalBody>
|
|
176
|
+
{/* Toggle sections, forms, danger zones */}
|
|
177
|
+
</ModalBody>
|
|
178
|
+
</ModalContent>
|
|
179
|
+
</Modal>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Real-World Example: Command Palette
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
<Modal>
|
|
186
|
+
<ModalTrigger>
|
|
187
|
+
<button>Search</button>
|
|
188
|
+
</ModalTrigger>
|
|
189
|
+
<ModalContent size="md" animation="scale" spring="stiff"
|
|
190
|
+
backdropColor="rgba(0, 0, 0, 0.6)" backdropBlur={8}>
|
|
191
|
+
<div style={{ padding: '12px' }}>
|
|
192
|
+
<input type="text" placeholder="Search commands..." autoFocus />
|
|
193
|
+
{/* Command list */}
|
|
194
|
+
</div>
|
|
195
|
+
</ModalContent>
|
|
196
|
+
</Modal>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Real-World Example: Payment Form
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
<Modal>
|
|
203
|
+
<ModalTrigger>
|
|
204
|
+
<button>Upgrade to Pro</button>
|
|
205
|
+
</ModalTrigger>
|
|
206
|
+
<ModalContent size="md" spring="gentle"
|
|
207
|
+
backdropColor="rgba(0, 0, 0, 0.5)" backdropBlur={6}>
|
|
208
|
+
<ModalHeader>
|
|
209
|
+
<h2>Upgrade to Pro</h2>
|
|
210
|
+
<ModalClose />
|
|
211
|
+
</ModalHeader>
|
|
212
|
+
<ModalBody>
|
|
213
|
+
<p>$49 / one time</p>
|
|
214
|
+
{/* Card form */}
|
|
215
|
+
</ModalBody>
|
|
216
|
+
<ModalFooter>
|
|
217
|
+
<ModalClose>Cancel</ModalClose>
|
|
218
|
+
<button>Pay $49</button>
|
|
219
|
+
</ModalFooter>
|
|
220
|
+
</ModalContent>
|
|
221
|
+
</Modal>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Real-World Example: Image Viewer
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
<Modal>
|
|
228
|
+
<ModalTrigger>
|
|
229
|
+
<button>View Image</button>
|
|
230
|
+
</ModalTrigger>
|
|
231
|
+
<ModalContent size="xl" animation="scale" spring="gentle"
|
|
232
|
+
backdropColor="rgba(0, 0, 0, 0.85)" backdropBlur={12}>
|
|
233
|
+
<ModalClose />
|
|
234
|
+
{/* Full-width image content */}
|
|
235
|
+
</ModalContent>
|
|
236
|
+
</Modal>
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Real-World Example: Destructive Alert
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
<Modal>
|
|
243
|
+
<ModalTrigger>
|
|
244
|
+
<button>Delete Account</button>
|
|
245
|
+
</ModalTrigger>
|
|
246
|
+
<ModalContent size="sm" animation="slide-top" spring="stiff"
|
|
247
|
+
backdropColor="rgba(0, 0, 0, 0.6)" backdropBlur={4}>
|
|
248
|
+
<div style={{ padding: '24px', textAlign: 'center' }}>
|
|
249
|
+
<h3>Are you sure?</h3>
|
|
250
|
+
<p>This action cannot be undone.</p>
|
|
251
|
+
<ModalClose>Cancel</ModalClose>
|
|
252
|
+
<button style={{ background: '#ef4444', color: 'white' }}>
|
|
253
|
+
Yes, delete everything
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
</ModalContent>
|
|
257
|
+
</Modal>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Real-World Example: Login Form
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
<Modal>
|
|
264
|
+
<ModalTrigger>
|
|
265
|
+
<button>Sign in</button>
|
|
266
|
+
</ModalTrigger>
|
|
267
|
+
<ModalContent size="sm" animation="slide-bottom" spring="gentle"
|
|
268
|
+
backdropColor="rgba(16, 185, 129, 0.15)" backdropBlur={12}>
|
|
269
|
+
<div style={{ padding: '24px' }}>
|
|
270
|
+
<h3>Welcome back</h3>
|
|
271
|
+
<input type="email" placeholder="you@example.com" />
|
|
272
|
+
<input type="password" placeholder="Password" />
|
|
273
|
+
<button>Sign in</button>
|
|
274
|
+
</div>
|
|
275
|
+
</ModalContent>
|
|
276
|
+
</Modal>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Real-World Example: Pricing Cards
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
<Modal>
|
|
283
|
+
<ModalTrigger>
|
|
284
|
+
<button>View Pricing</button>
|
|
285
|
+
</ModalTrigger>
|
|
286
|
+
<ModalContent size="lg" animation="slide-right" spring="wobbly"
|
|
287
|
+
backdropColor="rgba(0, 0, 0, 0.5)" backdropBlur={8}>
|
|
288
|
+
<ModalHeader>
|
|
289
|
+
<h2>Choose your plan</h2>
|
|
290
|
+
<ModalClose />
|
|
291
|
+
</ModalHeader>
|
|
292
|
+
<ModalBody>
|
|
293
|
+
{/* Three-column pricing cards */}
|
|
294
|
+
</ModalBody>
|
|
295
|
+
</ModalContent>
|
|
296
|
+
</Modal>
|
|
297
|
+
```
|
|
298
|
+
|
|
112
299
|
## Sizes
|
|
113
300
|
|
|
114
301
|
| Size | Max Width |
|
|
@@ -152,15 +339,6 @@ Pass a `SpringConfig` object to fine-tune the animation:
|
|
|
152
339
|
| `damping` | Damping force. Higher = less oscillation. |
|
|
153
340
|
| `mass` | Mass of the animated object. Lower = lighter feel. |
|
|
154
341
|
|
|
155
|
-
Spring config can also be set on the `Modal` root and it applies to all content panels:
|
|
156
|
-
|
|
157
|
-
```tsx
|
|
158
|
-
<Modal spring={{ stiffness: 300, damping: 15, mass: 0.8 }}>
|
|
159
|
-
<ModalTrigger>...</ModalTrigger>
|
|
160
|
-
<ModalContent>...</ModalContent>
|
|
161
|
-
</Modal>
|
|
162
|
-
```
|
|
163
|
-
|
|
164
342
|
## Controlled Mode
|
|
165
343
|
|
|
166
344
|
Manage open/close state externally with `useState`:
|
|
@@ -261,9 +439,9 @@ function App() {
|
|
|
261
439
|
|
|
262
440
|
<Modal>
|
|
263
441
|
<ModalTrigger><button>Second</button></ModalTrigger>
|
|
264
|
-
<ModalContent size="lg" spring="gentle">
|
|
442
|
+
<ModalContent size="lg" animation="slide-left" spring="gentle">
|
|
265
443
|
<ModalHeader><h2>Second Modal</h2><ModalClose /></ModalHeader>
|
|
266
|
-
<ModalBody>Different size and spring</ModalBody>
|
|
444
|
+
<ModalBody>Different size, animation, and spring</ModalBody>
|
|
267
445
|
</ModalContent>
|
|
268
446
|
</Modal>
|
|
269
447
|
</>
|
|
@@ -310,12 +488,13 @@ import type {
|
|
|
310
488
|
ModalFooterProps,
|
|
311
489
|
SpringConfig,
|
|
312
490
|
SpringPreset,
|
|
491
|
+
AnimationVariant,
|
|
313
492
|
} from 'entity-react-modals';
|
|
314
493
|
```
|
|
315
494
|
|
|
316
495
|
## Tree Shaking
|
|
317
496
|
|
|
318
|
-
The package uses the `exports` field with conditional ESM/CJS builds and
|
|
497
|
+
The package uses the `exports` field with conditional ESM/CJS builds and `sideEffects: false`. Styles are auto-injected on first import. Bundlers will tree-shake unused components automatically.
|
|
319
498
|
|
|
320
499
|
```tsx
|
|
321
500
|
// Only imports what you use
|
package/dist/index.cjs
CHANGED
|
@@ -4,7 +4,124 @@ var react = require('react');
|
|
|
4
4
|
var jsxRuntime = require('react/jsx-runtime');
|
|
5
5
|
var reactDom = require('react-dom');
|
|
6
6
|
|
|
7
|
-
// src/
|
|
7
|
+
// src/inject.ts
|
|
8
|
+
var INJECTED = "__ERM_INJECTED__";
|
|
9
|
+
function injectStyles(css) {
|
|
10
|
+
if (typeof document === "undefined") return;
|
|
11
|
+
if (window[INJECTED]) return;
|
|
12
|
+
window[INJECTED] = true;
|
|
13
|
+
const style = document.createElement("style");
|
|
14
|
+
style.textContent = css;
|
|
15
|
+
document.head.appendChild(style);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/styles.ts
|
|
19
|
+
var styles = `
|
|
20
|
+
:root {
|
|
21
|
+
--rm-backdrop-bg: rgba(0, 0, 0, 0.6);
|
|
22
|
+
--rm-panel-bg: #ffffff;
|
|
23
|
+
--rm-panel-radius: 12px;
|
|
24
|
+
--rm-panel-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
|
25
|
+
--rm-close-color: #3b82f6;
|
|
26
|
+
--rm-close-hover: #2563eb;
|
|
27
|
+
--rm-header-border: #e2e8f0;
|
|
28
|
+
--rm-footer-border: #e2e8f0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.rm-backdrop {
|
|
32
|
+
position: fixed;
|
|
33
|
+
inset: 0;
|
|
34
|
+
z-index: 9998;
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
padding: 20px;
|
|
39
|
+
opacity: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.rm-panel {
|
|
43
|
+
position: relative;
|
|
44
|
+
z-index: 9999;
|
|
45
|
+
background: var(--rm-panel-bg);
|
|
46
|
+
border-radius: var(--rm-panel-radius);
|
|
47
|
+
box-shadow: var(--rm-panel-shadow);
|
|
48
|
+
width: 100%;
|
|
49
|
+
max-height: calc(100vh - 40px);
|
|
50
|
+
opacity: 0;
|
|
51
|
+
transform: scale(0.95);
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.rm-panel--sm { max-width: 400px; }
|
|
57
|
+
.rm-panel--md { max-width: 560px; }
|
|
58
|
+
.rm-panel--lg { max-width: 720px; }
|
|
59
|
+
.rm-panel--xl { max-width: 900px; }
|
|
60
|
+
|
|
61
|
+
.rm-header {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
justify-content: space-between;
|
|
65
|
+
padding: 16px 20px;
|
|
66
|
+
border-bottom: 1px solid var(--rm-header-border);
|
|
67
|
+
flex-shrink: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.rm-header h2,
|
|
71
|
+
.rm-header h3 {
|
|
72
|
+
margin: 0;
|
|
73
|
+
font-size: 1.125rem;
|
|
74
|
+
font-weight: 600;
|
|
75
|
+
color: #1e293b;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.rm-body {
|
|
79
|
+
padding: 20px;
|
|
80
|
+
overflow-y: auto;
|
|
81
|
+
flex: 1;
|
|
82
|
+
min-height: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.rm-footer {
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
justify-content: flex-end;
|
|
89
|
+
gap: 8px;
|
|
90
|
+
padding: 16px 20px;
|
|
91
|
+
border-top: 1px solid var(--rm-footer-border);
|
|
92
|
+
flex-shrink: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.rm-close {
|
|
96
|
+
display: inline-flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
width: 28px;
|
|
100
|
+
height: 28px;
|
|
101
|
+
padding: 0;
|
|
102
|
+
border: none;
|
|
103
|
+
border-radius: 6px;
|
|
104
|
+
background: transparent;
|
|
105
|
+
color: var(--rm-close-color);
|
|
106
|
+
font-size: 1.25rem;
|
|
107
|
+
line-height: 1;
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
transition: background-color 0.15s ease, color 0.15s ease;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.rm-close:hover {
|
|
113
|
+
background: rgba(59, 130, 246, 0.1);
|
|
114
|
+
color: var(--rm-close-hover);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@media (prefers-reduced-motion: reduce) {
|
|
118
|
+
.rm-backdrop,
|
|
119
|
+
.rm-panel {
|
|
120
|
+
transition: none !important;
|
|
121
|
+
animation: none !important;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
`;
|
|
8
125
|
var ModalContext = react.createContext(null);
|
|
9
126
|
function useModalContext() {
|
|
10
127
|
const ctx = react.useContext(ModalContext);
|
|
@@ -17,6 +134,7 @@ function Modal({
|
|
|
17
134
|
defaultOpen = false,
|
|
18
135
|
onOpenChange,
|
|
19
136
|
spring,
|
|
137
|
+
animation,
|
|
20
138
|
backdropColor,
|
|
21
139
|
backdropBlur,
|
|
22
140
|
size,
|
|
@@ -46,6 +164,7 @@ function Modal({
|
|
|
46
164
|
onClose,
|
|
47
165
|
onOpen,
|
|
48
166
|
spring,
|
|
167
|
+
animation,
|
|
49
168
|
backdropColor,
|
|
50
169
|
backdropBlur,
|
|
51
170
|
size,
|
|
@@ -114,21 +233,58 @@ function generateSpringKeyframes(from, to, config, options) {
|
|
|
114
233
|
const duration = steps * FIXED_DT * 1e3;
|
|
115
234
|
return { keyframes: positions, duration };
|
|
116
235
|
}
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
const waapiKeyframes = positions.map((pos, i) => {
|
|
236
|
+
function buildTransformKeyframes(positions, variant) {
|
|
237
|
+
return positions.map((pos, i) => {
|
|
120
238
|
const offset = i / (positions.length - 1);
|
|
121
|
-
|
|
122
|
-
|
|
239
|
+
switch (variant) {
|
|
240
|
+
case "fade":
|
|
241
|
+
return { opacity: String(pos), offset };
|
|
242
|
+
case "scale":
|
|
243
|
+
return {
|
|
244
|
+
transform: `scale(${0.95 + 0.05 * pos})`,
|
|
245
|
+
opacity: String(pos),
|
|
246
|
+
offset
|
|
247
|
+
};
|
|
248
|
+
case "slide-bottom":
|
|
249
|
+
return {
|
|
250
|
+
transform: `translateY(${(1 - pos) * 100}%)`,
|
|
251
|
+
opacity: String(pos),
|
|
252
|
+
offset
|
|
253
|
+
};
|
|
254
|
+
case "slide-top":
|
|
255
|
+
return {
|
|
256
|
+
transform: `translateY(${(pos - 1) * 100}%)`,
|
|
257
|
+
opacity: String(pos),
|
|
258
|
+
offset
|
|
259
|
+
};
|
|
260
|
+
case "slide-left":
|
|
261
|
+
return {
|
|
262
|
+
transform: `translateX(${(pos - 1) * 100}%)`,
|
|
263
|
+
opacity: String(pos),
|
|
264
|
+
offset
|
|
265
|
+
};
|
|
266
|
+
case "slide-right":
|
|
267
|
+
return {
|
|
268
|
+
transform: `translateX(${(1 - pos) * 100}%)`,
|
|
269
|
+
opacity: String(pos),
|
|
270
|
+
offset
|
|
271
|
+
};
|
|
272
|
+
default:
|
|
273
|
+
return { opacity: String(pos), offset };
|
|
123
274
|
}
|
|
124
|
-
const scale = 0.95 + 0.05 * pos;
|
|
125
|
-
return {
|
|
126
|
-
transform: `scale(${scale})`,
|
|
127
|
-
opacity: String(pos),
|
|
128
|
-
offset
|
|
129
|
-
};
|
|
130
275
|
});
|
|
131
|
-
|
|
276
|
+
}
|
|
277
|
+
function springToWAAPIKeyframes(fromValue, toValue, property, config, options) {
|
|
278
|
+
const { keyframes: positions, duration } = generateSpringKeyframes(fromValue, toValue, config, options);
|
|
279
|
+
if (property === "opacity") {
|
|
280
|
+
const keyframes = positions.map((pos, i) => ({
|
|
281
|
+
opacity: String(pos),
|
|
282
|
+
offset: i / (positions.length - 1)
|
|
283
|
+
}));
|
|
284
|
+
return { keyframes, duration };
|
|
285
|
+
}
|
|
286
|
+
const variant = options?.variant ?? "scale";
|
|
287
|
+
return { keyframes: buildTransformKeyframes(positions, variant), duration };
|
|
132
288
|
}
|
|
133
289
|
|
|
134
290
|
// src/animate.ts
|
|
@@ -137,10 +293,11 @@ function prefersReducedMotion() {
|
|
|
137
293
|
}
|
|
138
294
|
var EXIT_MAX_DURATION = 400;
|
|
139
295
|
function animateIn(backdropEl, panelEl, config) {
|
|
140
|
-
|
|
296
|
+
const variant = config?.animation ?? "scale";
|
|
297
|
+
if (prefersReducedMotion() || variant === "none") {
|
|
141
298
|
backdropEl.style.opacity = "1";
|
|
142
299
|
panelEl.style.opacity = "1";
|
|
143
|
-
panelEl.style.transform = "
|
|
300
|
+
panelEl.style.transform = "none";
|
|
144
301
|
return [];
|
|
145
302
|
}
|
|
146
303
|
const springConfig = resolveSpringConfig(config?.spring);
|
|
@@ -148,7 +305,7 @@ function animateIn(backdropEl, panelEl, config) {
|
|
|
148
305
|
...springConfig,
|
|
149
306
|
stiffness: springConfig.stiffness * 0.8
|
|
150
307
|
});
|
|
151
|
-
const { keyframes: panelKF, duration: panelDur } = springToWAAPIKeyframes(0, 1, "transform", springConfig);
|
|
308
|
+
const { keyframes: panelKF, duration: panelDur } = springToWAAPIKeyframes(0, 1, "transform", springConfig, { variant });
|
|
152
309
|
const backdropAnim = backdropEl.animate(backdropKF, {
|
|
153
310
|
duration: backdropDur,
|
|
154
311
|
easing: "linear",
|
|
@@ -162,7 +319,8 @@ function animateIn(backdropEl, panelEl, config) {
|
|
|
162
319
|
return [backdropAnim, panelAnim];
|
|
163
320
|
}
|
|
164
321
|
function animateOut(backdropEl, panelEl, config) {
|
|
165
|
-
|
|
322
|
+
const variant = config?.animation ?? "scale";
|
|
323
|
+
if (prefersReducedMotion() || variant === "none") {
|
|
166
324
|
return Promise.resolve();
|
|
167
325
|
}
|
|
168
326
|
const springConfig = resolveSpringConfig(config?.spring);
|
|
@@ -176,7 +334,8 @@ function animateOut(backdropEl, panelEl, config) {
|
|
|
176
334
|
stiffness: exitConfig.stiffness * 0.8
|
|
177
335
|
}, { maxDuration: EXIT_MAX_DURATION });
|
|
178
336
|
const { keyframes: panelKF, duration: panelDur } = springToWAAPIKeyframes(1, 0, "transform", exitConfig, {
|
|
179
|
-
maxDuration: EXIT_MAX_DURATION
|
|
337
|
+
maxDuration: EXIT_MAX_DURATION,
|
|
338
|
+
variant
|
|
180
339
|
});
|
|
181
340
|
const backdropAnim = backdropEl.animate(backdropKF, {
|
|
182
341
|
duration: backdropDur,
|
|
@@ -219,16 +378,12 @@ function useClickOutside(enabled, onClick) {
|
|
|
219
378
|
}, [enabled, handleClick]);
|
|
220
379
|
return panelRef;
|
|
221
380
|
}
|
|
222
|
-
var SIZE_MAP = {
|
|
223
|
-
sm: "400px",
|
|
224
|
-
md: "560px",
|
|
225
|
-
lg: "720px",
|
|
226
|
-
xl: "900px"
|
|
227
|
-
};
|
|
228
381
|
var SAFETY_UNMOUNT_MS = 600;
|
|
229
382
|
function ModalContent({
|
|
230
383
|
children,
|
|
384
|
+
className,
|
|
231
385
|
size: sizeProp,
|
|
386
|
+
animation: animationProp,
|
|
232
387
|
closeOnBackdropClick = true,
|
|
233
388
|
closeOnEscape = true,
|
|
234
389
|
backdropColor,
|
|
@@ -238,6 +393,7 @@ function ModalContent({
|
|
|
238
393
|
}) {
|
|
239
394
|
const ctx = useModalContext();
|
|
240
395
|
const size = sizeProp ?? ctx.size ?? "md";
|
|
396
|
+
const resolvedAnimation = animationProp ?? ctx.animation ?? "scale";
|
|
241
397
|
const resolvedBackdropColor = backdropColor ?? ctx.backdropColor ?? "rgba(0, 0, 0, 0.6)";
|
|
242
398
|
const resolvedBackdropBlur = backdropBlur ?? ctx.backdropBlur ?? 4;
|
|
243
399
|
const resolvedSpring = spring ?? ctx.spring;
|
|
@@ -268,7 +424,8 @@ function ModalContent({
|
|
|
268
424
|
requestAnimationFrame(() => {
|
|
269
425
|
if (backdropRef.current && panelRef.current) {
|
|
270
426
|
animateIn(backdropRef.current, panelRef.current, {
|
|
271
|
-
spring: resolvedSpring
|
|
427
|
+
spring: resolvedSpring,
|
|
428
|
+
animation: resolvedAnimation});
|
|
272
429
|
setTimeout(() => {
|
|
273
430
|
animatingRef.current = false;
|
|
274
431
|
}, 500);
|
|
@@ -288,14 +445,15 @@ function ModalContent({
|
|
|
288
445
|
safetyTimerRef.current = setTimeout(unmount, SAFETY_UNMOUNT_MS);
|
|
289
446
|
if (backdropRef.current && panelRef.current) {
|
|
290
447
|
animateOut(backdropRef.current, panelRef.current, {
|
|
291
|
-
spring: resolvedSpring
|
|
448
|
+
spring: resolvedSpring,
|
|
449
|
+
animation: resolvedAnimation}).then(unmount);
|
|
292
450
|
} else {
|
|
293
451
|
unmount();
|
|
294
452
|
}
|
|
295
453
|
}
|
|
296
454
|
prevOpen.current = ctx.open;
|
|
297
455
|
return clearSafetyTimer;
|
|
298
|
-
}, [ctx.open, resolvedSpring, animationDuration, clearSafetyTimer]);
|
|
456
|
+
}, [ctx.open, resolvedSpring, resolvedAnimation, animationDuration, clearSafetyTimer]);
|
|
299
457
|
if (!render) return null;
|
|
300
458
|
const setRefs = (el) => {
|
|
301
459
|
clickOutsideRef.current = el;
|
|
@@ -316,10 +474,9 @@ function ModalContent({
|
|
|
316
474
|
"div",
|
|
317
475
|
{
|
|
318
476
|
ref: setRefs,
|
|
319
|
-
className: `rm-panel rm-panel--${size}`,
|
|
477
|
+
className: `rm-panel rm-panel--${size}${className ? ` ${className}` : ""}`,
|
|
320
478
|
role: "dialog",
|
|
321
479
|
"aria-modal": "true",
|
|
322
|
-
style: { maxWidth: SIZE_MAP[size] },
|
|
323
480
|
children
|
|
324
481
|
}
|
|
325
482
|
)
|
|
@@ -342,6 +499,9 @@ function ModalFooter({ children, className }) {
|
|
|
342
499
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `rm-footer ${className ?? ""}`, children });
|
|
343
500
|
}
|
|
344
501
|
|
|
502
|
+
// src/index.ts
|
|
503
|
+
injectStyles(styles);
|
|
504
|
+
|
|
345
505
|
exports.Modal = Modal;
|
|
346
506
|
exports.ModalBody = ModalBody;
|
|
347
507
|
exports.ModalClose = ModalClose;
|