@estiva-app/ui 0.24.0 โ†’ 0.24.1

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 (74) hide show
  1. package/dist/ListColumn.d.ts +41 -0
  2. package/dist/ListColumn.d.ts.map +1 -0
  3. package/dist/Select.d.ts +1 -0
  4. package/dist/Select.d.ts.map +1 -1
  5. package/dist/Toolbar.d.ts +18 -0
  6. package/dist/Toolbar.d.ts.map +1 -1
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +65 -38
  10. package/dist/index.js.map +4 -4
  11. package/package.json +1 -1
  12. package/registry.json +164 -4
  13. package/src/AppShell.mdx +8 -1
  14. package/src/AttachmentCard.mdx +3 -1
  15. package/src/Avatar.mdx +7 -2
  16. package/src/Banner.mdx +4 -5
  17. package/src/Breadcrumb.mdx +2 -0
  18. package/src/Button.mdx +3 -2
  19. package/src/Button.tsx +2 -2
  20. package/src/Card.mdx +5 -1
  21. package/src/Checkbox.mdx +1 -0
  22. package/src/ChipInput.mdx +1 -1
  23. package/src/CollapsibleSection.mdx +3 -3
  24. package/src/ConfirmDialog.mdx +2 -2
  25. package/src/ContainerHeader.mdx +12 -3
  26. package/src/DialogShell.mdx +1 -1
  27. package/src/Divider.mdx +3 -0
  28. package/src/EditableText.mdx +6 -1
  29. package/src/EmptyState.mdx +2 -2
  30. package/src/Field.mdx +3 -4
  31. package/src/Form.mdx +1 -1
  32. package/src/IconButton.mdx +1 -1
  33. package/src/IdentityMenu.mdx +1 -1
  34. package/src/InlineChip.mdx +3 -0
  35. package/src/Kbd.mdx +2 -2
  36. package/src/Link.mdx +3 -1
  37. package/src/ListColumn.mdx +87 -0
  38. package/src/ListColumn.stories.tsx +136 -0
  39. package/src/ListColumn.test.tsx +50 -0
  40. package/src/ListColumn.tsx +63 -0
  41. package/src/Menu.mdx +2 -0
  42. package/src/MenuItem.mdx +3 -0
  43. package/src/NavItem.mdx +4 -0
  44. package/src/Person.mdx +3 -0
  45. package/src/PersonTrigger.mdx +2 -1
  46. package/src/PersonTrigger.tsx +1 -1
  47. package/src/Popover.mdx +2 -0
  48. package/src/Rail.mdx +3 -0
  49. package/src/RailItem.mdx +4 -0
  50. package/src/RailItem.tsx +1 -1
  51. package/src/Reaction.mdx +1 -0
  52. package/src/Reaction.tsx +1 -1
  53. package/src/ScrollArea.mdx +3 -0
  54. package/src/SectionHeader.mdx +5 -1
  55. package/src/SectionHeader.tsx +1 -1
  56. package/src/SectionLabel.mdx +6 -2
  57. package/src/Select.mdx +3 -2
  58. package/src/Select.test.tsx +12 -0
  59. package/src/Select.tsx +4 -3
  60. package/src/Skeleton.mdx +5 -1
  61. package/src/Skeleton.tsx +1 -1
  62. package/src/TextInput.mdx +1 -0
  63. package/src/TextInput.tsx +1 -1
  64. package/src/Toolbar.mdx +9 -2
  65. package/src/Toolbar.stories.tsx +19 -2
  66. package/src/Toolbar.test.tsx +24 -1
  67. package/src/Toolbar.tsx +22 -0
  68. package/src/Tooltip.mdx +2 -1
  69. package/src/Tooltip.test.tsx +22 -0
  70. package/src/Tooltip.tsx +1 -1
  71. package/src/TopBar.mdx +8 -1
  72. package/src/heights.test.tsx +78 -0
  73. package/src/index.ts +2 -1
  74. package/src/registry/registry.test.ts +16 -8
package/src/Skeleton.tsx CHANGED
@@ -21,7 +21,7 @@ const ROW_BAR_WIDTHS = [150, 100, 170, 120, 90, 140, 110, 160]
21
21
  /** A 32px row: a 16px square and a bar, like a row with a face and a name. */
