@duro-app/ui 5.5.0 → 5.6.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 (66) hide show
  1. package/dist/{Table-DYN-bp0e.js → Table-B1sWL3GA.js} +404 -387
  2. package/dist/Table-B1sWL3GA.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/LinkButton/styles.css.d.ts +1 -0
  10. package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
  11. package/dist/components/Menu/Menu.d.ts +4 -1
  12. package/dist/components/Menu/Menu.d.ts.map +1 -1
  13. package/dist/components/Menu/styles.css.d.ts +14 -0
  14. package/dist/components/Menu/styles.css.d.ts.map +1 -1
  15. package/dist/components/Popover/Popover.d.ts +14 -2
  16. package/dist/components/Popover/Popover.d.ts.map +1 -1
  17. package/dist/components/Popover/Popover.meta.d.ts.map +1 -1
  18. package/dist/components/Popover/PopoverLayerContext.d.ts +3 -0
  19. package/dist/components/Popover/PopoverLayerContext.d.ts.map +1 -1
  20. package/dist/components/Popover/PopoverRaised.stories.d.ts +28 -0
  21. package/dist/components/Popover/PopoverRaised.stories.d.ts.map +1 -0
  22. package/dist/components/Popover/styles.css.d.ts +15 -0
  23. package/dist/components/Popover/styles.css.d.ts.map +1 -1
  24. package/dist/components/Select/Select.d.ts +4 -1
  25. package/dist/components/Select/Select.d.ts.map +1 -1
  26. package/dist/components/Select/styles.css.d.ts +9 -0
  27. package/dist/components/Select/styles.css.d.ts.map +1 -1
  28. package/dist/components/Toggle/Toggle.d.ts +2 -1
  29. package/dist/components/Toggle/Toggle.d.ts.map +1 -1
  30. package/dist/components/Toggle/styles.css.d.ts +1 -0
  31. package/dist/components/Toggle/styles.css.d.ts.map +1 -1
  32. package/dist/index.css +1 -1
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1603 -1558
  36. package/dist/index.js.map +1 -1
  37. package/dist/shared/ModalContext.d.ts +7 -0
  38. package/dist/shared/ModalContext.d.ts.map +1 -1
  39. package/dist/shared/exports.typecheck.d.ts +9 -0
  40. package/dist/shared/exports.typecheck.d.ts.map +1 -0
  41. package/dist/shared/types.d.ts +2 -0
  42. package/dist/shared/types.d.ts.map +1 -1
  43. package/dist/table.js +1 -1
  44. package/package.json +2 -2
  45. package/src/components/Button/Button.tsx +2 -1
  46. package/src/components/Button/styles.css.ts +3 -0
  47. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +172 -14
  48. package/src/components/LinkButton/styles.css.ts +4 -0
  49. package/src/components/Menu/Menu.meta.ts +1 -1
  50. package/src/components/Menu/Menu.tsx +17 -2
  51. package/src/components/Menu/styles.css.ts +18 -0
  52. package/src/components/Popover/Popover.meta.ts +2 -1
  53. package/src/components/Popover/Popover.tsx +29 -4
  54. package/src/components/Popover/PopoverLayerContext.ts +3 -0
  55. package/src/components/Popover/PopoverRaised.stories.tsx +334 -0
  56. package/src/components/Popover/styles.css.ts +21 -0
  57. package/src/components/Select/Select.meta.ts +1 -1
  58. package/src/components/Select/Select.tsx +5 -1
  59. package/src/components/Select/styles.css.ts +12 -0
  60. package/src/components/Toggle/Toggle.tsx +2 -1
  61. package/src/components/Toggle/styles.css.ts +2 -0
  62. package/src/index.ts +2 -0
  63. package/src/shared/ModalContext.ts +8 -1
  64. package/src/shared/exports.typecheck.ts +16 -0
  65. package/src/shared/types.ts +3 -0
  66. 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
  },
@@ -4,9 +4,10 @@ import {useControllableValue} from '../../hooks/useControllableValue'
4
4
  import {useToggleGroup} from '../ToggleGroup/ToggleGroupContext'
5
5
  import {useGroupedControl} from '../ButtonGroup/useGroupedControl'
6
6
  import {mergeRefs} from '../../shared/mergeRefs'
