@latte-macchiat-io/latte-vanilla-components 0.0.185 → 0.0.187
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 +286 -10
- package/package.json +8 -11
- package/src/components/Button/stories.tsx +261 -0
- package/src/components/Footer/stories.tsx +1 -1
- package/src/components/Header/stories.tsx +1 -1
- package/src/components/Logo/stories.tsx +2 -1
- package/src/components/Modal/stories.tsx +409 -0
- package/src/components/Nav/stories.tsx +1 -1
- package/src/components/NavLegal/stories.tsx +1 -1
- package/src/components/Section/stories.tsx +345 -0
- package/src/index.ts +10 -10
- package/src/components/Actions/stories.tsx +0 -34
- package/src/components/ConsentCookie/stories.tsx +0 -28
- package/src/components/Form/Row/stories.tsx +0 -41
- package/src/components/Form/TextField/Textarea/stories.tsx +0 -44
- package/src/components/NavSocial/stories.tsx +0 -33
- package/src/themes/default.css.ts +0 -20
- package/src/utils/createCustomTheme.ts +0 -52
- package/src/utils/themeOverrides.ts +0 -10
@@ -0,0 +1,409 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
3
|
+
import { useState } from 'react';
|
4
|
+
import { Modal } from './Modal';
|
5
|
+
import { Button } from '../Button/Button';
|
6
|
+
import { Section } from '../Section/Section';
|
7
|
+
|
8
|
+
const meta: Meta<typeof Modal> = {
|
9
|
+
title: 'Interactive Components/Modal',
|
10
|
+
component: Modal,
|
11
|
+
parameters: {
|
12
|
+
layout: 'fullscreen',
|
13
|
+
docs: {
|
14
|
+
description: {
|
15
|
+
component: `
|
16
|
+
The Modal component provides an accessible, animated modal dialog with various customization options.
|
17
|
+
|
18
|
+
## Features
|
19
|
+
- Accessible (ARIA compliant, focus management)
|
20
|
+
- Animated entrance/exit
|
21
|
+
- Multiple sizes (small, medium, large, fullscreen)
|
22
|
+
- Centered or top-aligned positioning
|
23
|
+
- Backdrop click to close (optional)
|
24
|
+
- Escape key to close (optional)
|
25
|
+
- Close button (optional)
|
26
|
+
- Body scroll lock when open
|
27
|
+
- TypeScript support
|
28
|
+
|
29
|
+
## Accessibility
|
30
|
+
- Traps focus within the modal
|
31
|
+
- Restores focus to trigger element on close
|
32
|
+
- Supports keyboard navigation
|
33
|
+
- ARIA attributes for screen readers
|
34
|
+
`,
|
35
|
+
},
|
36
|
+
},
|
37
|
+
},
|
38
|
+
tags: ['autodocs'],
|
39
|
+
argTypes: {
|
40
|
+
isOpen: {
|
41
|
+
control: 'boolean',
|
42
|
+
description: 'Controls whether the modal is visible',
|
43
|
+
},
|
44
|
+
size: {
|
45
|
+
control: 'select',
|
46
|
+
options: ['small', 'medium', 'large', 'fullscreen'],
|
47
|
+
description: 'Size of the modal dialog',
|
48
|
+
},
|
49
|
+
centered: {
|
50
|
+
control: 'boolean',
|
51
|
+
description: 'Whether to center the modal vertically',
|
52
|
+
},
|
53
|
+
showCloseButton: {
|
54
|
+
control: 'boolean',
|
55
|
+
description: 'Whether to show the close button',
|
56
|
+
},
|
57
|
+
closeOnBackdropClick: {
|
58
|
+
control: 'boolean',
|
59
|
+
description: 'Whether clicking the backdrop closes the modal',
|
60
|
+
},
|
61
|
+
closeOnEscape: {
|
62
|
+
control: 'boolean',
|
63
|
+
description: 'Whether pressing Escape closes the modal',
|
64
|
+
},
|
65
|
+
},
|
66
|
+
};
|
67
|
+
|
68
|
+
export default meta;
|
69
|
+
type Story = StoryObj<typeof Modal>;
|
70
|
+
|
71
|
+
// Interactive Modal Example
|
72
|
+
const InteractiveModal = ({ size = 'medium', centered = true, ...args }: any) => {
|
73
|
+
const [isOpen, setIsOpen] = useState(false);
|
74
|
+
|
75
|
+
return (
|
76
|
+
<Section>
|
77
|
+
<Button variant="primary" onClick={() => setIsOpen(true)}>
|
78
|
+
Open Modal
|
79
|
+
</Button>
|
80
|
+
<Modal
|
81
|
+
isOpen={isOpen}
|
82
|
+
onClose={() => setIsOpen(false)}
|
83
|
+
size={size}
|
84
|
+
centered={centered}
|
85
|
+
{...args}
|
86
|
+
>
|
87
|
+
<div style={{ padding: '2rem' }}>
|
88
|
+
<h2>Modal Title</h2>
|
89
|
+
<p>This is the modal content. You can put any React components here.</p>
|
90
|
+
<div style={{ marginTop: '2rem', display: 'flex', gap: '1rem' }}>
|
91
|
+
<Button variant="primary" onClick={() => setIsOpen(false)}>
|
92
|
+
Confirm
|
93
|
+
</Button>
|
94
|
+
<Button variant="secondary" onClick={() => setIsOpen(false)}>
|
95
|
+
Cancel
|
96
|
+
</Button>
|
97
|
+
</div>
|
98
|
+
</div>
|
99
|
+
</Modal>
|
100
|
+
</Section>
|
101
|
+
);
|
102
|
+
};
|
103
|
+
|
104
|
+
export const Default: Story = {
|
105
|
+
render: (args) => <InteractiveModal {...args} />,
|
106
|
+
args: {
|
107
|
+
size: 'medium',
|
108
|
+
centered: true,
|
109
|
+
showCloseButton: true,
|
110
|
+
closeOnBackdropClick: true,
|
111
|
+
closeOnEscape: true,
|
112
|
+
},
|
113
|
+
};
|
114
|
+
|
115
|
+
export const Small: Story = {
|
116
|
+
render: (args) => <InteractiveModal {...args} size="small" />,
|
117
|
+
parameters: {
|
118
|
+
docs: {
|
119
|
+
description: {
|
120
|
+
story: 'Small modal size, perfect for confirmations and quick actions.',
|
121
|
+
},
|
122
|
+
},
|
123
|
+
},
|
124
|
+
};
|
125
|
+
|
126
|
+
export const Medium: Story = {
|
127
|
+
render: (args) => <InteractiveModal {...args} size="medium" />,
|
128
|
+
parameters: {
|
129
|
+
docs: {
|
130
|
+
description: {
|
131
|
+
story: 'Medium modal size, the default and most commonly used size.',
|
132
|
+
},
|
133
|
+
},
|
134
|
+
},
|
135
|
+
};
|
136
|
+
|
137
|
+
export const Large: Story = {
|
138
|
+
render: (args) => <InteractiveModal {...args} size="large" />,
|
139
|
+
parameters: {
|
140
|
+
docs: {
|
141
|
+
description: {
|
142
|
+
story: 'Large modal size, good for forms and detailed content.',
|
143
|
+
},
|
144
|
+
},
|
145
|
+
},
|
146
|
+
};
|
147
|
+
|
148
|
+
export const Fullscreen: Story = {
|
149
|
+
render: (args) => <InteractiveModal {...args} size="fullscreen" />,
|
150
|
+
parameters: {
|
151
|
+
docs: {
|
152
|
+
description: {
|
153
|
+
story: 'Fullscreen modal that takes up the entire viewport.',
|
154
|
+
},
|
155
|
+
},
|
156
|
+
},
|
157
|
+
};
|
158
|
+
|
159
|
+
export const TopAligned: Story = {
|
160
|
+
render: (args) => <InteractiveModal {...args} centered={false} />,
|
161
|
+
parameters: {
|
162
|
+
docs: {
|
163
|
+
description: {
|
164
|
+
story: 'Modal aligned to the top of the viewport instead of centered.',
|
165
|
+
},
|
166
|
+
},
|
167
|
+
},
|
168
|
+
};
|
169
|
+
|
170
|
+
export const NoCloseButton: Story = {
|
171
|
+
render: (args) => <InteractiveModal {...args} showCloseButton={false} />,
|
172
|
+
parameters: {
|
173
|
+
docs: {
|
174
|
+
description: {
|
175
|
+
story: 'Modal without the close button - must be closed programmatically.',
|
176
|
+
},
|
177
|
+
},
|
178
|
+
},
|
179
|
+
};
|
180
|
+
|
181
|
+
export const NoBackdropClose: Story = {
|
182
|
+
render: (args) => <InteractiveModal {...args} closeOnBackdropClick={false} />,
|
183
|
+
parameters: {
|
184
|
+
docs: {
|
185
|
+
description: {
|
186
|
+
story: 'Modal that cannot be closed by clicking the backdrop.',
|
187
|
+
},
|
188
|
+
},
|
189
|
+
},
|
190
|
+
};
|
191
|
+
|
192
|
+
export const ConfirmationDialog: Story = {
|
193
|
+
render: () => {
|
194
|
+
const [isOpen, setIsOpen] = useState(false);
|
195
|
+
|
196
|
+
return (
|
197
|
+
<Section>
|
198
|
+
<Button variant="destructive" onClick={() => setIsOpen(true)}>
|
199
|
+
Delete Item
|
200
|
+
</Button>
|
201
|
+
<Modal
|
202
|
+
isOpen={isOpen}
|
203
|
+
onClose={() => setIsOpen(false)}
|
204
|
+
size="small"
|
205
|
+
centered={true}
|
206
|
+
>
|
207
|
+
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
208
|
+
<h3 style={{ color: 'var(--latte-colors-error)', marginBottom: '1rem' }}>
|
209
|
+
Delete Confirmation
|
210
|
+
</h3>
|
211
|
+
<p style={{ marginBottom: '2rem' }}>
|
212
|
+
Are you sure you want to delete this item? This action cannot be undone.
|
213
|
+
</p>
|
214
|
+
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'center' }}>
|
215
|
+
<Button variant="destructive" onClick={() => setIsOpen(false)}>
|
216
|
+
Delete
|
217
|
+
</Button>
|
218
|
+
<Button variant="secondary" onClick={() => setIsOpen(false)}>
|
219
|
+
Cancel
|
220
|
+
</Button>
|
221
|
+
</div>
|
222
|
+
</div>
|
223
|
+
</Modal>
|
224
|
+
</Section>
|
225
|
+
);
|
226
|
+
},
|
227
|
+
parameters: {
|
228
|
+
docs: {
|
229
|
+
description: {
|
230
|
+
story: 'A real-world example of a confirmation dialog for destructive actions.',
|
231
|
+
},
|
232
|
+
},
|
233
|
+
},
|
234
|
+
};
|
235
|
+
|
236
|
+
export const FormModal: Story = {
|
237
|
+
render: () => {
|
238
|
+
const [isOpen, setIsOpen] = useState(false);
|
239
|
+
|
240
|
+
return (
|
241
|
+
<Section>
|
242
|
+
<Button variant="primary" onClick={() => setIsOpen(true)}>
|
243
|
+
Add New User
|
244
|
+
</Button>
|
245
|
+
<Modal
|
246
|
+
isOpen={isOpen}
|
247
|
+
onClose={() => setIsOpen(false)}
|
248
|
+
size="medium"
|
249
|
+
centered={true}
|
250
|
+
>
|
251
|
+
<div style={{ padding: '2rem' }}>
|
252
|
+
<h2 style={{ marginBottom: '2rem' }}>Add New User</h2>
|
253
|
+
<form style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
254
|
+
<div>
|
255
|
+
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}>
|
256
|
+
Name
|
257
|
+
</label>
|
258
|
+
<input
|
259
|
+
type="text"
|
260
|
+
style={{
|
261
|
+
width: '100%',
|
262
|
+
padding: '0.75rem',
|
263
|
+
border: '1px solid var(--latte-colors-border)',
|
264
|
+
borderRadius: 'var(--latte-radii-md)',
|
265
|
+
fontSize: 'var(--latte-fontSizes-md)'
|
266
|
+
}}
|
267
|
+
/>
|
268
|
+
</div>
|
269
|
+
<div>
|
270
|
+
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}>
|
271
|
+
Email
|
272
|
+
</label>
|
273
|
+
<input
|
274
|
+
type="email"
|
275
|
+
style={{
|
276
|
+
width: '100%',
|
277
|
+
padding: '0.75rem',
|
278
|
+
border: '1px solid var(--latte-colors-border)',
|
279
|
+
borderRadius: 'var(--latte-radii-md)',
|
280
|
+
fontSize: 'var(--latte-fontSizes-md)'
|
281
|
+
}}
|
282
|
+
/>
|
283
|
+
</div>
|
284
|
+
<div>
|
285
|
+
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}>
|
286
|
+
Role
|
287
|
+
</label>
|
288
|
+
<select
|
289
|
+
style={{
|
290
|
+
width: '100%',
|
291
|
+
padding: '0.75rem',
|
292
|
+
border: '1px solid var(--latte-colors-border)',
|
293
|
+
borderRadius: 'var(--latte-radii-md)',
|
294
|
+
fontSize: 'var(--latte-fontSizes-md)'
|
295
|
+
}}
|
296
|
+
>
|
297
|
+
<option>User</option>
|
298
|
+
<option>Admin</option>
|
299
|
+
<option>Moderator</option>
|
300
|
+
</select>
|
301
|
+
</div>
|
302
|
+
<div style={{ marginTop: '2rem', display: 'flex', gap: '1rem', justifyContent: 'flex-end' }}>
|
303
|
+
<Button variant="secondary" onClick={() => setIsOpen(false)}>
|
304
|
+
Cancel
|
305
|
+
</Button>
|
306
|
+
<Button variant="primary" onClick={() => setIsOpen(false)}>
|
307
|
+
Add User
|
308
|
+
</Button>
|
309
|
+
</div>
|
310
|
+
</form>
|
311
|
+
</div>
|
312
|
+
</Modal>
|
313
|
+
</Section>
|
314
|
+
);
|
315
|
+
},
|
316
|
+
parameters: {
|
317
|
+
docs: {
|
318
|
+
description: {
|
319
|
+
story: 'A practical example of using a modal for form input.',
|
320
|
+
},
|
321
|
+
},
|
322
|
+
},
|
323
|
+
};
|
324
|
+
|
325
|
+
export const ImageGallery: Story = {
|
326
|
+
render: () => {
|
327
|
+
const [isOpen, setIsOpen] = useState(false);
|
328
|
+
const [selectedImage, setSelectedImage] = useState(0);
|
329
|
+
|
330
|
+
const images = [
|
331
|
+
'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop',
|
332
|
+
'https://images.unsplash.com/photo-1519904981063-b0cf448d479e?w=800&h=600&fit=crop',
|
333
|
+
'https://images.unsplash.com/photo-1454391304352-2bf4678b1a7a?w=800&h=600&fit=crop',
|
334
|
+
];
|
335
|
+
|
336
|
+
const openImage = (index: number) => {
|
337
|
+
setSelectedImage(index);
|
338
|
+
setIsOpen(true);
|
339
|
+
};
|
340
|
+
|
341
|
+
return (
|
342
|
+
<Section>
|
343
|
+
<h3>Click an image to view in modal:</h3>
|
344
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem', marginTop: '1rem' }}>
|
345
|
+
{images.map((src, index) => (
|
346
|
+
<img
|
347
|
+
key={index}
|
348
|
+
src={src}
|
349
|
+
alt={`Gallery ${index + 1}`}
|
350
|
+
style={{
|
351
|
+
width: '100%',
|
352
|
+
height: '150px',
|
353
|
+
objectFit: 'cover',
|
354
|
+
borderRadius: 'var(--latte-radii-md)',
|
355
|
+
cursor: 'pointer'
|
356
|
+
}}
|
357
|
+
onClick={() => openImage(index)}
|
358
|
+
/>
|
359
|
+
))}
|
360
|
+
</div>
|
361
|
+
<Modal
|
362
|
+
isOpen={isOpen}
|
363
|
+
onClose={() => setIsOpen(false)}
|
364
|
+
size="large"
|
365
|
+
centered={true}
|
366
|
+
>
|
367
|
+
<div style={{ padding: '1rem' }}>
|
368
|
+
<img
|
369
|
+
src={images[selectedImage]}
|
370
|
+
alt={`Gallery ${selectedImage + 1}`}
|
371
|
+
style={{
|
372
|
+
width: '100%',
|
373
|
+
height: 'auto',
|
374
|
+
borderRadius: 'var(--latte-radii-md)'
|
375
|
+
}}
|
376
|
+
/>
|
377
|
+
<div style={{
|
378
|
+
marginTop: '1rem',
|
379
|
+
display: 'flex',
|
380
|
+
justifyContent: 'space-between',
|
381
|
+
alignItems: 'center'
|
382
|
+
}}>
|
383
|
+
<Button
|
384
|
+
variant="ghost"
|
385
|
+
onClick={() => setSelectedImage((prev) => (prev > 0 ? prev - 1 : images.length - 1))}
|
386
|
+
>
|
387
|
+
← Previous
|
388
|
+
</Button>
|
389
|
+
<span>Image {selectedImage + 1} of {images.length}</span>
|
390
|
+
<Button
|
391
|
+
variant="ghost"
|
392
|
+
onClick={() => setSelectedImage((prev) => (prev < images.length - 1 ? prev + 1 : 0))}
|
393
|
+
>
|
394
|
+
Next →
|
395
|
+
</Button>
|
396
|
+
</div>
|
397
|
+
</div>
|
398
|
+
</Modal>
|
399
|
+
</Section>
|
400
|
+
);
|
401
|
+
},
|
402
|
+
parameters: {
|
403
|
+
docs: {
|
404
|
+
description: {
|
405
|
+
story: 'An example of using modals for image galleries with navigation.',
|
406
|
+
},
|
407
|
+
},
|
408
|
+
},
|
409
|
+
};
|