@duro-app/ui 0.18.0 → 0.20.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/dist/components/Checkbox/Checkbox.meta.d.ts.map +1 -1
- package/dist/components/Cluster/Cluster.meta.d.ts.map +1 -1
- package/dist/components/DetailPanel/DetailPanel.d.ts +53 -0
- package/dist/components/DetailPanel/DetailPanel.d.ts.map +1 -0
- package/dist/components/DetailPanel/DetailPanel.meta.d.ts +3 -0
- package/dist/components/DetailPanel/DetailPanel.meta.d.ts.map +1 -0
- package/dist/components/DetailPanel/DetailPanel.stories.d.ts +9 -0
- package/dist/components/DetailPanel/DetailPanel.stories.d.ts.map +1 -0
- package/dist/components/DetailPanel/styles.css.d.ts +126 -0
- package/dist/components/DetailPanel/styles.css.d.ts.map +1 -0
- package/dist/components/Dialog/Dialog.d.ts +58 -0
- package/dist/components/Dialog/Dialog.d.ts.map +1 -0
- package/dist/components/Dialog/Dialog.meta.d.ts +3 -0
- package/dist/components/Dialog/Dialog.meta.d.ts.map +1 -0
- package/dist/components/Dialog/Dialog.stories.d.ts +13 -0
- package/dist/components/Dialog/Dialog.stories.d.ts.map +1 -0
- package/dist/components/Dialog/styles.css.d.ts +140 -0
- package/dist/components/Dialog/styles.css.d.ts.map +1 -0
- package/dist/components/Drawer/Drawer.d.ts +62 -0
- package/dist/components/Drawer/Drawer.d.ts.map +1 -0
- package/dist/components/Drawer/Drawer.meta.d.ts +3 -0
- package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -0
- package/dist/components/Drawer/Drawer.stories.d.ts +15 -0
- package/dist/components/Drawer/Drawer.stories.d.ts.map +1 -0
- package/dist/components/Drawer/styles.css.d.ts +184 -0
- package/dist/components/Drawer/styles.css.d.ts.map +1 -0
- package/dist/components/Drawer/useSwipeDismiss.d.ts +17 -0
- package/dist/components/Drawer/useSwipeDismiss.d.ts.map +1 -0
- package/dist/components/Field/Field.meta.d.ts.map +1 -1
- package/dist/components/Fieldset/Fieldset.meta.d.ts.map +1 -1
- package/dist/components/Form/Form.meta.d.ts.map +1 -1
- package/dist/components/Grid/Grid.meta.d.ts.map +1 -1
- package/dist/components/Inline/Inline.meta.d.ts.map +1 -1
- package/dist/components/Input/Input.meta.d.ts.map +1 -1
- package/dist/components/LinkButton/LinkButton.meta.d.ts.map +1 -1
- package/dist/components/Menu/Menu.meta.d.ts.map +1 -1
- package/dist/components/Panel/Panel.d.ts +27 -0
- package/dist/components/Panel/Panel.d.ts.map +1 -0
- package/dist/components/Panel/Panel.meta.d.ts +3 -0
- package/dist/components/Panel/Panel.meta.d.ts.map +1 -0
- package/dist/components/Panel/Panel.stories.d.ts +10 -0
- package/dist/components/Panel/Panel.stories.d.ts.map +1 -0
- package/dist/components/Panel/styles.css.d.ts +50 -0
- package/dist/components/Panel/styles.css.d.ts.map +1 -0
- package/dist/components/SideNav/SideNav.meta.d.ts.map +1 -1
- package/dist/components/Stack/Stack.meta.d.ts.map +1 -1
- package/dist/components/Switch/Switch.meta.d.ts.map +1 -1
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/Table/Table.stories.d.ts +1 -0
- package/dist/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/components/Textarea/Textarea.meta.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3286 -2007
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Checkbox/Checkbox.meta.ts +4 -1
- package/src/components/Cluster/Cluster.meta.ts +2 -6
- package/src/components/DetailPanel/DetailPanel.meta.ts +41 -0
- package/src/components/DetailPanel/DetailPanel.stories.tsx +270 -0
- package/src/components/DetailPanel/DetailPanel.tsx +312 -0
- package/src/components/DetailPanel/styles.css.ts +187 -0
- package/src/components/Dialog/Dialog.meta.ts +44 -0
- package/src/components/Dialog/Dialog.stories.tsx +325 -0
- package/src/components/Dialog/Dialog.tsx +310 -0
- package/src/components/Dialog/styles.css.ts +197 -0
- package/src/components/Drawer/Drawer.meta.ts +45 -0
- package/src/components/Drawer/Drawer.stories.tsx +364 -0
- package/src/components/Drawer/Drawer.tsx +370 -0
- package/src/components/Drawer/styles.css.ts +242 -0
- package/src/components/Drawer/useSwipeDismiss.ts +295 -0
- package/src/components/Field/Field.meta.ts +1 -3
- package/src/components/Fieldset/Fieldset.meta.ts +1 -3
- package/src/components/Form/Form.meta.ts +1 -4
- package/src/components/Grid/Grid.meta.ts +1 -3
- package/src/components/Inline/Inline.meta.ts +1 -2
- package/src/components/Input/Input.meta.ts +2 -7
- package/src/components/LinkButton/LinkButton.meta.ts +1 -3
- package/src/components/Menu/Menu.meta.ts +1 -4
- package/src/components/Panel/Panel.meta.ts +37 -0
- package/src/components/Panel/Panel.stories.tsx +124 -0
- package/src/components/Panel/Panel.tsx +56 -0
- package/src/components/Panel/styles.css.ts +55 -0
- package/src/components/SideNav/SideNav.meta.ts +1 -4
- package/src/components/Stack/Stack.meta.ts +1 -2
- package/src/components/Switch/Switch.meta.ts +4 -1
- package/src/components/Table/Table.stories.tsx +107 -0
- package/src/components/Table/Table.tsx +0 -1
- package/src/components/Textarea/Textarea.meta.ts +2 -6
- package/src/index.ts +4 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ReactNode,
|
|
3
|
+
type RefObject,
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useState,
|
|
7
|
+
useEffect,
|
|
8
|
+
useCallback,
|
|
9
|
+
useId,
|
|
10
|
+
useRef,
|
|
11
|
+
} from 'react'
|
|
12
|
+
import {html} from 'react-strict-dom'
|
|
13
|
+
import {styles} from './styles.css'
|
|
14
|
+
|
|
15
|
+
// --- Types ---
|
|
16
|
+
|
|
17
|
+
export type DialogSize = 'sm' | 'md' | 'lg'
|
|
18
|
+
|
|
19
|
+
// --- Context ---
|
|
20
|
+
|
|
21
|
+
interface DialogContextValue {
|
|
22
|
+
open: boolean
|
|
23
|
+
closing: boolean
|
|
24
|
+
dismissable: boolean
|
|
25
|
+
requestOpen: () => void
|
|
26
|
+
requestClose: () => void
|
|
27
|
+
titleId: string
|
|
28
|
+
descriptionId: string
|
|
29
|
+
popupRef: RefObject<HTMLDivElement | null>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DialogContext = createContext<DialogContextValue | null>(null)
|
|
33
|
+
|
|
34
|
+
function useDialog() {
|
|
35
|
+
const ctx = useContext(DialogContext)
|
|
36
|
+
if (!ctx) throw new Error('Dialog compound components must be used within Dialog.Root')
|
|
37
|
+
return ctx
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- Root ---
|
|
41
|
+
|
|
42
|
+
interface RootProps {
|
|
43
|
+
children: ReactNode
|
|
44
|
+
/** Controlled open state */
|
|
45
|
+
open?: boolean
|
|
46
|
+
/** Default open state (uncontrolled) */
|
|
47
|
+
defaultOpen?: boolean
|
|
48
|
+
/** Called when dialog open state changes */
|
|
49
|
+
onOpenChange?: (open: boolean) => void
|
|
50
|
+
/** Whether clicking backdrop closes the dialog. Default: true */
|
|
51
|
+
dismissable?: boolean
|
|
52
|
+
/** Duration of the close animation in ms. Default: 140 */
|
|
53
|
+
closeAnimationDuration?: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Root({
|
|
57
|
+
children,
|
|
58
|
+
open: controlledOpen,
|
|
59
|
+
defaultOpen = false,
|
|
60
|
+
onOpenChange,
|
|
61
|
+
dismissable = true,
|
|
62
|
+
closeAnimationDuration = 140,
|
|
63
|
+
}: RootProps) {
|
|
64
|
+
const isControlled = controlledOpen !== undefined
|
|
65
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen)
|
|
66
|
+
const [closing, setClosing] = useState(false)
|
|
67
|
+
const closingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
68
|
+
const popupRef = useRef<HTMLDivElement>(null)
|
|
69
|
+
|
|
70
|
+
const isOpen = isControlled ? controlledOpen : internalOpen
|
|
71
|
+
|
|
72
|
+
const titleId = useId()
|
|
73
|
+
const descriptionId = useId()
|
|
74
|
+
|
|
75
|
+
const requestOpen = useCallback(() => {
|
|
76
|
+
if (closingTimerRef.current) {
|
|
77
|
+
clearTimeout(closingTimerRef.current)
|
|
78
|
+
closingTimerRef.current = null
|
|
79
|
+
}
|
|
80
|
+
setClosing(false)
|
|
81
|
+
if (!isControlled) setInternalOpen(true)
|
|
82
|
+
onOpenChange?.(true)
|
|
83
|
+
}, [isControlled, onOpenChange])
|
|
84
|
+
|
|
85
|
+
const requestClose = useCallback(() => {
|
|
86
|
+
setClosing(true)
|
|
87
|
+
closingTimerRef.current = setTimeout(() => {
|
|
88
|
+
setClosing(false)
|
|
89
|
+
closingTimerRef.current = null
|
|
90
|
+
if (!isControlled) setInternalOpen(false)
|
|
91
|
+
onOpenChange?.(false)
|
|
92
|
+
}, closeAnimationDuration)
|
|
93
|
+
}, [isControlled, onOpenChange, closeAnimationDuration])
|
|
94
|
+
|
|
95
|
+
// If controlled open goes false externally, trigger close animation
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (isControlled && !controlledOpen && !closing) {
|
|
98
|
+
// Already closed, nothing to animate
|
|
99
|
+
}
|
|
100
|
+
}, [isControlled, controlledOpen, closing])
|
|
101
|
+
|
|
102
|
+
// Clean up timer on unmount
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
return () => {
|
|
105
|
+
if (closingTimerRef.current) clearTimeout(closingTimerRef.current)
|
|
106
|
+
}
|
|
107
|
+
}, [])
|
|
108
|
+
|
|
109
|
+
// Handle Escape key
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!isOpen && !closing) return
|
|
112
|
+
if (!dismissable) return
|
|
113
|
+
|
|
114
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
115
|
+
if (e.key === 'Escape') {
|
|
116
|
+
e.stopPropagation()
|
|
117
|
+
requestClose()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
document.addEventListener('keydown', onKeyDown)
|
|
121
|
+
return () => document.removeEventListener('keydown', onKeyDown)
|
|
122
|
+
}, [isOpen, closing, dismissable, requestClose])
|
|
123
|
+
|
|
124
|
+
// Lock body scroll when open
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (isOpen || closing) {
|
|
127
|
+
const prev = document.body.style.overflow
|
|
128
|
+
document.body.style.overflow = 'hidden'
|
|
129
|
+
return () => {
|
|
130
|
+
document.body.style.overflow = prev
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}, [isOpen, closing])
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<DialogContext.Provider
|
|
137
|
+
value={{
|
|
138
|
+
open: isOpen || closing,
|
|
139
|
+
closing,
|
|
140
|
+
dismissable,
|
|
141
|
+
requestOpen,
|
|
142
|
+
requestClose,
|
|
143
|
+
titleId,
|
|
144
|
+
descriptionId,
|
|
145
|
+
popupRef,
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
{children}
|
|
149
|
+
</DialogContext.Provider>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- Trigger ---
|
|
154
|
+
|
|
155
|
+
interface TriggerProps {
|
|
156
|
+
children: ReactNode
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function Trigger({children}: TriggerProps) {
|
|
160
|
+
const {requestOpen} = useDialog()
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<html.div onClick={requestOpen} style={styles.inlineWrapper}>
|
|
164
|
+
{children}
|
|
165
|
+
</html.div>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- Portal (renders backdrop + viewport + popup) ---
|
|
170
|
+
|
|
171
|
+
interface PortalProps {
|
|
172
|
+
children: ReactNode
|
|
173
|
+
size?: DialogSize
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function Portal({children, size = 'md'}: PortalProps) {
|
|
177
|
+
const {open, closing, dismissable, requestClose, titleId, descriptionId, popupRef} = useDialog()
|
|
178
|
+
|
|
179
|
+
const handleBackdropClick = useCallback(() => {
|
|
180
|
+
if (dismissable) {
|
|
181
|
+
requestClose()
|
|
182
|
+
}
|
|
183
|
+
}, [dismissable, requestClose])
|
|
184
|
+
|
|
185
|
+
if (!open) return null
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<>
|
|
189
|
+
{/* Backdrop — click to dismiss */}
|
|
190
|
+
<html.div
|
|
191
|
+
style={[
|
|
192
|
+
styles.backdrop,
|
|
193
|
+
closing && styles.backdropClosing,
|
|
194
|
+
!closing && styles.backdropOpen,
|
|
195
|
+
]}
|
|
196
|
+
aria-hidden
|
|
197
|
+
onClick={handleBackdropClick}
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
{/* Viewport (centering container, pointer-events: none) */}
|
|
201
|
+
<html.div style={styles.viewport}>
|
|
202
|
+
<html.div
|
|
203
|
+
ref={popupRef}
|
|
204
|
+
role="dialog"
|
|
205
|
+
aria-modal="true"
|
|
206
|
+
aria-labelledby={titleId}
|
|
207
|
+
aria-describedby={descriptionId}
|
|
208
|
+
style={[
|
|
209
|
+
styles.popup,
|
|
210
|
+
styles[size],
|
|
211
|
+
closing && styles.popupClosing,
|
|
212
|
+
!closing && styles.popupOpen,
|
|
213
|
+
]}
|
|
214
|
+
>
|
|
215
|
+
{children}
|
|
216
|
+
</html.div>
|
|
217
|
+
</html.div>
|
|
218
|
+
</>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// --- Header ---
|
|
223
|
+
|
|
224
|
+
function Header({children}: {children: ReactNode}) {
|
|
225
|
+
return <html.div style={styles.header}>{children}</html.div>
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- Title ---
|
|
229
|
+
|
|
230
|
+
function Title({children}: {children: ReactNode}) {
|
|
231
|
+
const {titleId} = useDialog()
|
|
232
|
+
return (
|
|
233
|
+
<html.h2 id={titleId} style={styles.title}>
|
|
234
|
+
{children}
|
|
235
|
+
</html.h2>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// --- Description ---
|
|
240
|
+
|
|
241
|
+
function Description({children}: {children: ReactNode}) {
|
|
242
|
+
const {descriptionId} = useDialog()
|
|
243
|
+
return (
|
|
244
|
+
<html.p id={descriptionId} style={styles.description}>
|
|
245
|
+
{children}
|
|
246
|
+
</html.p>
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- Body ---
|
|
251
|
+
|
|
252
|
+
function Body({children}: {children: ReactNode}) {
|
|
253
|
+
return <html.div style={styles.body}>{children}</html.div>
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// --- Footer ---
|
|
257
|
+
|
|
258
|
+
function Footer({children}: {children: ReactNode}) {
|
|
259
|
+
return <html.div style={styles.footer}>{children}</html.div>
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- Close ---
|
|
263
|
+
|
|
264
|
+
interface CloseProps {
|
|
265
|
+
children?: ReactNode
|
|
266
|
+
'aria-label'?: string
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function Close({children, 'aria-label': ariaLabel = 'Close'}: CloseProps) {
|
|
270
|
+
const {requestClose} = useDialog()
|
|
271
|
+
|
|
272
|
+
if (children) {
|
|
273
|
+
return (
|
|
274
|
+
<html.div onClick={requestClose} style={styles.inlineWrapper}>
|
|
275
|
+
{children}
|
|
276
|
+
</html.div>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<html.button
|
|
282
|
+
onClick={requestClose}
|
|
283
|
+
aria-label={ariaLabel}
|
|
284
|
+
style={styles.closeButton}
|
|
285
|
+
>
|
|
286
|
+
<svg width={16} height={16} viewBox="0 0 16 16" fill="none">
|
|
287
|
+
<path
|
|
288
|
+
d="M4 4l8 8M12 4l-8 8"
|
|
289
|
+
stroke="currentColor"
|
|
290
|
+
strokeWidth={1.5}
|
|
291
|
+
strokeLinecap="round"
|
|
292
|
+
/>
|
|
293
|
+
</svg>
|
|
294
|
+
</html.button>
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// --- Export ---
|
|
299
|
+
|
|
300
|
+
export const Dialog = {
|
|
301
|
+
Root,
|
|
302
|
+
Trigger,
|
|
303
|
+
Portal,
|
|
304
|
+
Header,
|
|
305
|
+
Title,
|
|
306
|
+
Description,
|
|
307
|
+
Body,
|
|
308
|
+
Footer,
|
|
309
|
+
Close,
|
|
310
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
|
+
import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
6
|
+
|
|
7
|
+
export const styles = css.create({
|
|
8
|
+
// --- Backdrop ---
|
|
9
|
+
backdrop: {
|
|
10
|
+
position: 'fixed',
|
|
11
|
+
inset: 0,
|
|
12
|
+
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
|
13
|
+
zIndex: 1000,
|
|
14
|
+
},
|
|
15
|
+
backdropOpen: {
|
|
16
|
+
opacity: {
|
|
17
|
+
default: 1,
|
|
18
|
+
},
|
|
19
|
+
animationName: css.keyframes({
|
|
20
|
+
from: {opacity: 0},
|
|
21
|
+
to: {opacity: 1},
|
|
22
|
+
}),
|
|
23
|
+
animationDuration: '180ms',
|
|
24
|
+
animationTimingFunction: 'ease-out',
|
|
25
|
+
animationFillMode: 'both',
|
|
26
|
+
},
|
|
27
|
+
backdropClosing: {
|
|
28
|
+
opacity: {
|
|
29
|
+
default: 0,
|
|
30
|
+
},
|
|
31
|
+
animationName: css.keyframes({
|
|
32
|
+
from: {opacity: 1},
|
|
33
|
+
to: {opacity: 0},
|
|
34
|
+
}),
|
|
35
|
+
animationDuration: '140ms',
|
|
36
|
+
animationTimingFunction: 'ease-in',
|
|
37
|
+
animationFillMode: 'both',
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// --- Viewport (centering container) ---
|
|
41
|
+
viewport: {
|
|
42
|
+
position: 'fixed',
|
|
43
|
+
inset: 0,
|
|
44
|
+
zIndex: 1001,
|
|
45
|
+
display: 'flex',
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
justifyContent: 'center',
|
|
48
|
+
padding: spacing.lg,
|
|
49
|
+
// Allow clicking backdrop through viewport when clicking outside popup
|
|
50
|
+
pointerEvents: 'none',
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// --- Popup ---
|
|
54
|
+
popup: {
|
|
55
|
+
position: 'relative',
|
|
56
|
+
backgroundColor: colors.bgCard,
|
|
57
|
+
borderRadius: radii.lg,
|
|
58
|
+
boxShadow: shadows.lg,
|
|
59
|
+
width: '100%',
|
|
60
|
+
maxHeight: '85vh',
|
|
61
|
+
display: 'flex',
|
|
62
|
+
flexDirection: 'column',
|
|
63
|
+
overflow: 'hidden',
|
|
64
|
+
pointerEvents: 'auto',
|
|
65
|
+
borderWidth: 1,
|
|
66
|
+
borderStyle: 'solid',
|
|
67
|
+
borderColor: colors.border,
|
|
68
|
+
},
|
|
69
|
+
popupOpen: {
|
|
70
|
+
opacity: {
|
|
71
|
+
default: 1,
|
|
72
|
+
},
|
|
73
|
+
animationName: css.keyframes({
|
|
74
|
+
from: {
|
|
75
|
+
opacity: 0,
|
|
76
|
+
transform: 'scale(0.97) translateY(8px)',
|
|
77
|
+
},
|
|
78
|
+
to: {
|
|
79
|
+
opacity: 1,
|
|
80
|
+
transform: 'scale(1) translateY(0)',
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
animationDuration: '180ms',
|
|
84
|
+
animationTimingFunction: 'ease-out',
|
|
85
|
+
animationFillMode: 'both',
|
|
86
|
+
},
|
|
87
|
+
popupClosing: {
|
|
88
|
+
opacity: {
|
|
89
|
+
default: 0,
|
|
90
|
+
},
|
|
91
|
+
animationName: css.keyframes({
|
|
92
|
+
from: {
|
|
93
|
+
opacity: 1,
|
|
94
|
+
transform: 'scale(1) translateY(0)',
|
|
95
|
+
},
|
|
96
|
+
to: {
|
|
97
|
+
opacity: 0,
|
|
98
|
+
transform: 'scale(0.97) translateY(8px)',
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
animationDuration: '140ms',
|
|
102
|
+
animationTimingFunction: 'ease-in',
|
|
103
|
+
animationFillMode: 'both',
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// --- Sizes ---
|
|
107
|
+
sm: {maxWidth: 400},
|
|
108
|
+
md: {maxWidth: 520},
|
|
109
|
+
lg: {maxWidth: 680},
|
|
110
|
+
|
|
111
|
+
// --- Header ---
|
|
112
|
+
header: {
|
|
113
|
+
display: 'flex',
|
|
114
|
+
alignItems: 'flex-start',
|
|
115
|
+
justifyContent: 'space-between',
|
|
116
|
+
paddingTop: spacing.lg,
|
|
117
|
+
paddingLeft: spacing.lg,
|
|
118
|
+
paddingRight: spacing.lg,
|
|
119
|
+
paddingBottom: spacing.md,
|
|
120
|
+
gap: spacing.md,
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// --- Title ---
|
|
124
|
+
title: {
|
|
125
|
+
fontSize: typography.fontSizeLg,
|
|
126
|
+
fontWeight: typography.fontWeightSemibold,
|
|
127
|
+
lineHeight: typography.lineHeight,
|
|
128
|
+
color: colors.text,
|
|
129
|
+
margin: 0,
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// --- Description ---
|
|
133
|
+
description: {
|
|
134
|
+
fontSize: typography.fontSizeSm,
|
|
135
|
+
lineHeight: typography.lineHeight,
|
|
136
|
+
color: colors.textMuted,
|
|
137
|
+
margin: 0,
|
|
138
|
+
paddingLeft: spacing.lg,
|
|
139
|
+
paddingRight: spacing.lg,
|
|
140
|
+
paddingBottom: spacing.md,
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
// --- Body ---
|
|
144
|
+
body: {
|
|
145
|
+
paddingLeft: spacing.lg,
|
|
146
|
+
paddingRight: spacing.lg,
|
|
147
|
+
paddingBottom: spacing.lg,
|
|
148
|
+
overflowY: 'auto',
|
|
149
|
+
flex: 1,
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// --- Footer ---
|
|
153
|
+
footer: {
|
|
154
|
+
display: 'flex',
|
|
155
|
+
alignItems: 'center',
|
|
156
|
+
justifyContent: 'flex-end',
|
|
157
|
+
gap: spacing.sm,
|
|
158
|
+
paddingTop: spacing.md,
|
|
159
|
+
paddingLeft: spacing.lg,
|
|
160
|
+
paddingRight: spacing.lg,
|
|
161
|
+
paddingBottom: spacing.lg,
|
|
162
|
+
borderTopWidth: 1,
|
|
163
|
+
borderTopStyle: 'solid',
|
|
164
|
+
borderTopColor: colors.border,
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
// --- Close button ---
|
|
168
|
+
closeButton: {
|
|
169
|
+
display: 'flex',
|
|
170
|
+
alignItems: 'center',
|
|
171
|
+
justifyContent: 'center',
|
|
172
|
+
width: 32,
|
|
173
|
+
height: 32,
|
|
174
|
+
borderRadius: radii.sm,
|
|
175
|
+
borderWidth: 0,
|
|
176
|
+
backgroundColor: {
|
|
177
|
+
default: 'transparent',
|
|
178
|
+
':hover': colors.bgCardHover,
|
|
179
|
+
},
|
|
180
|
+
color: colors.textMuted,
|
|
181
|
+
cursor: 'pointer',
|
|
182
|
+
padding: 0,
|
|
183
|
+
flexShrink: 0,
|
|
184
|
+
transitionProperty: 'background-color',
|
|
185
|
+
transitionDuration: '150ms',
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
// --- Inline wrapper (for Trigger / Close children) ---
|
|
189
|
+
inlineWrapper: {
|
|
190
|
+
display: 'inline-flex',
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
// --- Reduced motion ---
|
|
194
|
+
reducedMotion: {
|
|
195
|
+
animationDuration: '1ms',
|
|
196
|
+
},
|
|
197
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Modal sliding panel from a screen edge (right, left, or bottom). Slides in with backdrop overlay. Supports swipe-to-dismiss gestures on mobile, backdrop click dismiss, and Escape key. Use for secondary workflows, settings panels, or mobile-friendly detail views.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Settings or configuration panels that slide in from the side',
|
|
8
|
+
'Mobile-friendly detail views (bottom sheet pattern)',
|
|
9
|
+
'Secondary workflows that need more space than a dialog',
|
|
10
|
+
'Filters, navigation drawers, or form panels',
|
|
11
|
+
],
|
|
12
|
+
whenNotToUse: [
|
|
13
|
+
'Centered modal confirmations — use Dialog',
|
|
14
|
+
'Non-modal side inspection panels — use DetailPanel',
|
|
15
|
+
'Inline content expansion — use disclosure patterns',
|
|
16
|
+
'Primary navigation — use SideNav',
|
|
17
|
+
],
|
|
18
|
+
anatomy: {
|
|
19
|
+
required: ['Root', 'Portal'],
|
|
20
|
+
optional: ['Trigger', 'Header', 'Title', 'Description', 'Body', 'Footer', 'Close'],
|
|
21
|
+
},
|
|
22
|
+
relatedTo: [
|
|
23
|
+
{component: 'Dialog', relationship: 'Dialog is centered modal; Drawer slides from an edge'},
|
|
24
|
+
{component: 'DetailPanel', relationship: 'DetailPanel is non-modal and in-flow; Drawer is modal with backdrop'},
|
|
25
|
+
],
|
|
26
|
+
example: `<Drawer.Root anchor="right" open={open} onOpenChange={setOpen}>
|
|
27
|
+
<Drawer.Trigger>
|
|
28
|
+
<Button>Open drawer</Button>
|
|
29
|
+
</Drawer.Trigger>
|
|
30
|
+
<Drawer.Portal size="md">
|
|
31
|
+
<Drawer.Header>
|
|
32
|
+
<Drawer.Title>Settings</Drawer.Title>
|
|
33
|
+
<Drawer.Close />
|
|
34
|
+
</Drawer.Header>
|
|
35
|
+
<Drawer.Body>
|
|
36
|
+
<Stack gap="md">
|
|
37
|
+
<Text>Drawer content goes here.</Text>
|
|
38
|
+
</Stack>
|
|
39
|
+
</Drawer.Body>
|
|
40
|
+
<Drawer.Footer>
|
|
41
|
+
<Button variant="secondary" onClick={() => setOpen(false)}>Done</Button>
|
|
42
|
+
</Drawer.Footer>
|
|
43
|
+
</Drawer.Portal>
|
|
44
|
+
</Drawer.Root>`,
|
|
45
|
+
}
|