@duro-app/ui 0.51.0 → 1.0.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 (114) hide show
  1. package/dist/{Table-xlMa_Kqe.js → Table-P-UHkEqJ.js} +333 -329
  2. package/dist/Table-P-UHkEqJ.js.map +1 -0
  3. package/dist/components/ActionBar/ActionBar.d.ts +6 -4
  4. package/dist/components/ActionBar/ActionBar.d.ts.map +1 -1
  5. package/dist/components/ButtonGroup/ButtonGroup.d.ts +3 -1
  6. package/dist/components/ButtonGroup/ButtonGroup.d.ts.map +1 -1
  7. package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -1
  8. package/dist/components/Cluster/Cluster.d.ts +2 -2
  9. package/dist/components/Cluster/Cluster.d.ts.map +1 -1
  10. package/dist/components/Cluster/Cluster.stories.d.ts.map +1 -1
  11. package/dist/components/ColorMode/ColorMode.d.ts +3 -2
  12. package/dist/components/ColorMode/ColorMode.d.ts.map +1 -1
  13. package/dist/components/Combobox/Combobox.stories.d.ts.map +1 -1
  14. package/dist/components/ConfirmDialog/ConfirmDialog.d.ts +1 -1
  15. package/dist/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -1
  16. package/dist/components/DetailPanel/DetailPanel.d.ts +3 -2
  17. package/dist/components/DetailPanel/DetailPanel.d.ts.map +1 -1
  18. package/dist/components/DetailPanel/styles.css.d.ts +15 -3
  19. package/dist/components/DetailPanel/styles.css.d.ts.map +1 -1
  20. package/dist/components/Dialog/Dialog.d.ts +3 -2
  21. package/dist/components/Dialog/Dialog.d.ts.map +1 -1
  22. package/dist/components/Dialog/styles.css.d.ts +16 -4
  23. package/dist/components/Dialog/styles.css.d.ts.map +1 -1
  24. package/dist/components/Drawer/Drawer.d.ts +3 -2
  25. package/dist/components/Drawer/Drawer.d.ts.map +1 -1
  26. package/dist/components/Drawer/styles.css.d.ts +16 -4
  27. package/dist/components/Drawer/styles.css.d.ts.map +1 -1
  28. package/dist/components/Fieldset/Fieldset.d.ts +2 -1
  29. package/dist/components/Fieldset/Fieldset.d.ts.map +1 -1
  30. package/dist/components/Grid/Grid.d.ts +2 -2
  31. package/dist/components/Grid/Grid.d.ts.map +1 -1
  32. package/dist/components/Grid/Grid.stories.d.ts.map +1 -1
  33. package/dist/components/Icon/Icon.d.ts +6 -4
  34. package/dist/components/Icon/Icon.d.ts.map +1 -1
  35. package/dist/components/Icon/index.d.ts +1 -1
  36. package/dist/components/Icon/index.d.ts.map +1 -1
  37. package/dist/components/Inline/Inline.d.ts +2 -2
  38. package/dist/components/Inline/Inline.d.ts.map +1 -1
  39. package/dist/components/Inline/Inline.stories.d.ts.map +1 -1
  40. package/dist/components/SideNav/SideNav.d.ts +32 -3
  41. package/dist/components/SideNav/SideNav.d.ts.map +1 -1
  42. package/dist/components/SideNav/SideNav.meta.d.ts.map +1 -1
  43. package/dist/components/SideNav/SideNav.stories.d.ts +1 -0
  44. package/dist/components/SideNav/SideNav.stories.d.ts.map +1 -1
  45. package/dist/components/SideNav/styles.css.d.ts +4 -0
  46. package/dist/components/SideNav/styles.css.d.ts.map +1 -1
  47. package/dist/components/Stack/Stack.d.ts +2 -1
  48. package/dist/components/Stack/Stack.d.ts.map +1 -1
  49. package/dist/components/Stack/Stack.stories.d.ts.map +1 -1
  50. package/dist/components/StatusIcon/StatusIcon.d.ts +2 -2
  51. package/dist/components/StatusIcon/StatusIcon.d.ts.map +1 -1
  52. package/dist/components/Table/Table.d.ts +1 -7
  53. package/dist/components/Table/Table.d.ts.map +1 -1
  54. package/dist/components/Table/Table.stories.d.ts.map +1 -1
  55. package/dist/components/Tabs/TabsContext.d.ts +2 -1
  56. package/dist/components/Tabs/TabsContext.d.ts.map +1 -1
  57. package/dist/components/ToggleGroup/ToggleGroupContext.d.ts +2 -1
  58. package/dist/components/ToggleGroup/ToggleGroupContext.d.ts.map +1 -1
  59. package/dist/components/VirtualTable/VirtualTable.d.ts +2 -1
  60. package/dist/components/VirtualTable/VirtualTable.d.ts.map +1 -1
  61. package/dist/hooks/useContainerQuery.d.ts +3 -2
  62. package/dist/hooks/useContainerQuery.d.ts.map +1 -1
  63. package/dist/index.css +1 -1
  64. package/dist/index.js +2120 -1983
  65. package/dist/index.js.map +1 -1
  66. package/dist/shared/types.d.ts +2 -0
  67. package/dist/shared/types.d.ts.map +1 -0
  68. package/dist/table.js +137 -139
  69. package/dist/table.js.map +1 -1
  70. package/package.json +3 -2
  71. package/src/components/ActionBar/ActionBar.tsx +10 -5
  72. package/src/components/Alert/Alert.tsx +2 -2
  73. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +3 -2
  74. package/src/components/ButtonGroup/ButtonGroup.tsx +5 -2
  75. package/src/components/Callout/Callout.tsx +2 -2
  76. package/src/components/Cluster/Cluster.stories.tsx +2 -1
  77. package/src/components/Cluster/Cluster.tsx +3 -3
  78. package/src/components/ColorMode/ColorMode.tsx +4 -4
  79. package/src/components/Combobox/Combobox.stories.tsx +7 -3
  80. package/src/components/ConfirmDialog/ConfirmDialog.tsx +1 -3
  81. package/src/components/DetailPanel/DetailPanel.tsx +23 -6
  82. package/src/components/DetailPanel/styles.css.ts +14 -7
  83. package/src/components/Dialog/Dialog.tsx +26 -5
  84. package/src/components/Dialog/styles.css.ts +12 -4
  85. package/src/components/Drawer/Drawer.tsx +17 -4
  86. package/src/components/Drawer/styles.css.ts +12 -4
  87. package/src/components/EmptyState/EmptyState.meta.ts +1 -1
  88. package/src/components/EmptyState/EmptyState.stories.tsx +2 -2
  89. package/src/components/Fieldset/Fieldset.tsx +3 -2
  90. package/src/components/Grid/Grid.stories.tsx +2 -1
  91. package/src/components/Grid/Grid.tsx +3 -3
  92. package/src/components/Icon/Icon.meta.ts +3 -3
  93. package/src/components/Icon/Icon.tsx +63 -2
  94. package/src/components/Icon/index.ts +1 -1
  95. package/src/components/Inline/Inline.stories.tsx +2 -1
  96. package/src/components/Inline/Inline.tsx +3 -3
  97. package/src/components/SideNav/SideNav.meta.ts +13 -10
  98. package/src/components/SideNav/SideNav.stories.tsx +67 -8
  99. package/src/components/SideNav/SideNav.tsx +50 -7
  100. package/src/components/SideNav/styles.css.ts +7 -0
  101. package/src/components/Stack/Stack.stories.tsx +3 -2
  102. package/src/components/Stack/Stack.tsx +3 -2
  103. package/src/components/StatusIcon/StatusIcon.stories.tsx +1 -1
  104. package/src/components/StatusIcon/StatusIcon.tsx +3 -3
  105. package/src/components/Table/Table.stories.tsx +29 -32
  106. package/src/components/Table/Table.tsx +0 -13
  107. package/src/components/Tabs/TabsContext.ts +2 -1
  108. package/src/components/Toast/ToastProvider.tsx +2 -2
  109. package/src/components/Toggle/Toggle.meta.ts +1 -1
  110. package/src/components/ToggleGroup/ToggleGroupContext.ts +2 -1
  111. package/src/components/VirtualTable/VirtualTable.tsx +6 -5
  112. package/src/hooks/useContainerQuery.ts +10 -3
  113. package/src/shared/types.ts +3 -0
  114. package/dist/Table-xlMa_Kqe.js.map +0 -1
