@duro-app/ui 5.5.0 → 5.7.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 (115) hide show
  1. package/dist/{Table-DYN-bp0e.js → Table-Dj7PXqO_.js} +405 -387
  2. package/dist/Table-Dj7PXqO_.js.map +1 -0
  3. package/dist/components/Button/Button.d.ts +2 -1
  4. package/dist/components/Button/Button.d.ts.map +1 -1
  5. package/dist/components/Button/styles.css.d.ts +1 -0
  6. package/dist/components/Button/styles.css.d.ts.map +1 -1
  7. package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts +14 -0
  8. package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -1
  9. package/dist/components/DragDrop/DragDrop.d.ts +95 -14
  10. package/dist/components/DragDrop/DragDrop.d.ts.map +1 -1
  11. package/dist/components/DragDrop/DragDrop.meta.d.ts.map +1 -1
  12. package/dist/components/DragDrop/DragDropBoard.stories.d.ts +69 -0
  13. package/dist/components/DragDrop/DragDropBoard.stories.d.ts.map +1 -0
  14. package/dist/components/DragDrop/styles.css.d.ts +80 -3
  15. package/dist/components/DragDrop/styles.css.d.ts.map +1 -1
  16. package/dist/components/LinkButton/styles.css.d.ts +1 -0
  17. package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
  18. package/dist/components/Menu/Menu.d.ts +4 -1
  19. package/dist/components/Menu/Menu.d.ts.map +1 -1
  20. package/dist/components/Menu/styles.css.d.ts +14 -0
  21. package/dist/components/Menu/styles.css.d.ts.map +1 -1
  22. package/dist/components/Meter/Meter.d.ts +17 -0
  23. package/dist/components/Meter/Meter.d.ts.map +1 -0
  24. package/dist/components/Meter/Meter.meta.d.ts +3 -0
  25. package/dist/components/Meter/Meter.meta.d.ts.map +1 -0
  26. package/dist/components/Meter/Meter.stories.d.ts +12 -0
  27. package/dist/components/Meter/Meter.stories.d.ts.map +1 -0
  28. package/dist/components/Meter/styles.css.d.ts +34 -0
  29. package/dist/components/Meter/styles.css.d.ts.map +1 -0
  30. package/dist/components/Popover/Popover.d.ts +14 -2
  31. package/dist/components/Popover/Popover.d.ts.map +1 -1
  32. package/dist/components/Popover/Popover.meta.d.ts.map +1 -1
  33. package/dist/components/Popover/PopoverLayerContext.d.ts +3 -0
  34. package/dist/components/Popover/PopoverLayerContext.d.ts.map +1 -1
  35. package/dist/components/Popover/PopoverRaised.stories.d.ts +28 -0
  36. package/dist/components/Popover/PopoverRaised.stories.d.ts.map +1 -0
  37. package/dist/components/Popover/styles.css.d.ts +15 -0
  38. package/dist/components/Popover/styles.css.d.ts.map +1 -1
  39. package/dist/components/Select/Select.d.ts +4 -1
  40. package/dist/components/Select/Select.d.ts.map +1 -1
  41. package/dist/components/Select/styles.css.d.ts +9 -0
  42. package/dist/components/Select/styles.css.d.ts.map +1 -1
  43. package/dist/components/Timeline/Timeline.d.ts +71 -0
  44. package/dist/components/Timeline/Timeline.d.ts.map +1 -0
  45. package/dist/components/Timeline/Timeline.meta.d.ts +3 -0
  46. package/dist/components/Timeline/Timeline.meta.d.ts.map +1 -0
  47. package/dist/components/Timeline/Timeline.stories.d.ts +26 -0
  48. package/dist/components/Timeline/Timeline.stories.d.ts.map +1 -0
  49. package/dist/components/Timeline/styles.css.d.ts +158 -0
  50. package/dist/components/Timeline/styles.css.d.ts.map +1 -0
  51. package/dist/components/Toast/Toast.meta.d.ts.map +1 -1
  52. package/dist/components/Toast/Toast.stories.d.ts +9 -0
  53. package/dist/components/Toast/Toast.stories.d.ts.map +1 -1
  54. package/dist/components/Toast/ToastProvider.d.ts +11 -5
  55. package/dist/components/Toast/ToastProvider.d.ts.map +1 -1
  56. package/dist/components/Toast/index.d.ts +1 -1
  57. package/dist/components/Toast/index.d.ts.map +1 -1
  58. package/dist/components/Toast/styles.css.d.ts +6 -0
  59. package/dist/components/Toast/styles.css.d.ts.map +1 -1
  60. package/dist/components/Toggle/Toggle.d.ts +2 -1
  61. package/dist/components/Toggle/Toggle.d.ts.map +1 -1
  62. package/dist/components/Toggle/styles.css.d.ts +1 -0
  63. package/dist/components/Toggle/styles.css.d.ts.map +1 -1
  64. package/dist/index.css +1 -1
  65. package/dist/index.d.ts +6 -2
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +4992 -3845
  68. package/dist/index.js.map +1 -1
  69. package/dist/shared/ModalContext.d.ts +7 -0
  70. package/dist/shared/ModalContext.d.ts.map +1 -1
  71. package/dist/shared/exports.typecheck.d.ts +9 -0
  72. package/dist/shared/exports.typecheck.d.ts.map +1 -0
  73. package/dist/shared/types.d.ts +2 -0
  74. package/dist/shared/types.d.ts.map +1 -1
  75. package/dist/table.js +1 -1
  76. package/package.json +2 -2
  77. package/src/components/Button/Button.tsx +2 -1
  78. package/src/components/Button/styles.css.ts +3 -0
  79. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +172 -14
  80. package/src/components/DragDrop/DragDrop.meta.ts +22 -21
  81. package/src/components/DragDrop/DragDrop.tsx +1147 -246
  82. package/src/components/DragDrop/DragDropBoard.stories.tsx +668 -0
  83. package/src/components/DragDrop/styles.css.ts +74 -4
  84. package/src/components/LinkButton/styles.css.ts +4 -0
  85. package/src/components/Menu/Menu.meta.ts +1 -1
  86. package/src/components/Menu/Menu.tsx +17 -2
  87. package/src/components/Menu/styles.css.ts +18 -0
  88. package/src/components/Meter/Meter.meta.ts +24 -0
  89. package/src/components/Meter/Meter.stories.tsx +64 -0
  90. package/src/components/Meter/Meter.tsx +55 -0
  91. package/src/components/Meter/styles.css.ts +30 -0
  92. package/src/components/Popover/Popover.meta.ts +2 -1
  93. package/src/components/Popover/Popover.tsx +29 -4
  94. package/src/components/Popover/PopoverLayerContext.ts +3 -0
  95. package/src/components/Popover/PopoverRaised.stories.tsx +334 -0
  96. package/src/components/Popover/styles.css.ts +21 -0
  97. package/src/components/Select/Select.meta.ts +1 -1
  98. package/src/components/Select/Select.tsx +5 -1
  99. package/src/components/Select/styles.css.ts +12 -0
  100. package/src/components/Timeline/Timeline.meta.ts +46 -0
  101. package/src/components/Timeline/Timeline.stories.tsx +238 -0
  102. package/src/components/Timeline/Timeline.tsx +332 -0
  103. package/src/components/Timeline/styles.css.ts +162 -0
  104. package/src/components/Toast/Toast.meta.ts +2 -1
  105. package/src/components/Toast/Toast.stories.tsx +84 -0
  106. package/src/components/Toast/ToastProvider.tsx +49 -11
  107. package/src/components/Toast/index.ts +7 -1
  108. package/src/components/Toast/styles.css.ts +2 -0
  109. package/src/components/Toggle/Toggle.tsx +2 -1
  110. package/src/components/Toggle/styles.css.ts +2 -0
  111. package/src/index.ts +18 -0
  112. package/src/shared/ModalContext.ts +8 -1
  113. package/src/shared/exports.typecheck.ts +16 -0
  114. package/src/shared/types.ts +3 -0
  115. package/dist/Table-DYN-bp0e.js.map +0 -1