22
22
  export function SkeletonRow({ barWidth = 130 }: { barWidth?: number }) {
23
23
  return (
24
- <div className="flex items-center gap-2 px-2 h-[32px] rounded-lg">
24
+ <div className="flex shrink-0 items-center gap-2 px-2 h-[32px] rounded-lg">
25
25
  <SkeletonBar className="w-4 h-4 shrink-0" />
26
26
  <SkeletonBar className="h-3.5" style={{ width: barWidth }} />
27
27
  </div>
package/src/TextInput.mdx CHANGED
@@ -53,6 +53,7 @@ It does these by itself. Write none of them around it.
53
53
  | It does | So you never write | Refused when written by hand |
54
54
  |---|---|---|
55
55
  | Inside a `Field`, takes its name from the Field's label, and its error state | an `id` / `htmlFor` pair, or `aria-invalid` | |
56
+ | Keeps its height in a scrolling column | `shrink-0` | |
56
57
 
57
58
  ## Props
58
59
 
package/src/TextInput.tsx CHANGED
@@ -38,7 +38,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function T
38
38
  'bg-bg-inset border border-border-default hover:border-border-strong focus:border-border-focus rounded-lg',
39
39
  // The small size is the small Select's trigger, class for class.
40
40
  size === 'default' && 'px-3 py-2 text-input-value',
41
- size === 'small' && 'h-6 px-2 text-caption',
41
+ size === 'small' && 'h-6 min-h-6 px-2 text-caption',
42
42
  'text-text-primary placeholder:text-text-muted',
43
43
  'outline-none transition-colors',
44
44
  'disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled',
package/src/Toolbar.mdx CHANGED
@@ -69,8 +69,8 @@ import { Toolbar, ToolbarButton, ToolbarSeparator } from '@estiva-app/ui'
69
69
  A toolbar floats over what it acts on โ€” a card, a paragraph, an image โ€” so it
70
70
  draws the elevated surface that separates it from that: **the same `MenuPanel`
71
71
  a `Menu` draws**, because a floating strip and a floating list are the same
72
- box. Peek had built this twice and the two had already drifted: one
73
- `rounded-sm` with `shadow-sm`, the other `rounded-lg` with `shadow-lg`.
72
+ box. Built twice by hand, the two drift: one `rounded-sm` with `shadow-sm`,
73
+ the other `rounded-lg` with `shadow-lg`.
74
74
 
75
75
  `surface={false}` for a strip inside something that draws that box already โ€” a
76
76
  `Popover`, a dialog, a card's own panel.
@@ -82,6 +82,13 @@ why the placement is its job and not arithmetic of ours.
82
82
 
83
83
  <Canvas of={ToolbarStories.OnAnExistingSurface} />
84
84
 
85
+ **A control that goes somewhere is a `ToolbarLink`** with an `href`: the
86
+ strip's look, in the same walk, with a real address, so a Ctrl+click opens a
87
+ new tab. A `ToolbarButton` cannot be a link. A place you cannot go is left out
88
+ of the strip rather than disabled.
89
+
90
+ <Canvas of={ToolbarStories.WithALink} />
91
+
85
92
  ## What it owns
86
93
 
87
94
  It does these by itself. Write none of them around it.
@@ -1,9 +1,9 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
- import { IconArrowBackUp, IconArrowForwardUp, IconBold, IconItalic, IconLink, IconSquareRounded, IconUnderline } from '@tabler/icons-react'
2
+ import { IconArrowBackUp, IconArrowForwardUp, IconArrowUpRight, IconBold, IconItalic, IconLink, IconSquareRounded, IconUnderline } from '@tabler/icons-react'
3
3
  import { useState } from 'react'
4
4
  import { IconButton } from './IconButton'
5
5
  import { Popover } from './Popover'
6
- import { Toolbar, ToolbarButton, ToolbarInput, ToolbarSeparator } from './Toolbar'
6
+ import { Toolbar, ToolbarButton, ToolbarInput, ToolbarLink, ToolbarSeparator } from './Toolbar'
7
7
 
8
8
  /**
9
9
  * A strip of controls that behaves as **one** control: Tab in, arrow keys
@@ -100,6 +100,23 @@ export const WithADisabledControl: Story = {
100
100
  ),
101
101
  }
102
102
 
103
+ /**
104
+ * A control that goes somewhere, beside controls that do something. The link
105
+ * has the buttons' look and joins the same walk; it has a real address, so a
106
+ * Ctrl+click opens a new tab.
107
+ */
108
+ export const WithALink: Story = {
109
+ parameters: { controls: { disable: true } },
110
+ render: () => (
111
+ <Toolbar aria-label="Item actions" surface={false}>
112
+ <ToolbarButton aria-label="Item one" tooltip="Item one">{icon}</ToolbarButton>
113
+ <ToolbarLink href="#item" aria-label="Open the item" tooltip="Open">
114
+ <IconArrowUpRight size={16} stroke={1.5} />
115
+ </ToolbarLink>
116
+ </Toolbar>
117
+ ),
118
+ }
119
+
103
120
  /** Down rather than across: โ†‘ and โ†“ walk it. */
104
121
  export const Vertical: Story = {
105
122
  parameters: { controls: { disable: true } },
@@ -7,7 +7,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
7
7
  import { cleanup, render, screen } from '@testing-library/react'
8
8
  import userEvent from '@testing-library/user-event'
9
9
  import { IconButton } from './IconButton'
10
- import { Toolbar, ToolbarButton, ToolbarInput, ToolbarSeparator } from './Toolbar'
10
+ import { Toolbar, ToolbarButton, ToolbarInput, ToolbarLink, ToolbarSeparator } from './Toolbar'
11
11
  import { Menu, MenuItem } from './Menu'
12
12
  import { Popover } from './Popover'
13
13
 
@@ -176,6 +176,29 @@ describe('Toolbar', () => {
176
176
  })
177
177
  })
178
178
 
179
+ describe('a link in the strip (F3)', () => {
180
+ it('is a real link, in the walk with the buttons beside it', async () => {
181
+ const user = userEvent.setup()
182
+ render(
183
+ <>
184
+ <button type="button">Before</button>
185
+ <Toolbar aria-label="Item actions">
186
+ <ToolbarButton aria-label="One">{dot}</ToolbarButton>
187
+ <ToolbarLink href="/somewhere" aria-label="Open" tooltip="Open">{dot}</ToolbarLink>
188
+ </Toolbar>
189
+ </>,
190
+ )
191
+ const link = screen.getByRole('link', { name: 'Open' })
192
+ expect(link.getAttribute('href')).toBe('/somewhere')
193
+ await user.click(screen.getByRole('button', { name: 'Before' }))
194
+ await user.tab()
195
+ expect(focused()).toBe('One')
196
+ await user.keyboard('{ArrowRight}')
197
+ expect(document.activeElement).toBe(link)
198
+ expect((await screen.findByRole('tooltip')).textContent).toBe('Open')
199
+ })
200
+ })
201
+
179
202
  describe('a loose row, for comparison', () => {
180
203
  /** The measurement the component exists to change: the same three buttons
181
204
  * outside a toolbar are three Tab stops. */
package/src/Toolbar.tsx CHANGED
@@ -129,6 +129,28 @@ export function ToolbarButton({ ref, disabled, disabledReason, ...props }: Toolb
129
129
  )
130
130
  }
131
131
 
132
+ /**
133
+ * A link in the strip: the strip's own look, and it goes somewhere. It has a
134
+ * real address, so a Ctrl+click or a middle click opens a new tab, which a
135
+ * button that navigates cannot do.
136
+ *
137
+ * It is `IconButton`'s `href` joined to the toolbar's walk: Base UI's
138
+ * `Toolbar.Link`. A `ToolbarButton` cannot be a link โ€” Base UI expects a real
139
+ * button there and warns when it gets an anchor (UIG-14, finding F3).
140
+ *
141
+ * A link is never disabled: a place you cannot go is left out of the strip.
142
+ */
143
+ export interface ToolbarLinkProps extends Omit<IconButtonProps, 'href' | 'disabled' | 'disabledReason' | 'pressed' | 'type' | 'ref'> {
144
+ href: string
145
+ ref?: Ref<HTMLAnchorElement>
146
+ }
147
+
148
+ /** An `IconButton` that is a link, and an item of a `Toolbar`: the arrow keys
149
+ * reach it like the buttons beside it. */
150
+ export function ToolbarLink({ ref, href, ...props }: ToolbarLinkProps) {
151
+ return <BaseToolbar.Link ref={ref} render={<IconButton href={href} {...props} />} />
152
+ }
153
+
132
154
  /**
133
155
  * A text field inside the strip โ€” the link editor in a selection toolbar.
134
156
  *
package/src/Tooltip.mdx CHANGED
@@ -52,7 +52,8 @@ import { WithTooltip } from '@estiva-app/ui'
52
52
  `wrapperClassName="min-w-0 shrink"`.
53
53
  - **`inline`** for a trigger inside a line of text โ€” a reference, a name in a
54
54
  sentence: the wrapper is a `<span>`, because a `<div>` is not allowed inside
55
- a paragraph.
55
+ a paragraph. It sits at the top of the line by itself, so the line keeps its
56
+ height; add no wrapper of your own around it.
56
57
  - A label longer than 320px wraps onto more lines rather than running off the
57
58
  screen; a short one stays one line at 30px.
58
59
  - **`Button` and `IconButton` need no wrapper**: pass `tooltip` (or
@@ -78,6 +78,28 @@ describe('WithTooltip', () => {
78
78
  expect(errors).toEqual([])
79
79
  })
80
80
 
81
+ // F1: without `align-top` a chip in the wrapper drops 0.2px and its line grows
82
+ // from 19.6 to 20px; the apps may not add the class to the part, so it carries it.
83
+ it('inline: the wrapper sits at the top of the line by itself', () => {
84
+ render(
85
+ <p>
86
+ <WithTooltip inline label="Item one">
87
+ <span>Item one</span>
88
+ </WithTooltip>
89
+ </p>,
90
+ )
91
+ expect(screen.getByText('Item one').parentElement?.className.split(' ')).toContain('align-top')
92
+ })
93
+
94
+ it('a block wrapper is not moved', () => {
95
+ render(
96
+ <WithTooltip label="Item one">
97
+ <span>Item one</span>
98
+ </WithTooltip>,
99
+ )
100
+ expect(screen.getByText('Item one').parentElement?.className.split(' ')).not.toContain('align-top')
101
+ })
102
+
81
103
  it('takes wrapperClassName, which is what keeps a truncating label truncating', () => {
82
104
  const { container } = render(
83
105
  <WithTooltip label="A long name" wrapperClassName="min-w-0 shrink">
package/src/Tooltip.tsx CHANGED
@@ -171,7 +171,7 @@ export function WithTooltip({ label, shortcut, placement = 'top', wrapperClassNa
171
171
  can carry the handlers for all of them. A control that IS the trigger
172
172
  (Button, IconButton) composes the part onto itself instead, which is
173
173
  what lets a `Dialog.Close` or a `Menu.Trigger` be one of those. */}
174
- <BaseTooltip.Trigger delay={OPEN_DELAY} render={<Wrapper className={cn('inline-flex shrink-0', wrapperClassName)} />}>
174
+ <BaseTooltip.Trigger delay={OPEN_DELAY} render={<Wrapper className={cn('inline-flex shrink-0', inline && 'align-top', wrapperClassName)} />}>
175
175
  {children}
176
176
  </BaseTooltip.Trigger>
177
177
  <TooltipSurface label={label} shortcut={shortcut} placement={placement} />
package/src/TopBar.mdx CHANGED
@@ -57,9 +57,16 @@ import { TopBar, IdentityMenu, SearchInput } from '@estiva-app/ui'
57
57
  - The frame is desktop-only for now โ€” there is no narrow-screen
58
58
  behaviour; the menu button collapses panels, it does not open a drawer.
59
59
 
60
+ - The bar is 52px tall, 20px in on the left and 26px on the right, 16px between
61
+ its slots.
62
+
60
63
  ## What it owns
61
64
 
62
- Nothing. It only draws. Clicks and keys are yours.
65
+ It does these by itself. Write none of them around it.
66
+
67
+ | It does | So you never write | Refused when written by hand |
68
+ |---|---|---|
69
+ | Keeps its 52px in a scrolling column | `shrink-0` | |
63
70
 
64
71
  ## Props
65
72
 
@@ -0,0 +1,78 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * A part with a set height keeps it in a scrolling column (UIG-15; UIG-14 cards
4
+ * T1, M4, M6). A scrolling flex column squashes any child that may shrink:
5
+ * measured in Chrome, a 32px SectionHeader went to 24px, a Skeleton row to 18px,
6
+ * a Button to 18px. jsdom draws nothing, so this pins the class that prevents it;
7
+ * the measurement is in the record.
8
+ *
9
+ * Rows that span their column refuse to shrink (`shrink-0`). Controls, which
10
+ * mostly sit in rows, keep a minimum height instead: `shrink-0` would also stop
11
+ * a row from narrowing them.
12
+ */
13
+ import { afterEach, describe, expect, it } from 'vitest'
14
+ import { cleanup, render, screen } from '@testing-library/react'
15
+ import { Button } from './Button'
16
+ import { PersonTrigger } from './PersonTrigger'
17
+ import { RailItem } from './RailItem'
18
+ import { Reaction } from './Reaction'
19
+ import { SectionHeader } from './SectionHeader'
20
+ import { Select } from './Select'
21
+ import { SkeletonRow } from './Skeleton'
22
+ import { TextInput } from './TextInput'
23
+
24
+ afterEach(cleanup)
25
+
26
+ const classes = (el: Element | null) => (el?.getAttribute('class') ?? '').split(' ')
27
+
28
+ describe('a part with a set height keeps it in a scrolling column', () => {
29
+ it('SectionHeader: shrink-0', () => {
30
+ const { container } = render(<SectionHeader title="Group" />)
31
+ expect(classes(container.querySelector('[class*="h-[32px]"]'))).toContain('shrink-0')
32
+ })
33
+
34
+ it('SkeletonRow: shrink-0', () => {
35
+ const { container } = render(<SkeletonRow />)
36
+ expect(classes(container.firstElementChild)).toContain('shrink-0')
37
+ })
38
+
39
+ it('RailItem: a set 48px, and shrink-0', () => {
40
+ render(<RailItem href="#" label="Item" icon={<span />} />)
41
+ const tile = classes(screen.getByRole('link'))
42
+ expect(tile).toContain('h-12')
43
+ expect(tile).toContain('shrink-0')
44
+ })
45
+
46
+ it('Button: a minimum height at each size', () => {
47
+ render(
48
+ <>
49
+ <Button>Default</Button>
50
+ <Button size="small">Small</Button>
51
+ </>,
52
+ )
53
+ expect(classes(screen.getByRole('button', { name: 'Default' }))).toContain('min-h-8')
54
+ expect(classes(screen.getByRole('button', { name: 'Small' }))).toContain('min-h-6')
55
+ })
56
+
57
+ it('Select and TextInput, small: a minimum height', () => {
58
+ render(
59
+ <>
60
+ <Select size="small" value="a" onChange={() => {}} options={[{ value: 'a', label: 'A' }]} ariaLabel="Pick" />
61
+ <TextInput size="small" aria-label="Type" />
62
+ </>,
63
+ )
64
+ expect(classes(screen.getByRole('combobox', { name: 'Pick' }))).toContain('min-h-6')
65
+ expect(classes(screen.getByRole('textbox', { name: 'Type' }))).toContain('min-h-6')
66
+ })
67
+
68
+ it('PersonTrigger and Reaction: a minimum height', () => {
69
+ render(
70
+ <>
71
+ <PersonTrigger name="Ana Duarte" />
72
+ <Reaction emoji="๐Ÿ‘" count={2} aria-label="Thumbs up" />
73
+ </>,
74
+ )
75
+ expect(classes(screen.getByRole('button', { name: /Ana Duarte/ }))).toContain('min-h-8')
76
+ expect(classes(screen.getByRole('button', { name: 'Thumbs up' }))).toContain('min-h-6')
77
+ })
78
+ })
package/src/index.ts CHANGED
@@ -68,7 +68,7 @@ export { NavItem, type NavItemProps } from './NavItem'
68
68
  export { Popover, type PopoverProps } from './Popover'
69
69
  export { ScrollArea, type ScrollAreaProps } from './ScrollArea'
70
70
  export { ReactionPicker, type ReactionOption, type ReactionPickerProps } from './ReactionPicker'
71
- export { Toolbar, ToolbarButton, ToolbarInput, ToolbarSeparator, type ToolbarProps, type ToolbarButtonProps, type ToolbarInputProps } from './Toolbar'
71
+ export { Toolbar, ToolbarButton, ToolbarInput, ToolbarLink, ToolbarSeparator, type ToolbarProps, type ToolbarButtonProps, type ToolbarInputProps, type ToolbarLinkProps } from './Toolbar'
72
72
  export { PreviewCard, type PreviewCardProps } from './PreviewCard'
73
73
  export { Person, type PersonProps } from './Person'
74
74
  export { Rail, type RailProps } from './Rail'
@@ -81,6 +81,7 @@ export { Reaction, type ReactionProps } from './Reaction'
81
81
  export { SearchInput, type SearchInputProps } from './SearchInput'
82
82
  export { SectionHeader, type SectionAction, type SectionHeaderProps } from './SectionHeader'
83
83
  export { ContainerHeader, type ContainerHeaderProps } from './ContainerHeader'
84
+ export { ListColumn, type ListColumnProps } from './ListColumn'
84
85
  export { CollapsibleSection, type CollapsibleSectionProps } from './CollapsibleSection'
85
86
  export { SectionLabel } from './SectionLabel'
86
87
  export { Tabs, type TabDef, type TabsProps } from './Tabs'
@@ -45,17 +45,17 @@ describe('the registry builds', () => {
45
45
  expect(registry.entries.map((one) => one.name).sort()).toEqual(values.map((one) => one.name).sort())
46
46
  })
47
47
 
48
- it('counts 82 names over 54 files: 75 components, 6 helpers and 1 hook (ContainerHeader joined in UIG-13)', () => {
48
+ it('counts 84 names over 55 files: 77 components, 6 helpers and 1 hook (ListColumn and ToolbarLink joined in UIG-14)', () => {
49
49
  // The reconciliation GATES.md ยง24 explains. If this changes, that changes.
50
50
  const kinds = registry.entries.reduce<Record<string, number>>((all, one) => ({ ...all, [one.kind]: (all[one.kind] ?? 0) + 1 }), {})
51
- expect(kinds).toEqual({ component: 75, helper: 6, hook: 1 })
52
- expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(54)
51
+ expect(kinds).toEqual({ component: 77, helper: 6, hook: 1 })
52
+ expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(55)
53
53
  })
54
54
 
55
55
  it('gives every entry a purpose, from its own page or from the comment above it', () => {
56
56
  expect(registry.entries.filter((one) => one.purpose.trim() === '')).toEqual([])
57
- expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(54)
58
- expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(28)
57
+ expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(55)
58
+ expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(29)
59
59
  })
60
60
 
61
61
  it('takes a name with a page of its own from the page, and a name without one from the code', () => {
@@ -278,7 +278,7 @@ describe('the ids point at something', () => {
278
278
  describe('a second parser agrees', async () => {
279
279
  const docgen = await import('react-docgen').catch(() => null)
280
280
 
281
- it.skipIf(!docgen)('finds the same props on all 75 components', async () => {
281
+ it.skipIf(!docgen)('finds the same props on all 77 components', async () => {
282
282
  if (!docgen) return
283
283
  const resolver = new docgen.builtinResolvers.FindExportedDefinitionsResolver({ limit: 0 })
284
284
  const byName = new Map(registry.entries.map((one) => [one.name, one]))
@@ -321,12 +321,14 @@ describe('a second parser agrees', async () => {
321
321
  const INHERITED_WITH_A_DEFAULT = new Set(['Button.type', 'IconButton.type', 'TextInput.type', 'SearchInput.placeholder'])
322
322
 
323
323
  /**
324
- * The twelve where **this registry is right and `react-docgen` is not**.
324
+ * The eighteen where **this registry is right and `react-docgen` is not**.
325
325
  *
326
326
  * `IdentityPanelProps extends Omit<IdentityMenuProps, 'compact'>` and
327
327
  * `PersonTriggerProps extends Omit<PersonProps, 'className'>`: docgen does not
328
328
  * follow a heritage clause wrapped in `Omit`, so it reports neither `me` nor
329
- * `signedIn`, which `IdentityPanel` *requires*. Checked against the source โ€”
329
+ * `signedIn`, which `IdentityPanel` *requires*. `ToolbarLinkProps extends
330
+ * Omit<IconButtonProps, โ€ฆ>` (UIG-14, F3) is the same: docgen reports none of
331
+ * the IconButton props it keeps. Checked against the source โ€”
330
332
  * `IdentityMenuProps` declares both, and `IdentityPanel` spreads `...rest`
331
333
  * straight into `IdentityRows`, which destructures them.
332
334
  *
@@ -346,6 +348,12 @@ const DOCGEN_STOPS_AT_OMIT = new Set([
346
348
  'PersonTrigger.picture',
347
349
  'PersonTrigger.fallback',
348
350
  'PersonTrigger.size',
351
+ 'ToolbarLink.variant',
352
+ 'ToolbarLink.glow',
353
+ 'ToolbarLink.tooltip',
354
+ 'ToolbarLink.tooltipShortcut',
355
+ 'ToolbarLink.tooltipPlacement',
356
+ 'ToolbarLink.children',
349
357
  ])
350
358
 
351
359
  describe('ui:find', () => {