@@ -10,10 +10,18 @@ import {
10
10
  useRef,
11
11
  } from 'react'
12
12
  import {html} from 'react-strict-dom'
13
+ import {DURATION_MS, type DurationToken} from '@duro-app/tokens/keys'
13
14
  import {styles} from './styles.css'
14
15
 
15
16
  // --- Types ---
16
17
 
18
+ const closeDurationMap = {
19
+ instant: styles.closeDurationInstant,
20
+ fast: styles.closeDurationFast,
21
+ base: styles.closeDurationBase,
22
+ slow: styles.closeDurationSlow,
23
+ } as const satisfies Record<DurationToken, unknown>
24
+
17
25
  export type DetailPanelSize = 'sm' | 'md'
18
26
 
19
27
  // --- Context ---
@@ -21,6 +29,7 @@ export type DetailPanelSize = 'sm' | 'md'
21
29
  interface DetailPanelContextValue {
22
30
  present: boolean
23
31
  closing: boolean
32
+ closeDuration: DurationToken
24
33
  requestOpen: () => void
25
34
  requestClose: () => void
26
35
  titleId: string
@@ -48,8 +57,8 @@ interface RootProps {
48
57
  defaultOpen?: boolean
49
58
  /** Called when open state changes */
50
59
  onOpenChange?: (open: boolean) => void
51
- /** Duration of the close animation in ms. Default: 220 */
52
- closeAnimationDuration?: number
60
+ /** Motion token for the close animation. Default: 'base' (200ms) */
61
+ closeAnimationDuration?: DurationToken
53
62
  }
54
63
 
55
64
  function Root({
@@ -57,7 +66,7 @@ function Root({
57
66
  open: controlledOpen,
58
67
  defaultOpen = false,
59
68
  onOpenChange,
60
- closeAnimationDuration = 220,
69
+ closeAnimationDuration = 'base',
61
70
  }: RootProps) {
62
71
  const isControlled = controlledOpen !== undefined
63
72
  const [internalOpen, setInternalOpen] = useState(defaultOpen)
@@ -83,7 +92,7 @@ function Root({
83
92
  setClosing(false)
84
93
  setPresent(false)
85
94
  closingTimerRef.current = null
86
- }, closeAnimationDuration)
95
+ }, DURATION_MS[closeAnimationDuration])
87
96
  }
88
97
  }, [isOpen, present, closing, closeAnimationDuration])
