@duro-app/ui 0.17.1 → 0.19.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.
Files changed (70) hide show
  1. package/dist/components/ActionBar/ActionBar.d.ts +1 -1
  2. package/dist/components/ActionBar/ActionBar.d.ts.map +1 -1
  3. package/dist/components/ActionBar/ActionBarProvider.d.ts +12 -0
  4. package/dist/components/ActionBar/ActionBarProvider.d.ts.map +1 -0
  5. package/dist/components/Checkbox/Checkbox.meta.d.ts.map +1 -1
  6. package/dist/components/Cluster/Cluster.meta.d.ts.map +1 -1
  7. package/dist/components/Dialog/Dialog.d.ts +58 -0
  8. package/dist/components/Dialog/Dialog.d.ts.map +1 -0
  9. package/dist/components/Dialog/Dialog.stories.d.ts +13 -0
  10. package/dist/components/Dialog/Dialog.stories.d.ts.map +1 -0
  11. package/dist/components/Dialog/styles.css.d.ts +140 -0
  12. package/dist/components/Dialog/styles.css.d.ts.map +1 -0
  13. package/dist/components/Drawer/Drawer.d.ts +62 -0
  14. package/dist/components/Drawer/Drawer.d.ts.map +1 -0
  15. package/dist/components/Drawer/Drawer.stories.d.ts +15 -0
  16. package/dist/components/Drawer/Drawer.stories.d.ts.map +1 -0
  17. package/dist/components/Drawer/styles.css.d.ts +184 -0
  18. package/dist/components/Drawer/styles.css.d.ts.map +1 -0
  19. package/dist/components/Drawer/useSwipeDismiss.d.ts +17 -0
  20. package/dist/components/Drawer/useSwipeDismiss.d.ts.map +1 -0
  21. package/dist/components/Field/Field.meta.d.ts.map +1 -1
  22. package/dist/components/Fieldset/Fieldset.meta.d.ts.map +1 -1
  23. package/dist/components/Form/Form.meta.d.ts.map +1 -1
  24. package/dist/components/Grid/Grid.meta.d.ts.map +1 -1
  25. package/dist/components/Inline/Inline.meta.d.ts.map +1 -1
  26. package/dist/components/Input/Input.meta.d.ts.map +1 -1
  27. package/dist/components/LinkButton/LinkButton.meta.d.ts.map +1 -1
  28. package/dist/components/Menu/Menu.meta.d.ts.map +1 -1
  29. package/dist/components/SideNav/SideNav.meta.d.ts.map +1 -1
  30. package/dist/components/Stack/Stack.meta.d.ts.map +1 -1
  31. package/dist/components/Switch/Switch.meta.d.ts.map +1 -1
  32. package/dist/components/Table/Table.d.ts.map +1 -1
  33. package/dist/components/Table/Table.stories.d.ts +1 -0
  34. package/dist/components/Table/Table.stories.d.ts.map +1 -1
  35. package/dist/components/Textarea/Textarea.meta.d.ts.map +1 -1
  36. package/dist/components/ThemeProvider/ThemeProvider.d.ts +0 -2
  37. package/dist/components/ThemeProvider/ThemeProvider.d.ts.map +1 -1
  38. package/dist/index.css +1 -1
  39. package/dist/index.d.ts +4 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2869 -1904
  42. package/dist/index.js.map +1 -1
  43. package/package.json +2 -2
  44. package/src/components/ActionBar/ActionBar.tsx +28 -12
  45. package/src/components/ActionBar/ActionBarProvider.tsx +100 -0
  46. package/src/components/Checkbox/Checkbox.meta.ts +4 -1
  47. package/src/components/Cluster/Cluster.meta.ts +2 -6
  48. package/src/components/Dialog/Dialog.stories.tsx +325 -0
  49. package/src/components/Dialog/Dialog.tsx +310 -0
  50. package/src/components/Dialog/styles.css.ts +197 -0
  51. package/src/components/Drawer/Drawer.stories.tsx +364 -0
  52. package/src/components/Drawer/Drawer.tsx +370 -0
  53. package/src/components/Drawer/styles.css.ts +242 -0
  54. package/src/components/Drawer/useSwipeDismiss.ts +295 -0
  55. package/src/components/Field/Field.meta.ts +1 -3
  56. package/src/components/Fieldset/Fieldset.meta.ts +1 -3
  57. package/src/components/Form/Form.meta.ts +1 -4
  58. package/src/components/Grid/Grid.meta.ts +1 -3
  59. package/src/components/Inline/Inline.meta.ts +1 -2
  60. package/src/components/Input/Input.meta.ts +2 -7
  61. package/src/components/LinkButton/LinkButton.meta.ts +1 -3
  62. package/src/components/Menu/Menu.meta.ts +1 -4
  63. package/src/components/SideNav/SideNav.meta.ts +1 -4
  64. package/src/components/Stack/Stack.meta.ts +1 -2
  65. package/src/components/Switch/Switch.meta.ts +4 -1
  66. package/src/components/Table/Table.stories.tsx +107 -0
  67. package/src/components/Table/Table.tsx +0 -1
  68. package/src/components/Textarea/Textarea.meta.ts +2 -6
  69. package/src/components/ThemeProvider/ThemeProvider.tsx +2 -19
  70. package/src/index.ts +4 -5