@@ -0,0 +1,334 @@
1
+ import {useState, type ReactNode} from 'react'
2
+ import type {Meta, StoryObj} from '@storybook/react'
3
+ import {expect, userEvent, waitFor, within} from 'storybook/test'
4
+ import {html} from 'react-strict-dom'
5
+ import {LAYERS} from '@duro-app/tokens/keys'
6
+ import {Popover} from './Popover'
7
+ import {Button} from '../Button/Button'
8
+ import {Dialog} from '../Dialog/Dialog'
9
+ import {Drawer} from '../Drawer/Drawer'
10
+ import {Select} from '../Select/Select'
11
+ import {Stack} from '../Stack/Stack'
12
+ import {Text} from '../Text/Text'
13
+ import {useInModal} from '../../shared/ModalContext'
14
+
15
+ /**
16
+ * `Popover.Popup raised`: one floating surface over another (a link editor
17
+ * over a selection format bar). Outside a modal it is `floatingRaised` (60),
18
+ * inside a Dialog or Drawer `popoverRaised` (1041). The raised Popover renders
19
+ * first, so without the layer the plain one, portalled after it, would be on
20
+ * top.
21
+ */
22
+ const meta: Meta = {
23
+ title: 'Components/Popover/Raised',
24
+ }
25
+
26
+ export default meta
27
+ type Story = StoryObj
28
+
29
+ const page = () => within(document.body)
30
+
31
+ /** A small rect at the viewport's centre: every Popover here anchors to it. */
32
+ const centre = () => new DOMRect(window.innerWidth / 2 - 10, window.innerHeight / 2 - 10, 20, 20)
33
+
34
+ /** True when the element drawn at (x, y) is `el` or inside it. */
35
+ const drawnAt = (el: Element, x: number, y: number) => {
36
+ const hit = document.elementFromPoint(x, y)
37
+ return hit !== null && el.contains(hit)
38
+ }
39
+
40
+ const centreOf = (el: Element) => {
41
+ const r = el.getBoundingClientRect()
42
+ return [r.left + r.width / 2, r.top + r.height / 2] as const
43
+ }
44
+
45
+ const zIndexOf = (el: Element) => getComputedStyle(el).zIndex
46
+
47
+ /** The format bar: wider and taller than the link editor drawn over it. */
48
+ function FormatBar() {
49
+ return (
50
+ <Popover.Root defaultOpen anchor={centre} align="center">
51
+ <Popover.Popup label="Format bar">
52
+ <Stack gap="md">
53
+ <Text>Bold · Italic · Underline · Strikethrough · Code · Link</Text>
54
+ <Text>Second row of formatting</Text>
55
+ <Text>Third row of formatting</Text>
56
+ </Stack>
57
+ </Popover.Popup>
58
+ </Popover.Root>
59
+ )
60
+ }
61
+
62
+ function LinkEditor({children}: {children?: ReactNode}) {
63
+ return (
64
+ <Popover.Root defaultOpen anchor={centre} align="center">
65
+ <Popover.Popup label="Link editor" raised>
66
+ <Stack gap="sm">
67
+ <Text>https://</Text>
68
+ {children}
69
+ </Stack>
70
+ </Popover.Popup>
71
+ </Popover.Root>
72
+ )
73
+ }
74
+
75
+ /** Raised first, then the plain one: DOM order alone would put the bar on top. */
76
+ function Pair({children}: {children?: ReactNode}) {
77
+ return (
78
+ <>
79
+ <LinkEditor>{children}</LinkEditor>
80
+ <FormatBar />
81
+ </>
82
+ )
83
+ }
84
+
85
+ function OpenDialog({children}: {children: ReactNode}) {
86
+ return (
87
+ <Dialog.Root open>
88
+ <Dialog.Portal size="sm">
89
+ <Dialog.Header>
90
+ <Dialog.Title>Editor</Dialog.Title>
91
+ </Dialog.Header>
92
+ <Dialog.Body>
93
+ <Stack gap="md">
94
+ {/* Enough body for the panel to reach under the Popovers. */}
95
+ {['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth'].map((n) => (
96
+ <Text key={n}>{`${n} paragraph of the document being edited.`}</Text>
97
+ ))}
98
+ {children}
99
+ </Stack>
100
+ </Dialog.Body>
101
+ </Dialog.Portal>
102
+ </Dialog.Root>
103
+ )
104
+ }
105
+
106
+ async function expectRaisedOverBar(layer: number) {
107
+ const editor = await page().findByRole('dialog', {name: 'Link editor'})
108
+ const bar = await page().findByRole('dialog', {name: 'Format bar'})
109
+ await expect(zIndexOf(editor)).toBe(String(layer))
110
+ await waitFor(() => expect(drawnAt(editor, ...centreOf(editor))).toBe(true))
111
+ // The bar is still drawn where the editor does not cover it.
112
+ const b = bar.getBoundingClientRect()
113
+ const e = editor.getBoundingClientRect()
114
+ await expect(b.left).toBeLessThan(e.left)
115
+ await expect(drawnAt(bar, b.left + 2, b.top + b.height / 2)).toBe(true)
116
+ return {editor, bar}
117
+ }
118
+
119
+ /** Outside a modal: the raised editor (60) is over the bar (50). */
120
+ export const RaisedOverFloating: Story = {
121
+ render: () => <Pair />,
122
+ play: async () => {
123
+ const {bar} = await expectRaisedOverBar(LAYERS.floatingRaised)
124
+ await expect(zIndexOf(bar)).toBe(String(LAYERS.floating))
125
+ },
126
+ }
127
+
128
+ /** Inside an open Dialog: the raised editor (1041) is over the bar (1040),
129
+ * and both are over the Dialog panel. */
130
+ export const RaisedInDialog: Story = {
131
+ render: () => (
132
+ <OpenDialog>
133
+ <Pair />
134
+ </OpenDialog>
135
+ ),
136
+ play: async () => {
137
+ const {editor, bar} = await expectRaisedOverBar(LAYERS.popoverRaised)
138
+ await expect(zIndexOf(bar)).toBe(String(LAYERS.popover))
139
+ // Both overlap the panel and are drawn there: the editor at its centre,
140
+ // the bar where the editor leaves it uncovered.
141
+ const panel = page().getByRole('dialog', {name: 'Editor'})
142
+ const p = panel.getBoundingClientRect()
143
+ const b = bar.getBoundingClientRect()
144
+ const e = editor.getBoundingClientRect()
145
+ // Under the editor, inside the bar.
146
+ const x = b.left + b.width / 2
147
+ const y = (e.bottom + b.bottom) / 2
148
+ await expect(x > p.left && x < p.right && y > p.top && y < p.bottom).toBe(true)
149
+ await expect(drawnAt(bar, x, y)).toBe(true)
150
+ const [ex, ey] = centreOf(editor)
151
+ await expect(ex > p.left && ex < p.right && ey > p.top && ey < p.bottom).toBe(true)
152
+ await expect(drawnAt(editor, ex, ey)).toBe(true)
153
+ },
154
+ }
155
+
156
+ /** The Settings dialog's centre once it is open, the viewport's before. */
157
+ const settingsCentre = () => {
158
+ const dialog = page().queryByRole('dialog', {name: 'Settings'})
159
+ if (!dialog) return centre()
160
+ // Just above the centre: the editor opens below its anchor, over the centre.
161
+ const [x, y] = centreOf(dialog)
162
+ return new DOMRect(x - 10, y - 30, 20, 20)
163
+ }
164
+
165
+ function RaisedThenDialog() {
166
+ const [dialogOpen, setDialogOpen] = useState(false)
167
+ return (
168
+ <>
169
+ <Popover.Root open anchor={settingsCentre} align="center">
170
+ <Popover.Popup label="Link editor" raised>
171
+ <Text>Stays open: controlled</Text>
172
+ </Popover.Popup>
173
+ </Popover.Root>
174
+ <html.button type="button" onClick={() => setDialogOpen(true)}>
175
+ Open dialog
176
+ </html.button>
177
+ <Dialog.Root open={dialogOpen} onOpenChange={setDialogOpen}>
178
+ <Dialog.Portal size="sm">
179
+ <Dialog.Header>
180
+ <Dialog.Title>Settings</Dialog.Title>
181
+ </Dialog.Header>
182
+ <Dialog.Body>
183
+ <Text>A modal opened over a raised Popover.</Text>
184
+ </Dialog.Body>
185
+ </Dialog.Portal>
186
+ </Dialog.Root>
187
+ </>
188
+ )
189
+ }
190
+
191
+ /** A raised Popover held open, then a Dialog: the Popover is still mounted
192
+ * and the Dialog covers it at the Dialog's centre. */
193
+ export const DialogOverRaised: Story = {
194
+ render: () => <RaisedThenDialog />,
195
+ play: async ({canvas}) => {
196
+ const editor = await page().findByRole('dialog', {name: 'Link editor'})
197
+ await userEvent.click(canvas.getByRole('button', {name: 'Open dialog'}))
198
+ const dialog = await page().findByRole('dialog', {name: 'Settings'})
199
+ await expect(editor).toBeInTheDocument()
200
+ // Re-place the editor against the open Dialog, so it sits under the
201
+ // Dialog's centre: the check below is not vacuous.
202
+ window.dispatchEvent(new Event('resize'))
203
+ await waitFor(() => {
204
+ const [x, y] = centreOf(dialog)
205
+ const e = editor.getBoundingClientRect()
206
+ expect(x >= e.left && x <= e.right && y >= e.top && y <= e.bottom).toBe(true)
207
+ })
208
+ const [x, y] = centreOf(dialog)
209
+ await waitFor(() => expect(drawnAt(dialog, x, y)).toBe(true))
210
+ },
211
+ }
212
+
213
+ /** A Popover nested in a raised one inherits `raised`: it is drawn over its
214
+ * parent, not at the plain layer under it. */
215
+ function NestedInRaised() {
216
+ return (
217
+ <Popover.Root defaultOpen anchor={centre} align="center">
218
+ <Popover.Popup label="Link editor" raised>
219
+ <Stack gap="md">
220
+ <Popover.Root align="center">
221
+ <Popover.Trigger>Open child</Popover.Trigger>
222
+ <Popover.Popup label="Child">
223
+ <Text>Child</Text>
224
+ </Popover.Popup>
225
+ </Popover.Root>
226
+ <Text>Parent: a raised Popover, wider than the child</Text>
227
+ <Text>Second row</Text>
228
+ <Text>Third row</Text>
229
+ <Text>Fourth row</Text>
230
+ </Stack>
231
+ </Popover.Popup>
232
+ </Popover.Root>
233
+ )
234
+ }
235
+
236
+ async function expectChildOnTop(layer: number) {
237
+ await userEvent.click(await page().findByRole('button', {name: 'Open child'}))
238
+ const child = await page().findByRole('dialog', {name: 'Child'})
239
+ // Non-vacuous: the child overlaps its parent.
240
+ const parent = page().getByRole('dialog', {name: 'Link editor'}).getBoundingClientRect()
241
+ const [cx, cy] = centreOf(child)
242
+ await expect(cx > parent.left && cx < parent.right && cy > parent.top && cy < parent.bottom).toBe(
243
+ true,
244
+ )
245
+ await expect(zIndexOf(child)).toBe(String(layer))
246
+ await waitFor(() => expect(drawnAt(child, ...centreOf(child))).toBe(true))
247
+ }
248
+
249
+ export const NestedInRaised_NoModal: Story = {
250
+ name: 'Nested in raised, no modal',
251
+ render: () => <NestedInRaised />,
252
+ play: async () => expectChildOnTop(LAYERS.floatingRaised),
253
+ }
254
+
255
+ export const NestedInRaised_InDialog: Story = {
256
+ name: 'Nested in raised, in a Dialog',
257
+ render: () => (
258
+ <OpenDialog>
259
+ <NestedInRaised />
260
+ </OpenDialog>
261
+ ),
262
+ play: async () => expectChildOnTop(LAYERS.popoverRaised),
263
+ }
264
+
265
+ /** A Select opened from a raised Popover in a Dialog: its listbox (popup,
266
+ * 1050) is on top. */
267
+ export const SelectInRaisedInDialog: Story = {
268
+ render: () => (
269
+ <OpenDialog>
270
+ <Pair>
271
+ <Select.Root defaultValue="https">
272
+ <Select.Trigger aria-label="Scheme">
273
+ <Select.Value />
274
+ <Select.Icon />
275
+ </Select.Trigger>
276
+ <Select.Popup>
277
+ <Select.Item value="https">https</Select.Item>
278
+ <Select.Item value="mailto">mailto</Select.Item>
279
+ <Select.Item value="tel">tel</Select.Item>
280
+ </Select.Popup>
281
+ </Select.Root>
282
+ </Pair>
283
+ </OpenDialog>
284
+ ),
285
+ play: async () => {
286
+ await userEvent.click(await page().findByRole('combobox', {name: 'Scheme'}))
287
+ const listbox = await page().findByRole('listbox')
288
+ await expect(zIndexOf(listbox)).toBe(String(LAYERS.popup))
289
+ for (const name of ['https', 'mailto', 'tel']) {
290
+ const option = await page().findByRole('option', {name})
291
+ await waitFor(() => expect(drawnAt(option, ...centreOf(option))).toBe(true))
292
+ }
293
+ },
294
+ }
295
+
296
+ function InModalProbe({label}: {label: string}) {
297
+ return <Text>{`${label}: ${String(useInModal())}`}</Text>
298
+ }
299
+
300
+ /** `useInModal()`: true in a Dialog and a Drawer, false outside. Its export
301
+ * from the package root is checked by shared/exports.typecheck.ts. */
302
+ export const UseInModal: Story = {
303
+ render: () => (
304
+ <>
305
+ <InModalProbe label="page" />
306
+ <Button>Unrelated</Button>
307
+ <Dialog.Root open>
308
+ <Dialog.Portal size="sm">
309
+ <Dialog.Header>
310
+ <Dialog.Title>In a dialog</Dialog.Title>
311
+ </Dialog.Header>
312
+ <Dialog.Body>
313
+ <InModalProbe label="dialog" />
314
+ </Dialog.Body>
315
+ </Dialog.Portal>
316
+ </Dialog.Root>
317
+ <Drawer.Root open>
318
+ <Drawer.Portal>
319
+ <Drawer.Header>
320
+ <Drawer.Title>In a drawer</Drawer.Title>
321
+ </Drawer.Header>
322
+ <Drawer.Body>
323
+ <InModalProbe label="drawer" />
324
+ </Drawer.Body>
325
+ </Drawer.Portal>
326
+ </Drawer.Root>
327
+ </>
328
+ ),
329
+ play: async () => {
330
+ await expect(await page().findByText('page: false')).toBeInTheDocument()
331
+ await expect(await page().findByText('dialog: true')).toBeInTheDocument()
332
+ await expect(await page().findByText('drawer: true')).toBeInTheDocument()
333
+ },
334
+ }
@@ -4,6 +4,7 @@ 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
6
  import {duration} from '@duro-app/tokens/tokens/motion.css'
