@duro-app/ui 0.39.0 → 0.41.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 (85) hide show
  1. package/dist/components/ActionBar/styles.css.d.ts +2 -2
  2. package/dist/components/ActionBar/styles.css.d.ts.map +1 -1
  3. package/dist/components/Checkbox/styles.css.d.ts +2 -2
  4. package/dist/components/Checkbox/styles.css.d.ts.map +1 -1
  5. package/dist/components/ColorMode/styles.css.d.ts +2 -2
  6. package/dist/components/ColorMode/styles.css.d.ts.map +1 -1
  7. package/dist/components/Combobox/styles.css.d.ts +2 -2
  8. package/dist/components/Combobox/styles.css.d.ts.map +1 -1
  9. package/dist/components/Dialog/styles.css.d.ts +5 -5
  10. package/dist/components/Dialog/styles.css.d.ts.map +1 -1
  11. package/dist/components/Drawer/styles.css.d.ts +13 -13
  12. package/dist/components/Drawer/styles.css.d.ts.map +1 -1
  13. package/dist/components/Icon/Icon.d.ts +1 -1
  14. package/dist/components/Icon/Icon.d.ts.map +1 -1
  15. package/dist/components/Input/styles.css.d.ts +2 -2
  16. package/dist/components/Input/styles.css.d.ts.map +1 -1
  17. package/dist/components/InputGroup/styles.css.d.ts +4 -4
  18. package/dist/components/InputGroup/styles.css.d.ts.map +1 -1
  19. package/dist/components/LinkButton/styles.css.d.ts +2 -2
  20. package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
  21. package/dist/components/Menu/styles.css.d.ts +2 -2
  22. package/dist/components/Menu/styles.css.d.ts.map +1 -1
  23. package/dist/components/RadioGroup/styles.css.d.ts +2 -2
  24. package/dist/components/RadioGroup/styles.css.d.ts.map +1 -1
  25. package/dist/components/ScrollArea/styles.css.d.ts +3 -3
  26. package/dist/components/ScrollArea/styles.css.d.ts.map +1 -1
  27. package/dist/components/Select/styles.css.d.ts +2 -2
  28. package/dist/components/Select/styles.css.d.ts.map +1 -1
  29. package/dist/components/SideNav/SideNav.d.ts +16 -1
  30. package/dist/components/SideNav/SideNav.d.ts.map +1 -1
  31. package/dist/components/SideNav/SideNav.meta.d.ts.map +1 -1
  32. package/dist/components/SideNav/SideNav.stories.d.ts +1 -0
  33. package/dist/components/SideNav/SideNav.stories.d.ts.map +1 -1
  34. package/dist/components/SideNav/styles.css.d.ts +54 -6
  35. package/dist/components/SideNav/styles.css.d.ts.map +1 -1
  36. package/dist/components/Switch/styles.css.d.ts +4 -4
  37. package/dist/components/Switch/styles.css.d.ts.map +1 -1
  38. package/dist/components/Table/styles.css.d.ts +2 -2
  39. package/dist/components/Table/styles.css.d.ts.map +1 -1
  40. package/dist/components/Tabs/Tabs.d.ts.map +1 -1
  41. package/dist/components/Tabs/TabsContext.d.ts +2 -0
  42. package/dist/components/Tabs/TabsContext.d.ts.map +1 -1
  43. package/dist/components/Tabs/styles.css.d.ts +33 -2
  44. package/dist/components/Tabs/styles.css.d.ts.map +1 -1
  45. package/dist/components/Tabs/useTabsRoot.d.ts.map +1 -1
  46. package/dist/components/TagGroup/styles.css.d.ts +2 -2
  47. package/dist/components/TagGroup/styles.css.d.ts.map +1 -1
  48. package/dist/components/Textarea/styles.css.d.ts +2 -2
  49. package/dist/components/Textarea/styles.css.d.ts.map +1 -1
  50. package/dist/components/Toast/styles.css.d.ts +1 -1
  51. package/dist/components/Toast/styles.css.d.ts.map +1 -1
  52. package/dist/components/Toggle/styles.css.d.ts +2 -2
  53. package/dist/components/Toggle/styles.css.d.ts.map +1 -1
  54. package/dist/index.css +1 -1
  55. package/dist/index.js +2515 -2329
  56. package/dist/index.js.map +1 -1
  57. package/package.json +2 -2
  58. package/src/components/ActionBar/styles.css.ts +3 -2
  59. package/src/components/Checkbox/styles.css.ts +3 -2
  60. package/src/components/ColorMode/styles.css.ts +3 -2
  61. package/src/components/Combobox/styles.css.ts +3 -2
  62. package/src/components/Dialog/styles.css.ts +6 -5
  63. package/src/components/Drawer/styles.css.ts +14 -13
  64. package/src/components/Icon/Icon.tsx +46 -0
  65. package/src/components/Input/styles.css.ts +3 -2
  66. package/src/components/InputGroup/styles.css.ts +5 -4
  67. package/src/components/LinkButton/styles.css.ts +3 -2
  68. package/src/components/Menu/styles.css.ts +3 -2
  69. package/src/components/RadioGroup/styles.css.ts +3 -2
  70. package/src/components/ScrollArea/styles.css.ts +4 -3
  71. package/src/components/Select/styles.css.ts +3 -2
  72. package/src/components/SideNav/SideNav.meta.ts +17 -11
  73. package/src/components/SideNav/SideNav.stories.tsx +47 -3
  74. package/src/components/SideNav/SideNav.tsx +34 -2
  75. package/src/components/SideNav/styles.css.ts +58 -7
  76. package/src/components/Switch/styles.css.ts +5 -4
  77. package/src/components/Table/styles.css.ts +3 -2
  78. package/src/components/Tabs/Tabs.tsx +52 -3
  79. package/src/components/Tabs/TabsContext.ts +6 -0
  80. package/src/components/Tabs/styles.css.ts +41 -2
  81. package/src/components/Tabs/useTabsRoot.ts +12 -2
  82. package/src/components/TagGroup/styles.css.ts +3 -2
  83. package/src/components/Textarea/styles.css.ts +3 -2
  84. package/src/components/Toast/styles.css.ts +2 -1
  85. package/src/components/Toggle/styles.css.ts +3 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/ui",