7
+ import type {ControlSize} from '../../shared/types'
7
8
  import {styles} from './styles.css'
8
9
 
9
- export type ToggleSize = 'default' | 'small'
10
+ export type ToggleSize = ControlSize
10
11
 
11
12
  interface ToggleProps {
12
13
  /** Controlled pressed state (standalone usage). */
@@ -37,6 +37,8 @@ export const styles = css.create({
37
37
  fontSize: typography.fontSizeXs,
38
38
  borderRadius: radii.sm,
39
39
  gap: spacing.xs,
40
+ // Every small control is at least controlSm tall, whatever it holds (5.6).
41
+ minHeight: {default: sizes.controlSm, '@media (pointer: coarse)': sizes.touchTarget},
40
42
  },
41
43
  // wrapped toggles: a fixed block size per size, the touch target under a
42
44
  // coarse pointer. ToggleGroup's maxRows arithmetic reads the same tokens.
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export {Icon, type IconName} from './components/Icon'
14
14
  export {Badge, type BadgeVariant, type BadgeSize} from './components/Badge/Badge'
15
15
  export {Breadcrumb} from './components/Breadcrumb/Breadcrumb'
16
16
  export {Button, type ButtonVariant, type ButtonSize} from './components/Button/Button'
17
+ export type {ControlSize} from './shared/types'
17
18
  export {ButtonGroup, type ButtonGroupProps} from './components/ButtonGroup/ButtonGroup'
18
19
  export {Callout, type CalloutVariant} from './components/Callout/Callout'
19
20
  export {Card, type CardVariant, type CardSize} from './components/Card/Card'
@@ -103,6 +104,7 @@ export {Tooltip} from './components/Tooltip/Tooltip'
103
104
  export {Toolbar, type ToolbarProps} from './components/Toolbar/Toolbar'
104
105
  export {Popover} from './components/Popover/Popover'
105
106
  export type {PopoverHandle} from './components/Popover/Popover'
107
+ export {useInModal} from './shared/ModalContext'
106
108
  export {Switch} from './components/Switch/Switch'
107
109
 
108
110
  // Layout primitives
@@ -13,6 +13,13 @@ import {createContext, useContext} from 'react'
13
13
  */
14
14
  export const ModalContext = createContext(false)
15
15
 
16
- export function useInModal() {
16
+ /**
17
+ * True inside a Dialog or Drawer, false outside. A floating surface that is not
18
+ * a Duro Popover picks its layer with it, the way a Popover does:
19
+ * `useInModal() ? layers.popover : layers.floating`, and for one that must
20
+ * cover another floating surface (a link editor over a format bar)
21
+ * `useInModal() ? layers.popoverRaised : layers.floatingRaised`.
22
+ */
23
+ export function useInModal(): boolean {
17
24
  return useContext(ModalContext)
18
25
  }
@@ -0,0 +1,16 @@
1
+ // Compile-only checks that 5.6's additions are on the package root, where a
2
+ // consumer imports them. `pnpm typecheck` fails if one is dropped from
3
+ // index.ts or changes shape. Never imported at runtime.
4
+
5
+ import type {ButtonSize, ControlSize, ToggleSize, useInModal} from '../index'
6
+
7
+ type Same<T, U> = [T] extends [U] ? ([U] extends [T] ? true : false) : false
8
+
9
+ // A floating surface that is not a Popover picks its layer with it.
10
+ export const useInModalIsBoolean: Same<ReturnType<typeof useInModal>, boolean> = true
11
+ export const useInModalTakesNothing: Same<Parameters<typeof useInModal>, []> = true
12
+
13
+ // The shared size, and the two sizes that alias it.
14
+ export const controlSizes: Same<ControlSize, 'default' | 'small'> = true
15
+ export const buttonSizeIsControlSize: Same<ButtonSize, ControlSize> = true
16
+ export const toggleSizeIsControlSize: Same<ToggleSize, ControlSize> = true
@@ -1,3 +1,6 @@
1
1
  // Cross-component prop types that don't belong to any single component.
2
2
 
3
3
  export type Orientation = 'horizontal' | 'vertical'
4
+
5
+ /** The two control sizes shared by Button, Toggle and the Select, Menu and Popover triggers. */
6
+ export type ControlSize = 'default' | 'small'