89
98
 
@@ -154,6 +163,7 @@ function Root({
154
163
  value={{
155
164
  present,
156
165
  closing,
166
+ closeDuration: closeAnimationDuration,
157
167
  requestOpen,
158
168
  requestClose,
159
169
  titleId,
@@ -189,12 +199,18 @@ const wrapperCloseMap = {
189
199
  } as const
190
200
 
191
201
  function Content({children, size = 'sm', label}: ContentProps) {
192
- const {present, closing, labelId, panelRef} = useDetailPanel()
202
+ const {present, closing, closeDuration, labelId, panelRef} = useDetailPanel()
193
203
 
194
204
  if (!present) return null
195
205
 
196
206
  return (
197
- <html.div style={[styles.wrapper, closing ? wrapperCloseMap[size] : wrapperOpenMap[size]]}>
207
+ <html.div
208
+ style={[
209
+ styles.wrapper,
210
+ closing ? wrapperCloseMap[size] : wrapperOpenMap[size],
211
+ closing && closeDurationMap[closeDuration],
212
+ ]}
213
+ >
198
214
  <html.div
199
215
  ref={panelRef}
200
216
  role="complementary"
@@ -205,6 +221,7 @@ function Content({children, size = 'sm', label}: ContentProps) {
205
221
  styles.content,
206
222
  size === 'sm' ? styles.contentSm : styles.contentMd,
207
223
  closing ? styles.slideOut : styles.slideIn,
224
+ closing && closeDurationMap[closeDuration],
208
225
  ]}
209
226
  >
210
227
  {children}
@@ -4,10 +4,9 @@ import {spacing} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {shadows} from '@duro-app/tokens/tokens/shadows.css'
5
5
  import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
6
6
 
7
- // The "Apple ease" curves and the 280ms open now live in the shared motion
8
- // tokens (easing.easeOut/easeIn, duration.slow). The 220ms close is specific to
9
- // this panel's exit, so it stays local.
10
- const CLOSE_DURATION = '220ms'
7
+ // The "Apple ease" curves and the 280ms open live in the shared motion tokens
8
+ // (easing.easeOut/easeIn, duration.slow). The close is duration.base, and the
9
+ // closeAnimationDuration prop can override it via the closeDuration* styles.
11
10
 
12
11
  export const styles = css.create({
13
12
  // --- Outer wrapper: animates width from 0 → target ---
@@ -32,7 +31,7 @@ export const styles = css.create({
32
31
  from: {width: 360},
33
32
  to: {width: 0},
34
33
  }),
35
- animationDuration: CLOSE_DURATION,
34
+ animationDuration: duration.base,
36
35
  animationTimingFunction: easing.easeIn,
37
36
  animationFillMode: 'both',
38
37
  },
@@ -52,7 +51,7 @@ export const styles = css.create({
52
51
  from: {width: 480},
53
52
  to: {width: 0},
54
53
  }),
55
- animationDuration: CLOSE_DURATION,
54
+ animationDuration: duration.base,
56
55
  animationTimingFunction: easing.easeIn,
57
56
  animationFillMode: 'both',
58
57
  },
@@ -101,11 +100,19 @@ export const styles = css.create({
101
100
  opacity: 0,
102
101
  },
103
102
  }),
104
- animationDuration: CLOSE_DURATION,
103
+ animationDuration: duration.base,
105
104
  animationTimingFunction: easing.easeIn,
106
105
  animationFillMode: 'both',
107
106
  },
108
107
 
108
+ // --- Close-duration overrides ---
109
+ // The closeAnimationDuration prop token drives both these and the unmount
110
+ // timeout, so the exit animation can never race the unmount.
111
+ closeDurationInstant: {animationDuration: duration.instant},
112
+ closeDurationFast: {animationDuration: duration.fast},
113
+ closeDurationBase: {animationDuration: duration.base},
114
+ closeDurationSlow: {animationDuration: duration.slow},
115
+
109
116
  // --- Header ---
110
117
  header: {
111
118
  display: 'flex',
@@ -10,17 +10,26 @@ import {
10
10
  useRef,
11
11
  } from 'react'
12
12
  import {html} from 'react-strict-dom'
13
+ import {DURATION_MS, type DurationToken} from '@duro-app/tokens/keys'
13
14
  import {styles} from './styles.css'
14
15
 
15
16
  // --- Types ---
16
17
 
17
18
  export type DialogSize = 'sm' | 'md' | 'lg'
18
19
 
20
+ const closeDurationMap = {
21
+ instant: styles.closeDurationInstant,
22
+ fast: styles.closeDurationFast,
23
+ base: styles.closeDurationBase,
24
+ slow: styles.closeDurationSlow,
25
+ } as const satisfies Record<DurationToken, unknown>
26
+
19
27
  // --- Context ---
20
28
 
21
29
  interface DialogContextValue {
22
30
  open: boolean
23
31
  closing: boolean
32
+ closeDuration: DurationToken
24
33
  dismissable: boolean
25
34
  requestOpen: () => void
26
35
  requestClose: () => void
@@ -49,8 +58,8 @@ interface RootProps {
49
58
  onOpenChange?: (open: boolean) => void
50
59
  /** Whether clicking backdrop closes the dialog. Default: true */
51
60
  dismissable?: boolean
52
- /** Duration of the close animation in ms. Default: 140 */
53
- closeAnimationDuration?: number
61
+ /** Motion token for the close animation. Default: 'fast' (150ms) */
62
+ closeAnimationDuration?: DurationToken
54
63
  }
55
64
 
56
65
  function Root({
@@ -59,7 +68,7 @@ function Root({
59
68
  defaultOpen = false,
60
69
  onOpenChange,
61
70
  dismissable = true,
62
- closeAnimationDuration = 140,
71
+ closeAnimationDuration = 'fast',
63
72
  }: RootProps) {
64
73
  const isControlled = controlledOpen !== undefined
65
74
  const [internalOpen, setInternalOpen] = useState(defaultOpen)
@@ -89,7 +98,7 @@ function Root({
89
98
  closingTimerRef.current = null
90
99
  if (!isControlled) setInternalOpen(false)
91
100
  onOpenChange?.(false)
92
- }, closeAnimationDuration)
101
+ }, DURATION_MS[closeAnimationDuration])
93
102
  }, [isControlled, onOpenChange, closeAnimationDuration])
94
103
 
95
104
  // If controlled open goes false externally, trigger close animation
@@ -137,6 +146,7 @@ function Root({
137
146
  value={{
138
147
  open: isOpen || closing,
139
148
  closing,
149
+ closeDuration: closeAnimationDuration,
140
150
  dismissable,
141
151
  requestOpen,
142
152
  requestClose,
@@ -174,7 +184,16 @@ interface PortalProps {
174
184
  }
175
185
 
176
186
  function Portal({children, size = 'md'}: PortalProps) {
177
- const {open, closing, dismissable, requestClose, titleId, descriptionId, popupRef} = useDialog()
187
+ const {
188
+ open,
189
+ closing,
190
+ closeDuration,
191
+ dismissable,
192
+ requestClose,
193
+ titleId,
194
+ descriptionId,
195
+ popupRef,
196
+ } = useDialog()
178
197
 
179
198
  const handleBackdropClick = useCallback(() => {
180
199
  if (dismissable) {
@@ -191,6 +210,7 @@ function Portal({children, size = 'md'}: PortalProps) {
191
210
  style={[
192
211
  styles.backdrop,
193
212
  closing && styles.backdropClosing,
213
+ closing && closeDurationMap[closeDuration],
194
214
  !closing && styles.backdropOpen,
195
215
  ]}
196
216
  aria-hidden
@@ -209,6 +229,7 @@ function Portal({children, size = 'md'}: PortalProps) {
209
229
  styles.popup,
210
230
  styles[size],
211
231
  closing && styles.popupClosing,
232
+ closing && closeDurationMap[closeDuration],
212
233
  !closing && styles.popupOpen,
213
234
  ]}
214
235
  >
@@ -19,7 +19,7 @@ export const styles = css.create({
19
19
  from: {opacity: 0},
20
20
  to: {opacity: 1},
21
21
  }),
22
- animationDuration: '180ms',
22
+ animationDuration: duration.base,
23
23
  animationTimingFunction: easing.easeOut,
24
24
  animationFillMode: 'both',
25
25
  },
@@ -29,7 +29,7 @@ export const styles = css.create({
29
29
  from: {opacity: 1},
30
30
  to: {opacity: 0},
31
31
  }),
32
- animationDuration: '140ms',
32
+ animationDuration: duration.fast,
33
33
  animationTimingFunction: easing.easeIn,
34
34
  animationFillMode: 'both',
35
35
  },
@@ -75,7 +75,7 @@ export const styles = css.create({
75
75
  transform: 'scale(1) translateY(0)',
76
76
  },
77
77
  }),
78
- animationDuration: '180ms',
78
+ animationDuration: duration.base,
79
79
  animationTimingFunction: easing.easeOut,
80
80
  animationFillMode: 'both',
81
81
  },
@@ -91,11 +91,19 @@ export const styles = css.create({
91
91
  transform: 'scale(0.97) translateY(8px)',
92
92
  },
93
93
  }),
94
- animationDuration: '140ms',
94
+ animationDuration: duration.fast,
95
95
  animationTimingFunction: easing.easeIn,
96
96
  animationFillMode: 'both',
97
97
  },
98
98
 
99
+ // --- Close-duration overrides ---
100
+ // The closeAnimationDuration prop token drives both these and the unmount
101
+ // timeout, so the exit animation can never race the unmount.
102
+ closeDurationInstant: {animationDuration: duration.instant},
103
+ closeDurationFast: {animationDuration: duration.fast},
104
+ closeDurationBase: {animationDuration: duration.base},
105
+ closeDurationSlow: {animationDuration: duration.slow},
106
+
99
107
  // --- Sizes ---
100
108
  sm: {maxWidth: 400},
101
109
  md: {maxWidth: 520},
@@ -10,6 +10,7 @@ import {
10
10
  useRef,
11
11
  } from 'react'
12
12
  import {html} from 'react-strict-dom'
13
+ import {DURATION_MS, type DurationToken} from '@duro-app/tokens/keys'
13
14
  import {styles} from './styles.css'
14
15
  import {useSwipeDismiss} from './useSwipeDismiss'
15
16
 
@@ -18,11 +19,19 @@ import {useSwipeDismiss} from './useSwipeDismiss'
18
19
  export type DrawerAnchor = 'right' | 'left' | 'bottom'
19
20
  export type DrawerSize = 'sm' | 'md' | 'lg'
20
21
 
22
+ const closeDurationMap = {
23
+ instant: styles.closeDurationInstant,
24
+ fast: styles.closeDurationFast,
25
+ base: styles.closeDurationBase,
26
+ slow: styles.closeDurationSlow,
27
+ } as const satisfies Record<DurationToken, unknown>
28
+
21
29
  // --- Context ---
22
30
 
23
31
  interface DrawerContextValue {
24
32
  open: boolean
25
33
  closing: boolean
34
+ closeDuration: DurationToken
26
35
  anchor: DrawerAnchor
27
36
  dismissable: boolean
28
37
  swipeDismiss: boolean
@@ -59,8 +68,8 @@ interface RootProps {
59
68
  dismissable?: boolean
60
69
  /** Whether swipe gesture dismisses the drawer. Default: true */
61
70
  swipeDismiss?: boolean
62
- /** Duration of the close animation in ms. Default: 160 */
63
- closeAnimationDuration?: number
71
+ /** Motion token for the close animation. Default: 'fast' (150ms) */
72
+ closeAnimationDuration?: DurationToken
64
73
  }
65
74
 
66
75
  function Root({
@@ -71,7 +80,7 @@ function Root({
71
80
  anchor = 'right',
72
81
  dismissable = true,
73
82
  swipeDismiss = true,
74
- closeAnimationDuration = 160,
83
+ closeAnimationDuration = 'fast',
75
84
  }: RootProps) {
76
85
  const isControlled = controlledOpen !== undefined
77
86
  const [internalOpen, setInternalOpen] = useState(defaultOpen)
@@ -101,7 +110,7 @@ function Root({
101
110
  closingTimerRef.current = null
102
111
  if (!isControlled) setInternalOpen(false)
103
112
  onOpenChange?.(false)
104
- }, closeAnimationDuration)
113
+ }, DURATION_MS[closeAnimationDuration])
105
114
  }, [isControlled, onOpenChange, closeAnimationDuration])
106
115
 
107
116
  const requestCloseImmediate = useCallback(() => {
@@ -152,6 +161,7 @@ function Root({
152
161
  value={{
153
162
  open: isOpen || closing,
154
163
  closing,
164
+ closeDuration: closeAnimationDuration,
155
165
  anchor,
156
166
  dismissable,
157
167
  swipeDismiss,
@@ -210,6 +220,7 @@ function Portal({children, size = 'md'}: PortalProps) {
210
220
  const {
211
221
  open,
212
222
  closing,
223
+ closeDuration,
213
224
  anchor,
214
225
  dismissable,
215
226
  swipeDismiss: swipeEnabled,
@@ -245,6 +256,7 @@ function Portal({children, size = 'md'}: PortalProps) {
245
256
  style={[
246
257
  styles.backdrop,
247
258
  closing && styles.backdropClosing,
259
+ closing && closeDurationMap[closeDuration],
248
260
  !closing && styles.backdropOpen,
249
261
  ]}
250
262
  aria-hidden
@@ -266,6 +278,7 @@ function Portal({children, size = 'md'}: PortalProps) {
266
278
  !isHorizontal && styles.panelVertical,
267
279
  anchor === 'left' && styles.panelLeftBorder,
268
280
  closing && slideOutMap[anchor],
281
+ closing && closeDurationMap[closeDuration],
269
282
  !closing && slideInMap[anchor],
270
283
  ]}
271
284
  >
@@ -27,7 +27,7 @@ export const styles = css.create({
27
27
  from: {opacity: 1},
28
28
  to: {opacity: 0},
29
29
  }),
30
- animationDuration: '160ms',
30
+ animationDuration: duration.fast,
31
31
  animationTimingFunction: easing.easeIn,
32
32
  animationFillMode: 'both',
33
33
  },
@@ -114,7 +114,7 @@ export const styles = css.create({
114
114
  from: {transform: 'translateX(0)'},
115
115
  to: {transform: 'translateX(100%)'},
116
116
  }),
117
- animationDuration: '160ms',
117
+ animationDuration: duration.fast,
118
118
  animationTimingFunction: easing.easeIn,
119
119
  animationFillMode: 'both',
120
120
  },
@@ -134,7 +134,7 @@ export const styles = css.create({
134
134
  from: {transform: 'translateX(0)'},
135
135
  to: {transform: 'translateX(-100%)'},
136
136
  }),
137
- animationDuration: '160ms',
137
+ animationDuration: duration.fast,
138
138
  animationTimingFunction: easing.easeIn,
139
139
  animationFillMode: 'both',
140
140
  },
@@ -154,11 +154,19 @@ export const styles = css.create({
154
154
  from: {transform: 'translateY(0)'},
155
155
  to: {transform: 'translateY(100%)'},
156
156
  }),
157
- animationDuration: '160ms',
157
+ animationDuration: duration.fast,
158
158
  animationTimingFunction: easing.easeIn,
159
159
  animationFillMode: 'both',
160
160
  },
161
161
 
162
+ // --- Close-duration overrides ---
163
+ // The closeAnimationDuration prop token drives both these and the unmount
164
+ // timeout, so the exit animation can never race the unmount.
165
+ closeDurationInstant: {animationDuration: duration.instant},
166
+ closeDurationFast: {animationDuration: duration.fast},
167
+ closeDurationBase: {animationDuration: duration.base},
168
+ closeDurationSlow: {animationDuration: duration.slow},
169
+
162
170
  // --- Header ---
163
171
  header: {
164
172
  display: 'flex',
@@ -11,7 +11,7 @@ export const meta: ComponentMeta = {
11
11
  whenNotToUse: ['Loading states — use Spinner', 'Error states — use Alert or Callout'],
12
12
  example: `<Card>
13
13
  <EmptyState
14
- icon={<Icon name="info-circle" size={48} />}
14
+ icon={<Icon name="info-circle" size="xxl" />}
15
15
  message="No results found"
16
16
  action={<Button variant="secondary">Clear filters</Button>}
17
17
  />
@@ -23,7 +23,7 @@ export const Default: Story = {
23
23
  export const WithIcon: Story = {
24
24
  args: {
25
25
  message: 'Nothing to show here',
26
- icon: <StatusIcon name="forbidden" variant="muted" size={48} />,
26
+ icon: <StatusIcon name="forbidden" variant="muted" size="xxl" />,
27
27
  },
28
28
  play: async ({canvas}) => {
29
29
  await expect(canvas.getByText('Nothing to show here')).toBeInTheDocument()
@@ -44,7 +44,7 @@ export const WithAction: Story = {
44
44
  export const WithIconAndAction: Story = {
45
45
  args: {
46
46
  message: 'Your inbox is empty',
47
- icon: <StatusIcon name="check-done" variant="success" size={48} />,
47
+ icon: <StatusIcon name="check-done" variant="success" size="xxl" />,
48
48
  action: <Button variant="primary">Refresh</Button>,
49
49
  },
50
50
  play: async ({canvas}) => {
@@ -1,8 +1,9 @@
1
1
  import type {ReactNode} from 'react'
2
2
  import {html} from 'react-strict-dom'
3
+ import type {SpacingToken} from '@duro-app/tokens/keys'
3
4
  import {styles} from './styles.css'
4
5
 
5
- export type FieldsetGap = 'xs' | 'sm' | 'ms' | 'md' | 'lg' | 'xl'
6
+ export type FieldsetGap = Exclude<SpacingToken, 'xxl' | 'xxxl'>
6
7
 
7
8
  const gapMap = {
8
9
  xs: styles.gapXs,
@@ -11,7 +12,7 @@ const gapMap = {
11
12
  md: styles.gapMd,
12
13
  lg: styles.gapLg,
13
14
  xl: styles.gapXl,
14
- } as const
15
+ } as const satisfies Record<FieldsetGap, unknown>
15
16
 
16
17
  // --- Root ---
17
18
  interface RootProps {
@@ -4,6 +4,7 @@ import {Grid} from './Grid'
4
4
  import {Stack} from '../Stack/Stack'
5
5
  import {colors} from '@duro-app/tokens/tokens/colors.css'
6
6
  import {spacing} from '@duro-app/tokens/tokens/spacing.css'
7
+ import {SPACING_KEYS} from '@duro-app/tokens/keys'
7
8
  import {useContainerQuery} from '../../hooks/useContainerQuery'
8
9
 
9
10
  const meta: Meta<typeof Grid> = {
@@ -12,7 +13,7 @@ const meta: Meta<typeof Grid> = {
12
13
  argTypes: {
13
14
  gap: {
14
15
  control: 'select',
15
- options: ['xs', 'sm', 'ms', 'md', 'lg', 'xl', 'xxl', 'xxxl'],
16
+ options: [...SPACING_KEYS],
16
17
  },
17
18
  columns: {
18
19
  control: 'select',
@@ -1,10 +1,10 @@
1
1
  import type {ReactNode} from 'react'
2
2
  import {html} from 'react-strict-dom'
3
3
  import {styles} from './styles.css'
4
- import type {SpacingKey} from '../Stack/Stack'
4
+ import type {SpacingToken} from '@duro-app/tokens/keys'
5
5
 
6
6
  interface GridProps {
7
- gap?: SpacingKey
7
+ gap?: SpacingToken
8
8
  columns?: 1 | 2 | 3 | 4 | 5 | 6
9
9
  minColumnWidth?: string
10
10
  children: ReactNode
@@ -19,7 +19,7 @@ const gapMap = {
19
19
  xl: styles.gapXl,
20
20
  xxl: styles.gapXxl,
21
21
  xxxl: styles.gapXxxl,
22
- } as const
22
+ } as const satisfies Record<SpacingToken, unknown>
23
23
 
24
24
  const columnsMap = {
25
25
  1: styles.col1,
@@ -16,8 +16,8 @@ export const meta: ComponentMeta = {
16
16
  },
17
17
  ],
18
18
  example: `<Inline gap="sm" align="center">
19
- <Icon name="check-circle" size={20} />
20
- <Icon name="alert-triangle" size={20} />
21
- <Icon name="shield-filled" size={20} />
19
+ <Icon name="check-circle" size="md" />
20
+ <Icon name="alert-triangle" size="md" />
21
+ <Icon name="shield-filled" size="md" />
22
22
  </Inline>`,
23
23
  }
@@ -1,4 +1,7 @@
1
1
  import type {ReactNode} from 'react'
2
+ import {ICON_SIZES, type IconSize} from '@duro-app/tokens/keys'
3
+
4
+ export type {IconSize}
2
5
 
3
6
  export type IconName =
4
7
  | 'x-circle'
@@ -21,6 +24,13 @@ export type IconName =
21
24
  | 'git-branch'
22
25
  | 'menu'
23
26
  | 'pin'
27
+ // Infrastructure / inventory glyphs
28
+ | 'server'
29
+ | 'hard-drive'
30
+ | 'box'
31
+ | 'image'
32
+ | 'tag'
33
+ | 'pie-chart'
24
34
  // People / access / admin glyphs
25
35
  | 'users'
26
36
  | 'user-plus'
@@ -159,6 +169,55 @@ const strokeIcons: Partial<Record<IconName, ReactNode>> = {
159
169
  <line x1="3" y1="18" x2="21" y2="18" />
160
170
  </>
161
171
  ),
172
+ // ---- infrastructure / inventory ----
173
+ // Two stacked rack units — hosts, nodes, machines.
174
+ server: (
175
+ <>
176
+ <rect x="2" y="2" width="20" height="8" rx="2" ry="2" />
177
+ <rect x="2" y="14" width="20" height="8" rx="2" ry="2" />
178
+ <line x1="6" y1="6" x2="6.01" y2="6" />
179
+ <line x1="6" y1="18" x2="6.01" y2="18" />
180
+ </>
181
+ ),
182
+ // Disk enclosure — volumes, persistent storage.
183
+ 'hard-drive': (
184
+ <>
185
+ <line x1="22" y1="12" x2="2" y2="12" />
186
+ <path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
187
+ <line x1="6" y1="16" x2="6.01" y2="16" />
188
+ <line x1="10" y1="16" x2="10.01" y2="16" />
189
+ </>
190
+ ),
191
+ // Isometric crate — a deployable unit of work.
192
+ box: (
193
+ <>
194
+ <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
195
+ <polyline points="3.27 6.96 12 12.01 20.73 6.96" />
196
+ <line x1="12" y1="22.08" x2="12" y2="12" />
197
+ </>
198
+ ),
199
+ // Picture frame — container images, artefacts.
200
+ image: (
201
+ <>
202
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
203
+ <circle cx="8.5" cy="8.5" r="1.5" />
204
+ <polyline points="21 15 16 10 5 21" />
205
+ </>
206
+ ),
207
+ // Luggage tag — labels, annotations, metadata.
208
+ tag: (
209
+ <>
210
+ <path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" />
211
+ <line x1="7" y1="7" x2="7.01" y2="7" />
212
+ </>
213
+ ),
214
+ // Share of a whole — quotas, budgets, utilisation.
215
+ 'pie-chart': (
216
+ <>
217
+ <path d="M21.21 15.89A10 10 0 1 1 8 2.83" />
218
+ <path d="M22 12A10 10 0 0 0 12 2v10z" />
219
+ </>
220
+ ),
162
221
  // ---- people / access / admin ----
163
222
  users: (
164
223
  <>
@@ -294,10 +353,12 @@ const filledIcons: Partial<Record<IconName, ReactNode>> = {
294
353
 
295
354
  interface IconProps {
296
355
  name: IconName
297
- size?: number
356
+ /** Token from the icon size scale: sm 16, md 18, lg 24, xl 36, xxl 48. */
357
+ size?: IconSize
298
358
  }
299
359
 
300
- export function Icon({name, size = 24}: IconProps) {
360
+ export function Icon({name, size: sizeToken = 'lg'}: IconProps) {
361
+ const size = ICON_SIZES[sizeToken]
301
362
  const filled = filledIcons[name]
302
363
  if (filled) {
303
364
  return (
@@ -1,2 +1,2 @@
1
1
  export {Icon} from './Icon'
2
- export type {IconName} from './Icon'
2
+ export type {IconName, IconSize} from './Icon'
@@ -3,6 +3,7 @@ import {css, html} from 'react-strict-dom'
3
3
  import {Inline} from './Inline'
4
4
  import {colors} from '@duro-app/tokens/tokens/colors.css'
5
5
  import {spacing} from '@duro-app/tokens/tokens/spacing.css'
6
+ import {SPACING_KEYS} from '@duro-app/tokens/keys'
6
7
 
7
8
  const meta: Meta<typeof Inline> = {
8
9
  title: 'Layout/Inline',
@@ -10,7 +11,7 @@ const meta: Meta<typeof Inline> = {
10
11
  argTypes: {
11
12
  gap: {
12
13
  control: 'select',
13
- options: ['xs', 'sm', 'ms', 'md', 'lg', 'xl', 'xxl', 'xxxl'],
14
+ options: [...SPACING_KEYS],
14
15
  },
15
16
  align: {
16
17
  control: 'select',
@@ -1,10 +1,10 @@
1
1
  import type {ReactNode} from 'react'
2
2
  import {html} from 'react-strict-dom'
3
3
  import {styles} from './styles.css'
4
- import type {SpacingKey} from '../Stack/Stack'
4
+ import type {SpacingToken} from '@duro-app/tokens/keys'
5
5
 
6
6
  interface InlineProps {
7
- gap?: SpacingKey
7
+ gap?: SpacingToken
8
8
  align?: 'start' | 'center' | 'end' | 'baseline' | 'stretch'
9
9
  justify?: 'start' | 'center' | 'end' | 'between'
10
10
  children: ReactNode
@@ -19,7 +19,7 @@ const gapMap = {
19
19
  xl: styles.gapXl,
20
20
  xxl: styles.gapXxl,
21
21
  xxxl: styles.gapXxxl,
22
- } as const
22
+ } as const satisfies Record<SpacingToken, unknown>
23
23
 
24
24
  const alignMap = {
25
25
  start: styles.alignStart,