3
- "version": "0.39.0",
3
+ "version": "0.41.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,7 +54,7 @@
54
54
  }
55
55
  },
56
56
  "dependencies": {
57
- "@duro-app/tokens": "^0.39.0"
57
+ "@duro-app/tokens": "^0.41.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@babel/preset-typescript": "^7.28.0",
@@ -3,6 +3,7 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
5
  import {shadows} from '@duro-app/tokens/tokens/shadows.css'
6
+ import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
6
7
 
7
8
  export const styles = css.create({
8
9
  overlay: {
@@ -63,8 +64,8 @@ export const styles = css.create({
63
64
  },
64
65
  cursor: 'pointer',
65
66
  transitionProperty: 'color, background-color',
66
- transitionDuration: '150ms',
67
- transitionTimingFunction: 'ease',
67
+ transitionDuration: duration.fast,
68
+ transitionTimingFunction: easing.standard,
68
69
  outlineWidth: {
69
70
  default: 0,
70
71
  ':focus-visible': 2,
@@ -2,6 +2,7 @@ import {css} from 'react-strict-dom'
2
2
  import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+ import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
5
6
 
6
7
  export const styles = css.create({
7
8
  root: {
@@ -28,8 +29,8 @@ export const styles = css.create({
28
29
  justifyContent: 'center',
29
30
  flexShrink: 0,
30
31
  transitionProperty: 'background-color, border-color',
31
- transitionDuration: '150ms',
32
- transitionTimingFunction: 'ease',
32
+ transitionDuration: duration.fast,
33
+ transitionTimingFunction: easing.standard,
33
34
  },
34
35
  boxUnchecked: {
35
36
  backgroundColor: colors.bg,
@@ -1,6 +1,7 @@
1
1
  import {css} from 'react-strict-dom'
2
2
  import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
4
5
 
5
6
  export const styles = css.create({
6
7
  button: {
@@ -22,8 +23,8 @@ export const styles = css.create({
22
23
  color: colors.text,
23
24
  cursor: 'pointer',
24
25
  transitionProperty: 'background-color, border-color, color',
25
- transitionDuration: '150ms',
26
- transitionTimingFunction: 'ease',
26
+ transitionDuration: duration.fast,
27
+ transitionTimingFunction: easing.standard,
27
28
  outlineWidth: {default: 0, ':focus-visible': 2},
28
29
  outlineStyle: 'solid',
29
30
  outlineColor: colors.accent,
@@ -3,6 +3,7 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
5
  import {shadows} from '@duro-app/tokens/tokens/shadows.css'
6
+ import {duration} from '@duro-app/tokens/tokens/motion.css'
6
7
 
7
8
  export const styles = css.create({
8
9
  root: {
@@ -24,7 +25,7 @@ export const styles = css.create({
24
25
  },
25
26
  borderRadius: radii.sm,
26
27
  transitionProperty: 'border-color',
27
- transitionDuration: '150ms',
28
+ transitionDuration: duration.fast,
28
29
  },
29
30
  inputWrapperFocused: {
30
31
  borderColor: colors.accent,
@@ -94,7 +95,7 @@ export const styles = css.create({
94
95
  cursor: 'pointer',
95
96
  backgroundColor: 'transparent',
96
97
  transitionProperty: 'background-color',
97
- transitionDuration: '150ms',
98
+ transitionDuration: duration.fast,
98
99
  },
99
100
  itemSelected: {
100
101
  color: colors.accent,
@@ -3,6 +3,7 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
5
  import {shadows} from '@duro-app/tokens/tokens/shadows.css'
6
+ import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
6
7
 
7
8
  export const styles = css.create({
8
9
  // --- Backdrop ---
@@ -19,7 +20,7 @@ export const styles = css.create({
19
20
  to: {opacity: 1},
20
21
  }),
21
22
  animationDuration: '180ms',
22
- animationTimingFunction: 'ease-out',
23
+ animationTimingFunction: easing.easeOut,
23
24
  animationFillMode: 'both',
24
25
  },
25
26
  backdropClosing: {
@@ -29,7 +30,7 @@ export const styles = css.create({
29
30
  to: {opacity: 0},
30
31
  }),
31
32
  animationDuration: '140ms',
32
- animationTimingFunction: 'ease-in',
33
+ animationTimingFunction: easing.easeIn,
33
34
  animationFillMode: 'both',
34
35
  },
35
36
 
@@ -75,7 +76,7 @@ export const styles = css.create({
75
76
  },
76
77
  }),
77
78
  animationDuration: '180ms',
78
- animationTimingFunction: 'ease-out',
79
+ animationTimingFunction: easing.easeOut,
79
80
  animationFillMode: 'both',
80
81
  },
81
82
  popupClosing: {
@@ -91,7 +92,7 @@ export const styles = css.create({
91
92
  },
92
93
  }),
93
94
  animationDuration: '140ms',
94
- animationTimingFunction: 'ease-in',
95
+ animationTimingFunction: easing.easeIn,
95
96
  animationFillMode: 'both',
96
97
  },
97
98
 
@@ -174,7 +175,7 @@ export const styles = css.create({
174
175
  padding: 0,
175
176
  flexShrink: 0,
176
177
  transitionProperty: 'background-color',
177
- transitionDuration: '150ms',
178
+ transitionDuration: duration.fast,
178
179
  },
179
180
 
180
181
  // --- Inline wrapper (for Trigger / Close children) ---
@@ -3,6 +3,7 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
5
  import {shadows} from '@duro-app/tokens/tokens/shadows.css'
6
+ import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
6
7
 
7
8
  export const styles = css.create({
8
9
  // --- Backdrop ---
@@ -17,8 +18,8 @@ export const styles = css.create({
17
18
  from: {opacity: 0},
18
19
  to: {opacity: 1},
19
20
  }),
20
- animationDuration: '200ms',
21
- animationTimingFunction: 'ease-out',
21
+ animationDuration: duration.base,
22
+ animationTimingFunction: easing.easeOut,
22
23
  animationFillMode: 'both',
23
24
  },
24
25
  backdropClosing: {
@@ -27,7 +28,7 @@ export const styles = css.create({
27
28
  to: {opacity: 0},
28
29
  }),
29
30
  animationDuration: '160ms',
30
- animationTimingFunction: 'ease-in',
31
+ animationTimingFunction: easing.easeIn,
31
32
  animationFillMode: 'both',
32
33
  },
33
34
 
@@ -104,8 +105,8 @@ export const styles = css.create({
104
105
  from: {transform: 'translateX(100%)'},
105
106
  to: {transform: 'translateX(0)'},
106
107
  }),
107
- animationDuration: '200ms',
108
- animationTimingFunction: 'ease-out',
108
+ animationDuration: duration.base,
109
+ animationTimingFunction: easing.easeOut,
109
110
  animationFillMode: 'both',
110
111
  },
111
112
  slideOutRight: {
@@ -114,7 +115,7 @@ export const styles = css.create({
114
115
  to: {transform: 'translateX(100%)'},
115
116
  }),
116
117
  animationDuration: '160ms',
117
- animationTimingFunction: 'ease-in',
118
+ animationTimingFunction: easing.easeIn,
118
119
  animationFillMode: 'both',
119
120
  },
120
121
 
@@ -124,8 +125,8 @@ export const styles = css.create({
124
125
  from: {transform: 'translateX(-100%)'},
125
126
  to: {transform: 'translateX(0)'},
126
127
  }),
127
- animationDuration: '200ms',
128
- animationTimingFunction: 'ease-out',
128
+ animationDuration: duration.base,
129
+ animationTimingFunction: easing.easeOut,
129
130
  animationFillMode: 'both',
130
131
  },
131
132
  slideOutLeft: {
@@ -134,7 +135,7 @@ export const styles = css.create({
134
135
  to: {transform: 'translateX(-100%)'},
135
136
  }),
136
137
  animationDuration: '160ms',
137
- animationTimingFunction: 'ease-in',
138
+ animationTimingFunction: easing.easeIn,
138
139
  animationFillMode: 'both',
139
140
  },
140
141
 
@@ -144,8 +145,8 @@ export const styles = css.create({
144
145
  from: {transform: 'translateY(100%)'},
145
146
  to: {transform: 'translateY(0)'},
146
147
  }),
147
- animationDuration: '200ms',
148
- animationTimingFunction: 'ease-out',
148
+ animationDuration: duration.base,
149
+ animationTimingFunction: easing.easeOut,
149
150
  animationFillMode: 'both',
150
151
  },
151
152
  slideOutBottom: {
@@ -154,7 +155,7 @@ export const styles = css.create({
154
155
  to: {transform: 'translateY(100%)'},
155
156
  }),
156
157
  animationDuration: '160ms',
157
- animationTimingFunction: 'ease-in',
158
+ animationTimingFunction: easing.easeIn,
158
159
  animationFillMode: 'both',
159
160
  },
160
161
 
@@ -232,7 +233,7 @@ export const styles = css.create({
232
233
  padding: 0,
233
234
  flexShrink: 0,
234
235
  transitionProperty: 'background-color',
235
- transitionDuration: '150ms',
236
+ transitionDuration: duration.fast,
236
237
  },
237
238
 
238
239
  // --- Inline wrapper ---
@@ -21,6 +21,12 @@ export type IconName =
21
21
  | 'git-branch'
22
22
  | 'menu'
23
23
  | 'pin'
24
+ // People / access / admin glyphs
25
+ | 'users'
26
+ | 'user-plus'
27
+ | 'mail'
28
+ | 'file-text'
29
+ | 'plug'
24
30
  // Color-mode glyphs
25
31
  | 'sun'
26
32
  | 'moon'
@@ -153,6 +159,46 @@ const strokeIcons: Partial<Record<IconName, ReactNode>> = {
153
159
  <line x1="3" y1="18" x2="21" y2="18" />
154
160
  </>
155
161
  ),
162
+ // ---- people / access / admin ----
163
+ users: (
164
+ <>
165
+ <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
166
+ <circle cx="9" cy="7" r="4" />
167
+ <path d="M23 21v-2a4 4 0 0 0-3-3.87" />
168
+ <path d="M16 3.13a4 4 0 0 1 0 7.75" />
169
+ </>
170
+ ),
171
+ 'user-plus': (
172
+ <>
173
+ <path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
174
+ <circle cx="8.5" cy="7" r="4" />
175
+ <line x1="20" y1="8" x2="20" y2="14" />
176
+ <line x1="23" y1="11" x2="17" y2="11" />
177
+ </>
178
+ ),
179
+ mail: (
180
+ <>
181
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
182
+ <polyline points="22,6 12,13 2,6" />
183
+ </>
184
+ ),
185
+ 'file-text': (
186
+ <>
187
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
188
+ <polyline points="14 2 14 8 20 8" />
189
+ <line x1="16" y1="13" x2="8" y2="13" />
190
+ <line x1="16" y1="17" x2="8" y2="17" />
191
+ <polyline points="10 9 9 9 8 9" />
192
+ </>
193
+ ),
194
+ plug: (
195
+ <>
196
+ <path d="M12 22v-5" />
197
+ <path d="M9 8V2" />
198
+ <path d="M15 8V2" />
199
+ <path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z" />
200
+ </>
201
+ ),
156
202
  pin: (
157
203
  <>
158
204
  <line x1="12" y1="17" x2="12" y2="22" />
@@ -2,6 +2,7 @@ import {css} from 'react-strict-dom'
2
2
  import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+ import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
5
6
 
6
7
  export const styles = css.create({
7
8
  base: {
@@ -19,8 +20,8 @@ export const styles = css.create({
19
20
  borderStyle: 'solid',
20
21
  borderRadius: radii.sm,
21
22
  transitionProperty: 'border-color',
22
- transitionDuration: '150ms',
23
- transitionTimingFunction: 'ease',
23
+ transitionDuration: duration.fast,
24
+ transitionTimingFunction: easing.standard,
24
25
  outlineWidth: {
25
26
  default: 0,
26
27
  ':focus-visible': 2,
@@ -2,6 +2,7 @@ import {css} from 'react-strict-dom'
2
2
  import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+ import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
5
6
 
6
7
  export const styles = css.create({
7
8
  wrapper: {
@@ -16,8 +17,8 @@ export const styles = css.create({
16
17
  borderRadius: radii.sm,
17
18
  overflow: 'hidden',
18
19
  transitionProperty: 'border-color',
19
- transitionDuration: '150ms',
20
- transitionTimingFunction: 'ease',
20
+ transitionDuration: duration.fast,
21
+ transitionTimingFunction: easing.standard,
21
22
  },
22
23
  addon: {
23
24
  display: 'inline-flex',
@@ -51,8 +52,8 @@ export const styles = css.create({
51
52
  ':active': colors.bg,
52
53
  },
53
54
  transitionProperty: 'background-color',
54
- transitionDuration: '150ms',
55
- transitionTimingFunction: 'ease',
55
+ transitionDuration: duration.fast,
56
+ transitionTimingFunction: easing.standard,
56
57
  },
57
58
  addonDisabled: {
58
59
  opacity: 0.4,
@@ -2,6 +2,7 @@ import {css} from 'react-strict-dom'
2
2
  import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+ import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
5
6
 
6
7
  export const styles = css.create({
7
8
  base: {
@@ -17,8 +18,8 @@ export const styles = css.create({
17
18
  cursor: 'pointer',
18
19
  textDecoration: 'none',
19
20
  transitionProperty: 'background-color, border-color, color',
20
- transitionDuration: '150ms',
21
- transitionTimingFunction: 'ease',
21
+ transitionDuration: duration.fast,
22
+ transitionTimingFunction: easing.standard,
22
23
  },
23
24
  sizeDefault: {
24
25
  paddingTop: spacing.sm,
@@ -3,6 +3,7 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
5
  import {shadows} from '@duro-app/tokens/tokens/shadows.css'
6
+ import {duration} from '@duro-app/tokens/tokens/motion.css'
6
7
 
7
8
  export const styles = css.create({
8
9
  root: {
@@ -30,7 +31,7 @@ export const styles = css.create({
30
31
  borderRadius: radii.sm,
31
32
  cursor: 'pointer',
32
33
  transitionProperty: 'background-color, border-color',
33
- transitionDuration: '150ms',
34
+ transitionDuration: duration.fast,
34
35
  },
35
36
  backdrop: {
36
37
  position: 'fixed',
@@ -74,7 +75,7 @@ export const styles = css.create({
74
75
  cursor: 'pointer',
75
76
  backgroundColor: 'transparent',
76
77
  transitionProperty: 'background-color',
77
- transitionDuration: '150ms',
78
+ transitionDuration: duration.fast,
78
79
  },
79
80
  itemHighlighted: {
80
81
  backgroundColor: colors.bgCardHover,
@@ -2,6 +2,7 @@ import {css} from 'react-strict-dom'
2
2
  import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+ import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
5
6
 
6
7
  export const styles = css.create({
7
8
  root: {
@@ -36,8 +37,8 @@ export const styles = css.create({
36
37
  justifyContent: 'center',
37
38
  flexShrink: 0,
38
39
  transitionProperty: 'background-color, border-color',
39
- transitionDuration: '150ms',
40
- transitionTimingFunction: 'ease',
40
+ transitionDuration: duration.fast,
41
+ transitionTimingFunction: easing.standard,
41
42
  },
42
43
  circleUnchecked: {
43
44
  backgroundColor: colors.bg,
@@ -1,6 +1,7 @@
1
1
  import {css} from 'react-strict-dom'
2
2
  import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {radii} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
4
5
 
5
6
  export const styles = css.create({
6
7
  root: {
@@ -26,8 +27,8 @@ export const styles = css.create({
26
27
  touchAction: 'none',
27
28
  userSelect: 'none',
28
29
  transitionProperty: 'opacity',
29
- transitionDuration: '200ms',
30
- transitionTimingFunction: 'ease',
30
+ transitionDuration: duration.base,
31
+ transitionTimingFunction: easing.standard,
31
32
  },
32
33
  scrollbarVertical: {
33
34
  top: 0,
@@ -64,7 +65,7 @@ export const styles = css.create({
64
65
  },
65
66
  borderRadius: radii.full,
66
67
  transitionProperty: 'background-color',
67
- transitionDuration: '150ms',
68
+ transitionDuration: duration.fast,
68
69
  },
69
70
  // Dynamic styles — simple identifier params only (StyleX constraint)
70
71
  viewportMaxHeight: (maxHeight: number | string) => ({
@@ -3,6 +3,7 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
5
  import {shadows} from '@duro-app/tokens/tokens/shadows.css'
6
+ import {duration} from '@duro-app/tokens/tokens/motion.css'
6
7
 
7
8
  export const styles = css.create({
8
9
  root: {
@@ -31,7 +32,7 @@ export const styles = css.create({
31
32
  borderRadius: radii.sm,
32
33
  cursor: 'pointer',
33
34
  transitionProperty: 'border-color',
34
- transitionDuration: '150ms',
35
+ transitionDuration: duration.fast,
35
36
  },
36
37
  value: {
37
38
  color: colors.text,
@@ -94,7 +95,7 @@ export const styles = css.create({
94
95
  cursor: 'pointer',
95
96
  backgroundColor: 'transparent',
96
97
  transitionProperty: 'background-color',
97
- transitionDuration: '150ms',
98
+ transitionDuration: duration.fast,
98
99
  },
99
100
  itemSelected: {
100
101
  color: colors.accent,
@@ -2,27 +2,33 @@ import type {ComponentMeta} from '../component-meta'
2
2
 
3
3
  export const meta: ComponentMeta = {
4
4
  description:
5
- 'Vertical side navigation with collapsible groups. Supports controlled and uncontrolled active item. Compound component — Root is required.',
6
- whenToUse: ['App-level sidebar navigation', 'Settings navigation with grouped sections'],
5
+ 'Vertical side navigation. Use collapsible `Group`s (chevron accordion) or static `Section`s (always-open headers) — or a mix. `Item`s take an optional `icon` and show an active left-marker. Supports controlled and uncontrolled active item. Compound component — Root is required.',
6
+ whenToUse: [
7
+ 'App-level sidebar navigation',
8
+ 'Settings navigation with grouped sections',
9
+ 'Flat menu with labelled, always-open regions — use Section instead of Group',
10
+ ],
7
11
  whenNotToUse: [
8
12
  'In-page content switching — use Tabs',
9
13
  'Top-level horizontal nav — use Inline with LinkButton',
10
14
  ],
11
15
  anatomy: {
12
- required: ['Root', 'Group', 'Item'],
16
+ required: ['Root', 'Item'],
17
+ optional: ['Group', 'Section'],
13
18
  },
14
19
  relatedTo: [
15
20
  {component: 'Tabs', relationship: 'Tabs switch content in-place; SideNav navigates pages'},
16
21
  {component: 'PageShell', relationship: 'Often placed alongside PageShell'},
17
22
  ],
18
- example: `<SideNav.Root defaultValue="dashboard">
19
- <SideNav.Group label="Main">
20
- <SideNav.Item value="dashboard">Dashboard</SideNav.Item>
21
- <SideNav.Item value="analytics">Analytics</SideNav.Item>
22
- </SideNav.Group>
23
- <SideNav.Group label="Settings" defaultExpanded={false}>
24
- <SideNav.Item value="profile">Profile</SideNav.Item>
25
- <SideNav.Item value="billing">Billing</SideNav.Item>
23
+ example: `<SideNav.Root defaultValue="identities">
24
+ {/* Static, always-open sections with icons (flat menu) */}
25
+ <SideNav.Section label="People & access">
26
+ <SideNav.Item value="identities" icon={<Icon name="users" size={18} />}>Identities</SideNav.Item>
27
+ <SideNav.Item value="grants" icon={<Icon name="key" size={18} />}>Grants</SideNav.Item>
28
+ </SideNav.Section>
29
+ {/* Or collapsible groups (accordion) */}
30
+ <SideNav.Group label="Advanced" defaultExpanded={false}>
31
+ <SideNav.Item value="plugins" icon={<Icon name="plug" size={18} />}>Plugins</SideNav.Item>
26
32
  </SideNav.Group>
27
33
  </SideNav.Root>`,
28
34
  }
@@ -1,6 +1,7 @@
1
1
  import type {Meta, StoryObj} from '@storybook/react'
2
2
  import {expect, fn} from 'storybook/test'
3
3
  import {SideNav} from './SideNav'
4
+ import {Icon} from '../Icon/Icon'
4
5
 
5
6
  const meta: Meta<typeof SideNav.Root> = {
6
7
  title: 'Components/SideNav',
@@ -31,7 +32,7 @@ export const Default: Story = {
31
32
  const nav = canvas.getByRole('navigation')
32
33
  await expect(nav).toBeInTheDocument()
33
34
 
34
- const activeItem = canvas.getByText('Dashboard')
35
+ const activeItem = canvas.getByRole('button', {name: 'Dashboard'})
35
36
  await expect(activeItem).toHaveAttribute('aria-current', 'page')
36
37
  },
37
38
  }
@@ -74,7 +75,7 @@ export const DefaultExpandedWithActiveItem: Story = {
74
75
  play: async ({canvas}) => {
75
76
  const trigger = canvas.getByText('Settings')
76
77
  await expect(trigger).toHaveAttribute('aria-expanded', 'true')
77
- const active = canvas.getByText('Security')
78
+ const active = canvas.getByRole('button', {name: 'Security'})
78
79
  await expect(active).toBeInTheDocument()
79
80
  await expect(active).toHaveAttribute('aria-current', 'page')
80
81
  },
@@ -91,9 +92,52 @@ export const Interactive: Story = {
91
92
  </SideNav.Root>
92
93
  ),
93
94
  play: async ({canvas, userEvent, args}) => {
94
- const aboutItem = canvas.getByText('About')
95
+ const aboutItem = canvas.getByRole('button', {name: 'About'})
95
96
  await userEvent.click(aboutItem)
96
97
  await expect(args.onValueChange).toHaveBeenCalledWith('about')
97
98
  await expect(aboutItem).toHaveAttribute('aria-current', 'page')
98
99
  },
99
100
  }
101
+
102
+ // Flat menu: static (non-collapsible) Section headers + per-item icons + the
103
+ // active left-marker. This is the AppChrome-style layout — no chevrons, every
104
+ // section always open.
105
+ export const FlatWithIcons: Story = {
106
+ render: (args) => (
107
+ <SideNav.Root {...args} defaultValue="grants">
108
+ <SideNav.Section label="People & access">
109
+ <SideNav.Item value="identities" icon={<Icon name="users" size={18} />}>
110
+ Identities
111
+ </SideNav.Item>
112
+ <SideNav.Item value="grants" icon={<Icon name="key" size={18} />}>
113
+ Grants
114
+ </SideNav.Item>
115
+ </SideNav.Section>
116
+ <SideNav.Section label="Requests & invites">
117
+ <SideNav.Item value="invitations" icon={<Icon name="mail" size={18} />}>
118
+ Invitations
119
+ </SideNav.Item>
120
+ <SideNav.Item value="invites" icon={<Icon name="user-plus" size={18} />}>
121
+ User Invites
122
+ </SideNav.Item>
123
+ </SideNav.Section>
124
+ </SideNav.Root>
125
+ ),
126
+ play: async ({canvas, userEvent, args}) => {
127
+ // Static section headers render as plain text (no toggle button).
128
+ await expect(canvas.getByText('People & access')).toBeInTheDocument()
129
+ await expect(canvas.queryByRole('button', {name: 'People & access'})).toBeNull()
130
+
131
+ // All items are visible immediately (nothing is collapsed).
132
+ const grants = canvas.getByRole('button', {name: 'Grants'})
133
+ await expect(grants).toHaveAttribute('aria-current', 'page')
134
+ await expect(canvas.getByRole('button', {name: 'Identities'})).toBeInTheDocument()
135
+ await expect(canvas.getByRole('button', {name: 'User Invites'})).toBeInTheDocument()
136
+
137
+ // Selecting another item moves the active state.
138
+ const identities = canvas.getByRole('button', {name: 'Identities'})
139
+ await userEvent.click(identities)
140
+ await expect(args.onValueChange).toHaveBeenCalledWith('identities')
141
+ await expect(identities).toHaveAttribute('aria-current', 'page')
142
+ },
143
+ }
@@ -150,14 +150,39 @@ function Group({children, label, groupKey, defaultExpanded}: GroupProps) {
150
150
  )
151
151
  }
152
152
 
153
+ // --- Section ---
154
+
155
+ interface SectionProps {
156
+ children: ReactNode
157
+ label: string
158
+ }
159
+
160
+ /**
161
+ * A static, non-collapsible grouping: an always-visible uppercase header with
162
+ * its items below. Unlike `Group` there is no chevron, toggle, or hidden state
163
+ * — use it when every section should stay open at a glance (a flat menu with
164
+ * labelled regions rather than an accordion).
165
+ */
166
+ function Section({children, label}: SectionProps) {
167
+ return (
168
+ <html.div style={styles.section}>
169
+ <html.div style={styles.sectionLabel}>{label}</html.div>
170
+ {children}
171
+ </html.div>
172
+ )
173
+ }
174
+
153
175
  // --- Item ---
154
176
 
155
177
  interface ItemProps {
156
178
  value: string
157
179
  children: ReactNode
180
+ /** Optional leading glyph (e.g. a design-system `<Icon />`). Inherits the
181
+ * item's text color, so it turns accent when the item is active. */
182
+ icon?: ReactNode
158
183
  }
159
184
 
160
- function Item({value, children}: ItemProps) {
185
+ function Item({value, children, icon}: ItemProps) {
161
186
  const {activeValue, onSelect, registerItem} = useSideNav()
162
187
  const isActive = activeValue === value
163
188
 
@@ -173,7 +198,13 @@ function Item({value, children}: ItemProps) {
173
198
  style={[styles.item, isActive && styles.itemActive]}
174
199
  aria-current={isActive ? 'page' : undefined}
175
200
  >
176
- {children}
201
+ <html.span aria-hidden style={[styles.marker, isActive && styles.markerActive]} />
202
+ {icon ? (
203
+ <html.span aria-hidden style={styles.itemIcon}>
204
+ {icon}
205
+ </html.span>
206
+ ) : null}
207
+ <html.span style={styles.itemLabel}>{children}</html.span>
177
208
  </html.button>
178
209
  )
179
210
  }
@@ -181,5 +212,6 @@ function Item({value, children}: ItemProps) {
181
212
  export const SideNav = {
182
213
  Root,
183
214
  Group,
215
+ Section,
184
216
  Item,
185
217
  }