@@ -0,0 +1,370 @@
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
+ import {useSwipeDismiss} from './useSwipeDismiss'
15
+
16
+ // --- Types ---
17
+
18
+ export type DrawerAnchor = 'right' | 'left' | 'bottom'
19
+ export type DrawerSize = 'sm' | 'md' | 'lg'
20
+
21
+ // --- Context ---
22
+
23
+ interface DrawerContextValue {
24
+ open: boolean
25
+ closing: boolean
26
+ anchor: DrawerAnchor
27
+ dismissable: boolean
28
+ swipeDismiss: boolean
29
+ requestOpen: () => void
30
+ requestClose: () => void
31
+ /** Close immediately without CSS animation (used by swipe dismiss which handles its own exit animation) */
32
+ requestCloseImmediate: () => void
33
+ titleId: string
34
+ descriptionId: string
35
+ panelRef: RefObject<HTMLDivElement | null>
36
+ }
37
+
38
+ const DrawerContext = createContext<DrawerContextValue | null>(null)
39
+
40
+ function useDrawer() {
41
+ const ctx = useContext(DrawerContext)
42
+ if (!ctx) throw new Error('Drawer compound components must be used within Drawer.Root')
43
+ return ctx
44
+ }
45
+
46
+ // --- Root ---
47
+
48
+ interface RootProps {
49
+ children: ReactNode
50
+ /** Controlled open state */
51
+ open?: boolean
52
+ /** Default open state (uncontrolled) */
53
+ defaultOpen?: boolean
54
+ /** Called when drawer open state changes */
55
+ onOpenChange?: (open: boolean) => void
56
+ /** Which edge the drawer slides from. Default: 'right' */
57
+ anchor?: DrawerAnchor
58
+ /** Whether clicking backdrop or pressing Escape closes the drawer. Default: true */
59
+ dismissable?: boolean
60
+ /** Whether swipe gesture dismisses the drawer. Default: true */
61
+ swipeDismiss?: boolean
62
+ /** Duration of the close animation in ms. Default: 160 */
63
+ closeAnimationDuration?: number
64
+ }
65
+
66
+ function Root({
67
+ children,
68
+ open: controlledOpen,
69
+ defaultOpen = false,
70
+ onOpenChange,
71
+ anchor = 'right',
72
+ dismissable = true,
73
+ swipeDismiss = true,
74
+ closeAnimationDuration = 160,
75
+ }: RootProps) {
76
+ const isControlled = controlledOpen !== undefined
77
+ const [internalOpen, setInternalOpen] = useState(defaultOpen)
78
+ const [closing, setClosing] = useState(false)
79
+ const closingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
80
+ const panelRef = useRef<HTMLDivElement>(null)
81
+
82
+ const isOpen = isControlled ? controlledOpen : internalOpen
83
+
84
+ const titleId = useId()
85
+ const descriptionId = useId()
86
+
87
+ const requestOpen = useCallback(() => {
88
+ if (closingTimerRef.current) {
89
+ clearTimeout(closingTimerRef.current)
90
+ closingTimerRef.current = null
91
+ }
92
+ setClosing(false)
93
+ if (!isControlled) setInternalOpen(true)
94
+ onOpenChange?.(true)
95
+ }, [isControlled, onOpenChange])
96
+
97
+ const requestClose = useCallback(() => {
98
+ setClosing(true)
99
+ closingTimerRef.current = setTimeout(() => {
100
+ setClosing(false)
101
+ closingTimerRef.current = null
102
+ if (!isControlled) setInternalOpen(false)
103
+ onOpenChange?.(false)
104
+ }, closeAnimationDuration)
105
+ }, [isControlled, onOpenChange, closeAnimationDuration])
106
+
107
+ const requestCloseImmediate = useCallback(() => {
108
+ if (closingTimerRef.current) {
109
+ clearTimeout(closingTimerRef.current)
110
+ closingTimerRef.current = null
111
+ }
112
+ setClosing(false)
113
+ if (!isControlled) setInternalOpen(false)
114
+ onOpenChange?.(false)
115
+ }, [isControlled, onOpenChange])
116
+
117
+ // Clean up timer on unmount
118
+ useEffect(() => {
119
+ return () => {
120
+ if (closingTimerRef.current) clearTimeout(closingTimerRef.current)
121
+ }
122
+ }, [])
123
+
124
+ // Handle Escape key
125
+ useEffect(() => {
126
+ if (!isOpen && !closing) return
127
+ if (!dismissable) return
128
+
129
+ function onKeyDown(e: KeyboardEvent) {
130
+ if (e.key === 'Escape') {
131
+ e.stopPropagation()
132
+ requestClose()
133
+ }
134
+ }
135
+ document.addEventListener('keydown', onKeyDown)
136
+ return () => document.removeEventListener('keydown', onKeyDown)
137
+ }, [isOpen, closing, dismissable, requestClose])
138
+
139
+ // Lock body scroll when open
140
+ useEffect(() => {
141
+ if (isOpen || closing) {
142
+ const prev = document.body.style.overflow
143
+ document.body.style.overflow = 'hidden'
144
+ return () => {
145
+ document.body.style.overflow = prev
146
+ }
147
+ }
148
+ }, [isOpen, closing])
149
+
150
+ return (
151
+ <DrawerContext.Provider
152
+ value={{
153
+ open: isOpen || closing,
154
+ closing,
155
+ anchor,
156
+ dismissable,
157
+ swipeDismiss,
158
+ requestOpen,
159
+ requestClose,
160
+ requestCloseImmediate,
161
+ titleId,
162
+ descriptionId,
163
+ panelRef,
164
+ }}
165
+ >
166
+ {children}
167
+ </DrawerContext.Provider>
168
+ )
169
+ }
170
+
171
+ // --- Trigger ---
172
+
173
+ function Trigger({children}: {children: ReactNode}) {
174
+ const {requestOpen} = useDrawer()
175
+
176
+ return (
177
+ <html.div onClick={requestOpen} style={styles.inlineWrapper}>
178
+ {children}
179
+ </html.div>
180
+ )
181
+ }
182
+
183
+ // --- Portal ---
184
+
185
+ interface PortalProps {
186
+ children: ReactNode
187
+ size?: DrawerSize
188
+ }
189
+
190
+ // Slide animation maps
191
+ const slideInMap = {
192
+ right: styles.slideInRight,
193
+ left: styles.slideInLeft,
194
+ bottom: styles.slideInBottom,
195
+ } as const
196
+
197
+ const slideOutMap = {
198
+ right: styles.slideOutRight,
199
+ left: styles.slideOutLeft,
200
+ bottom: styles.slideOutBottom,
201
+ } as const
202
+
203
+ const viewportAnchorMap = {
204
+ right: styles.viewportRight,
205
+ left: styles.viewportLeft,
206
+ bottom: styles.viewportBottom,
207
+ } as const
208
+
209
+ function Portal({children, size = 'md'}: PortalProps) {
210
+ const {
211
+ open,
212
+ closing,
213
+ anchor,
214
+ dismissable,
215
+ swipeDismiss: swipeEnabled,
216
+ requestClose,
217
+ requestCloseImmediate,
218
+ titleId,
219
+ descriptionId,
220
+ panelRef,
221
+ } = useDrawer()
222
+
223
+ const handleBackdropClick = useCallback(() => {
224
+ if (dismissable) {
225
+ requestClose()
226
+ }
227
+ }, [dismissable, requestClose])
228
+
229
+ // Swipe-to-dismiss — uses immediate close since the hook handles its own exit animation
230
+ useSwipeDismiss({
231
+ anchor,
232
+ enabled: swipeEnabled && open && !closing,
233
+ onDismiss: requestCloseImmediate,
234
+ panelRef,
235
+ })
236
+
237
+ if (!open) return null
238
+
239
+ const isHorizontal = anchor === 'left' || anchor === 'right'
240
+
241
+ return (
242
+ <>
243
+ {/* Backdrop — click to dismiss */}
244
+ <html.div
245
+ style={[
246
+ styles.backdrop,
247
+ closing && styles.backdropClosing,
248
+ !closing && styles.backdropOpen,
249
+ ]}
250
+ aria-hidden
251
+ onClick={handleBackdropClick}
252
+ />
253
+
254
+ {/* Viewport */}
255
+ <html.div
256
+ style={[styles.viewport, viewportAnchorMap[anchor]]}
257
+ data-drawer-viewport=""
258
+ >
259
+ <html.div
260
+ ref={panelRef}
261
+ role="dialog"
262
+ aria-modal="true"
263
+ aria-labelledby={titleId}
264
+ aria-describedby={descriptionId}
265
+ style={[
266
+ styles.panel,
267
+ isHorizontal && styles.panelHorizontal,
268
+ isHorizontal && styles[size],
269
+ !isHorizontal && styles.panelVertical,
270
+ anchor === 'left' && styles.panelLeftBorder,
271
+ closing && slideOutMap[anchor],
272
+ !closing && slideInMap[anchor],
273
+ ]}
274
+ >
275
+ {children}
276
+ </html.div>
277
+ </html.div>
278
+ </>
279
+ )
280
+ }
281
+
282
+ // --- Header ---
283
+
284
+ function Header({children}: {children: ReactNode}) {
285
+ return <html.div style={styles.header}>{children}</html.div>
286
+ }
287
+
288
+ // --- Title ---
289
+
290
+ function Title({children}: {children: ReactNode}) {
291
+ const {titleId} = useDrawer()
292
+ return (
293
+ <html.h2 id={titleId} style={styles.title}>
294
+ {children}
295
+ </html.h2>
296
+ )
297
+ }
298
+
299
+ // --- Description ---
300
+
301
+ function Description({children}: {children: ReactNode}) {
302
+ const {descriptionId} = useDrawer()
303
+ return (
304
+ <html.p id={descriptionId} style={styles.description}>
305
+ {children}
306
+ </html.p>
307
+ )
308
+ }
309
+
310
+ // --- Body ---
311
+
312
+ function Body({children}: {children: ReactNode}) {
313
+ return <html.div style={styles.body}>{children}</html.div>
314
+ }
315
+
316
+ // --- Footer ---
317
+
318
+ function Footer({children}: {children: ReactNode}) {
319
+ return <html.div style={styles.footer}>{children}</html.div>
320
+ }
321
+
322
+ // --- Close ---
323
+
324
+ interface CloseProps {
325
+ children?: ReactNode
326
+ 'aria-label'?: string
327
+ }
328
+
329
+ function Close({children, 'aria-label': ariaLabel = 'Close'}: CloseProps) {
330
+ const {requestClose} = useDrawer()
331
+
332
+ if (children) {
333
+ return (
334
+ <html.div onClick={requestClose} style={styles.inlineWrapper}>
335
+ {children}
336
+ </html.div>
337
+ )
338
+ }
339
+
340
+ return (
341
+ <html.button
342
+ onClick={requestClose}
343
+ aria-label={ariaLabel}
344
+ style={styles.closeButton}
345
+ >
346
+ <svg width={16} height={16} viewBox="0 0 16 16" fill="none">
347
+ <path
348
+ d="M4 4l8 8M12 4l-8 8"
349
+ stroke="currentColor"
350
+ strokeWidth={1.5}
351
+ strokeLinecap="round"
352
+ />
353
+ </svg>
354
+ </html.button>
355
+ )
356
+ }
357
+
358
+ // --- Export ---
359
+
360
+ export const Drawer = {
361
+ Root,
362
+ Trigger,
363
+ Portal,
364
+ Header,
365
+ Title,
366
+ Description,
367
+ Body,
368
+ Footer,
369
+ Close,
370
+ }
@@ -0,0 +1,242 @@
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
+ animationName: css.keyframes({
17
+ from: {opacity: 0},
18
+ to: {opacity: 1},
19
+ }),
20
+ animationDuration: '200ms',
21
+ animationTimingFunction: 'ease-out',
22
+ animationFillMode: 'both',
23
+ },
24
+ backdropClosing: {
25
+ animationName: css.keyframes({
26
+ from: {opacity: 1},
27
+ to: {opacity: 0},
28
+ }),
29
+ animationDuration: '160ms',
30
+ animationTimingFunction: 'ease-in',
31
+ animationFillMode: 'both',
32
+ },
33
+
34
+ // --- Viewport (positioning container) ---
35
+ viewport: {
36
+ position: 'fixed',
37
+ inset: 0,
38
+ zIndex: 1001,
39
+ display: 'flex',
40
+ pointerEvents: 'none',
41
+ },
42
+
43
+ // Right anchor (desktop default)
44
+ viewportRight: {
45
+ justifyContent: 'flex-end',
46
+ alignItems: 'stretch',
47
+ },
48
+ // Left anchor
49
+ viewportLeft: {
50
+ justifyContent: 'flex-start',
51
+ alignItems: 'stretch',
52
+ },
53
+ // Bottom anchor (mobile default)
54
+ viewportBottom: {
55
+ justifyContent: 'stretch',
56
+ alignItems: 'flex-end',
57
+ },
58
+
59
+ // --- Panel ---
60
+ panel: {
61
+ position: 'relative',
62
+ backgroundColor: colors.bgCard,
63
+ boxShadow: shadows.lg,
64
+ display: 'flex',
65
+ flexDirection: 'column',
66
+ overflow: 'hidden',
67
+ pointerEvents: 'auto',
68
+ borderWidth: 0,
69
+ touchAction: 'none',
70
+ },
71
+
72
+ // Panel sizing per anchor
73
+ panelHorizontal: {
74
+ width: '100%',
75
+ height: '100%',
76
+ borderLeftWidth: 1,
77
+ borderLeftStyle: 'solid',
78
+ borderLeftColor: colors.border,
79
+ },
80
+ panelVertical: {
81
+ width: '100%',
82
+ borderTopWidth: 1,
83
+ borderTopStyle: 'solid',
84
+ borderTopColor: colors.border,
85
+ borderTopLeftRadius: radii.lg,
86
+ borderTopRightRadius: radii.lg,
87
+ maxHeight: '85vh',
88
+ },
89
+ panelLeftBorder: {
90
+ borderLeftWidth: 0,
91
+ borderRightWidth: 1,
92
+ borderRightStyle: 'solid',
93
+ borderRightColor: colors.border,
94
+ },
95
+
96
+ // --- Panel widths (horizontal anchors) ---
97
+ sm: {maxWidth: 360},
98
+ md: {maxWidth: 480},
99
+ lg: {maxWidth: 640},
100
+
101
+ // --- Slide animations: Right ---
102
+ slideInRight: {
103
+ animationName: css.keyframes({
104
+ from: {transform: 'translateX(100%)'},
105
+ to: {transform: 'translateX(0)'},
106
+ }),
107
+ animationDuration: '200ms',
108
+ animationTimingFunction: 'ease-out',
109
+ animationFillMode: 'both',
110
+ },
111
+ slideOutRight: {
112
+ animationName: css.keyframes({
113
+ from: {transform: 'translateX(0)'},
114
+ to: {transform: 'translateX(100%)'},
115
+ }),
116
+ animationDuration: '160ms',
117
+ animationTimingFunction: 'ease-in',
118
+ animationFillMode: 'both',
119
+ },
120
+
121
+ // --- Slide animations: Left ---
122
+ slideInLeft: {
123
+ animationName: css.keyframes({
124
+ from: {transform: 'translateX(-100%)'},
125
+ to: {transform: 'translateX(0)'},
126
+ }),
127
+ animationDuration: '200ms',
128
+ animationTimingFunction: 'ease-out',
129
+ animationFillMode: 'both',
130
+ },
131
+ slideOutLeft: {
132
+ animationName: css.keyframes({
133
+ from: {transform: 'translateX(0)'},
134
+ to: {transform: 'translateX(-100%)'},
135
+ }),
136
+ animationDuration: '160ms',
137
+ animationTimingFunction: 'ease-in',
138
+ animationFillMode: 'both',
139
+ },
140
+
141
+ // --- Slide animations: Bottom ---
142
+ slideInBottom: {
143
+ animationName: css.keyframes({
144
+ from: {transform: 'translateY(100%)'},
145
+ to: {transform: 'translateY(0)'},
146
+ }),
147
+ animationDuration: '200ms',
148
+ animationTimingFunction: 'ease-out',
149
+ animationFillMode: 'both',
150
+ },
151
+ slideOutBottom: {
152
+ animationName: css.keyframes({
153
+ from: {transform: 'translateY(0)'},
154
+ to: {transform: 'translateY(100%)'},
155
+ }),
156
+ animationDuration: '160ms',
157
+ animationTimingFunction: 'ease-in',
158
+ animationFillMode: 'both',
159
+ },
160
+
161
+ // --- Header ---
162
+ header: {
163
+ display: 'flex',
164
+ alignItems: 'flex-start',
165
+ justifyContent: 'space-between',
166
+ paddingTop: spacing.lg,
167
+ paddingLeft: spacing.lg,
168
+ paddingRight: spacing.lg,
169
+ paddingBottom: spacing.md,
170
+ gap: spacing.md,
171
+ },
172
+
173
+ // --- Title ---
174
+ title: {
175
+ fontSize: typography.fontSizeLg,
176
+ fontWeight: typography.fontWeightSemibold,
177
+ lineHeight: typography.lineHeight,
178
+ color: colors.text,
179
+ margin: 0,
180
+ },
181
+
182
+ // --- Description ---
183
+ description: {
184
+ fontSize: typography.fontSizeSm,
185
+ lineHeight: typography.lineHeight,
186
+ color: colors.textMuted,
187
+ margin: 0,
188
+ paddingLeft: spacing.lg,
189
+ paddingRight: spacing.lg,
190
+ paddingBottom: spacing.md,
191
+ },
192
+
193
+ // --- Body ---
194
+ body: {
195
+ paddingLeft: spacing.lg,
196
+ paddingRight: spacing.lg,
197
+ paddingBottom: spacing.lg,
198
+ overflowY: 'auto',
199
+ flex: 1,
200
+ },
201
+
202
+ // --- Footer ---
203
+ footer: {
204
+ display: 'flex',
205
+ alignItems: 'center',
206
+ justifyContent: 'flex-end',
207
+ gap: spacing.sm,
208
+ paddingTop: spacing.md,
209
+ paddingLeft: spacing.lg,
210
+ paddingRight: spacing.lg,
211
+ paddingBottom: spacing.lg,
212
+ borderTopWidth: 1,
213
+ borderTopStyle: 'solid',
214
+ borderTopColor: colors.border,
215
+ },
216
+
217
+ // --- Close button ---
218
+ closeButton: {
219
+ display: 'flex',
220
+ alignItems: 'center',
221
+ justifyContent: 'center',
222
+ width: 32,
223
+ height: 32,
224
+ borderRadius: radii.sm,
225
+ borderWidth: 0,
226
+ backgroundColor: {
227
+ default: 'transparent',
228
+ ':hover': colors.bgCardHover,
229
+ },
230
+ color: colors.textMuted,
231
+ cursor: 'pointer',
232
+ padding: 0,
233
+ flexShrink: 0,
234
+ transitionProperty: 'background-color',
235
+ transitionDuration: '150ms',
236
+ },
237
+
238
+ // --- Inline wrapper ---
239
+ inlineWrapper: {
240
+ display: 'inline-flex',
241
+ },
242
+ })