7
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
7
8
  import {borders} from '@duro-app/tokens/tokens/borders.css'
8
9
  import {layers} from '@duro-app/tokens/tokens/layers.css'
9
10
 
@@ -31,11 +32,31 @@ export const styles = css.create({
31
32
  transitionProperty: 'background-color, border-color',
32
33
  transitionDuration: duration.fast,
33
34
  },
35
+ // size="small": Toggle sizeSmall's values, as longhands so they win over the
36
+ // trigger's own longhands. Every small control is at least controlSm tall,
37
+ // whatever it holds, and the touch target under a coarse pointer.
38
+ triggerSmall: {
39
+ gap: spacing.xs,
40
+ paddingTop: spacing.xs,
41
+ paddingBottom: spacing.xs,
42
+ paddingLeft: spacing.sm,
43
+ paddingRight: spacing.sm,
44
+ fontSize: typography.fontSizeXs,
45
+ minHeight: {default: sizes.controlSm, '@media (pointer: coarse)': sizes.touchTarget},
46
+ },
34
47
  // Inside a Dialog or Drawer: above that modal (same portal mount), below a
35
48
  // Select or Menu opened from inside the Popover.
36
49
  popupInModal: {
37
50
  zIndex: layers.popover,
38
51
  },
52
+ // `raised`: over another floating surface, outside a modal (still under a
53
+ // Dialog or Drawer opened later) and inside one (still under its popups).
54
+ popupRaised: {
55
+ zIndex: layers.floatingRaised,
56
+ },
57
+ popupInModalRaised: {
58
+ zIndex: layers.popoverRaised,
59
+ },
39
60
  popup: {
40
61
  position: 'fixed',
41
62
  top: 0,
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
2
2
 
3
3
  export const meta: ComponentMeta = {
4
4
  description:
5
- 'Dropdown select for choosing one value from a list. Compound component — Root is required (throws without it).',
5
+ 'Dropdown select for choosing one value from a list. Compound component — Root is required (throws without it). Select.Trigger takes size="small" to match small Toggles and Buttons in a toolbar.',
6
6
  whenToUse: [
7
7
  'User must pick one option from a predefined list',
8
8
  'Form field for country, category, role selection',
@@ -10,6 +10,7 @@ import {usePopoverLayer} from '../Popover/PopoverLayerContext'
10
10
  import {useGroupedControl} from '../ButtonGroup/useGroupedControl'
11
11
  import {ControlContextBoundary} from '../Toolbar/ControlContextBoundary'
12
12
  import {mergeRefs} from '../../shared/mergeRefs'
13
+ import type {ControlSize} from '../../shared/types'
13
14
 
14
15
  // --- Root ---
15
16
  interface RootProps {
@@ -38,9 +39,12 @@ function Root({name, defaultValue, value, onValueChange, initialLabels, children
38
39
  function Trigger({
39
40
  children,
40
41
  'aria-label': ariaLabel,
42
+ size = 'default',
41
43
  }: {
42
44
  children: ReactNode
43
45
  'aria-label'?: string
46
+ /** `small` matches a small Toggle or Button beside it. Default: `default`. */
47
+ size?: ControlSize
44
48
  }) {
45
49
  const {open, toggle, listboxId, highlightedId, triggerRef} = useSelect()
46
50
  const localRef = useRef<HTMLButtonElement>(null)
@@ -74,7 +78,7 @@ function Trigger({
74
78
  aria-activedescendant={highlightedId ?? undefined}
75
79
  aria-describedby={describedBy || undefined}
76
80
  aria-invalid={fieldCtx?.invalid || undefined}
77
- style={[styles.trigger, grouped.style]}
81
+ style={[styles.trigger, size === 'small' && styles.triggerSmall, grouped.style]}
78
82
  >
79
83
  {children}
80
84
  </html.button>
@@ -37,6 +37,18 @@ export const styles = css.create({
37
37
  transitionProperty: 'border-color',
38
38
  transitionDuration: duration.fast,
39
39
  },
40
+ // size="small": Toggle sizeSmall's values, as longhands so they win over the
41
+ // trigger's own longhands. Every small control is at least controlSm tall,
42
+ // whatever it holds, and the touch target under a coarse pointer.
43
+ triggerSmall: {
44
+ gap: spacing.xs,
45
+ paddingTop: spacing.xs,
46
+ paddingBottom: spacing.xs,
47
+ paddingLeft: spacing.sm,
48
+ paddingRight: spacing.sm,
49
+ fontSize: typography.fontSizeXs,
50
+ minHeight: {default: sizes.controlSm, '@media (pointer: coarse)': sizes.touchTarget},
51
+ },
40
52
  value: {
41
53
  color: colors.text,
42
54
  },
@@ -0,0 +1,46 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Rows of date bars against a date axis (milestones, releases, sprints), with a progress fill per bar and a today marker. Dates are calendar days (YYYY-MM-DD, UTC). A bar is a toggle button (selected/onSelect), can be a DragDrop.Zone target (dropZone), and its edges are keyboard sliders whose value text is a date (onStartChange/onEndChange).',
6
+ anatomy: {required: ['Root', 'Row', 'Bar']},
7
+ whenToUse: [
8
+ 'Milestones or releases over weeks, where the overlap and today matter',
9
+ 'Dropping work onto a milestone (bars as DragDrop zones)',
10
+ ],
11
+ whenNotToUse: [
12
+ 'A schedule by hour — a calendar grid fits (timeGutterW, dayHeaderH)',
13
+ 'A dependency graph or a full Gantt chart with links — this draws bars, not edges',
14
+ 'Moving dates with the pointer — edges are keyboard sliders; give a date field for the rest',
15
+ ],
16
+ relatedTo: [
17
+ {
18
+ component: 'DragDrop',
19
+ kind: 'composition',
20
+ relationship: 'Pass dropZone to make a bar a DragDrop.Zone inside a DragDrop.Root',
21
+ },
22
+ {
23
+ component: 'Meter',
24
+ kind: 'contrast',
25
+ relationship: 'Meter shows one level; Timeline places spans of time',
26
+ },
27
+ ],
28
+ example: `<Timeline.Root aria-label="Milestones" start="2026-09-21" end="2026-11-08" today="2026-10-08">
29
+ {milestones.map((m) => (
30
+ <Timeline.Row key={m.id} label={m.title} description={\`\${m.done} of \${m.total} done\`}>
31
+ <Timeline.Bar
32
+ label={m.title}
33
+ start={m.start}
34
+ end={m.end}
35
+ progress={m.done / m.total}
36
+ selected={selected === m.id}
37
+ onSelect={() => select(m.id)}
38
+ onEndChange={(end) => reschedule(m.id, {end})}
39
+ dropZone={{id: \`ms:\${m.id}\`, label: m.title}}
40
+ >
41
+ {m.done}/{m.total}
42
+ </Timeline.Bar>
43
+ </Timeline.Row>
44
+ ))}
45
+ </Timeline.Root>`,
46
+ }