@estiva-app/ui 0.23.0 → 0.24.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 (98) hide show
  1. package/dist/AppShell.d.ts.map +1 -1
  2. package/dist/AttachmentCard.d.ts.map +1 -1
  3. package/dist/Form.d.ts.map +1 -1
  4. package/dist/IconButton.d.ts +9 -1
  5. package/dist/IconButton.d.ts.map +1 -1
  6. package/dist/PreviewCard.d.ts +9 -2
  7. package/dist/PreviewCard.d.ts.map +1 -1
  8. package/dist/SectionHeader.d.ts +7 -1
  9. package/dist/SectionHeader.d.ts.map +1 -1
  10. package/dist/Select.d.ts +11 -3
  11. package/dist/Select.d.ts.map +1 -1
  12. package/dist/Tooltip.d.ts +13 -1
  13. package/dist/Tooltip.d.ts.map +1 -1
  14. package/dist/eslint/index.js +2 -1
  15. package/dist/eslint/index.js.map +2 -2
  16. package/dist/eslint/no-restyled-part.d.ts.map +1 -1
  17. package/dist/index.js +127 -108
  18. package/dist/index.js.map +2 -2
  19. package/package.json +1 -1
  20. package/registry.json +51 -8
  21. package/src/AppShell.mdx +10 -0
  22. package/src/AppShell.test.tsx +16 -0
  23. package/src/AppShell.tsx +4 -1
  24. package/src/AttachmentCard.mdx +9 -0
  25. package/src/AttachmentCard.test.tsx +10 -0
  26. package/src/AttachmentCard.tsx +8 -5
  27. package/src/Avatar.mdx +13 -3
  28. package/src/AvatarGroup.mdx +4 -0
  29. package/src/Banner.mdx +13 -3
  30. package/src/Breadcrumb.mdx +10 -0
  31. package/src/Button.mdx +14 -3
  32. package/src/Card.mdx +9 -0
  33. package/src/Checkbox.mdx +10 -0
  34. package/src/Chip.mdx +4 -0
  35. package/src/ChipInput.mdx +12 -0
  36. package/src/CollapsibleSection.mdx +9 -0
  37. package/src/CommandPalette.mdx +14 -2
  38. package/src/ConfirmDialog.mdx +11 -0
  39. package/src/ContainerHeader.mdx +4 -0
  40. package/src/DialogShell.mdx +14 -0
  41. package/src/Divider.mdx +9 -1
  42. package/src/EditableText.mdx +11 -0
  43. package/src/EmptyState.mdx +15 -4
  44. package/src/Field.mdx +10 -1
  45. package/src/FieldLine.mdx +9 -1
  46. package/src/FilePicker.mdx +8 -0
  47. package/src/Form.mdx +10 -0
  48. package/src/Form.test.tsx +26 -0
  49. package/src/Form.tsx +7 -0
  50. package/src/IconButton.mdx +16 -2
  51. package/src/IconButton.stories.tsx +3 -0
  52. package/src/IconButton.test.tsx +25 -0
  53. package/src/IconButton.tsx +61 -46
  54. package/src/IdentityMenu.mdx +9 -0
  55. package/src/InlineChip.mdx +8 -0
  56. package/src/Kbd.mdx +4 -0
  57. package/src/Link.mdx +10 -0
  58. package/src/Menu.mdx +16 -1
  59. package/src/MenuItem.mdx +12 -2
  60. package/src/NavItem.mdx +8 -0
  61. package/src/Person.mdx +4 -0
  62. package/src/PersonTrigger.mdx +9 -0
  63. package/src/Popover.mdx +21 -0
  64. package/src/PreviewCard.mdx +19 -4
  65. package/src/PreviewCard.tsx +11 -4
  66. package/src/ProgressBar.mdx +8 -0
  67. package/src/Property.mdx +4 -0
  68. package/src/Rail.mdx +9 -1
  69. package/src/RailItem.mdx +8 -0
  70. package/src/Reaction.mdx +9 -0
  71. package/src/ReactionPicker.mdx +8 -0
  72. package/src/ScrollArea.mdx +13 -2
  73. package/src/SearchInput.mdx +9 -0
  74. package/src/SectionHeader.mdx +13 -0
  75. package/src/SectionHeader.stories.tsx +9 -0
  76. package/src/SectionHeader.test.tsx +9 -0
  77. package/src/SectionHeader.tsx +8 -2
  78. package/src/SectionLabel.mdx +4 -0
  79. package/src/Select.mdx +21 -2
  80. package/src/Select.stories.tsx +4 -1
  81. package/src/Select.test.tsx +17 -0
  82. package/src/Select.tsx +34 -13
  83. package/src/Sidebar.mdx +8 -0
  84. package/src/Skeleton.mdx +4 -0
  85. package/src/Tabs.mdx +12 -1
  86. package/src/TextInput.mdx +8 -0
  87. package/src/Textarea.mdx +8 -0
  88. package/src/Toast.mdx +12 -1
  89. package/src/Toolbar.mdx +11 -0
  90. package/src/Tooltip.mdx +26 -2
  91. package/src/Tooltip.stories.tsx +26 -0
  92. package/src/Tooltip.test.tsx +29 -0
  93. package/src/Tooltip.tsx +17 -4
  94. package/src/TopBar.mdx +4 -0
  95. package/src/eslint/no-rebuilt-behaviour.ts +1 -1
  96. package/src/eslint/no-restyled-part.ts +1 -0
  97. package/src/pages.test.ts +142 -0
  98. package/tailwind-preset.js +22 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estiva-app/ui",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Estiva's design tokens (the contract) and a small set of primitives (a convenience) for every Estiva app.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/registry.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "kind": "package",
5
5
  "repo": "estiva-ui",
6
6
  "package": "@estiva-app/ui",
7
- "packageVersion": "0.23.0",
7
+ "packageVersion": "0.24.0",
8
8
  "exports": 82,
9
9
  "typeExports": 92,
10
10
  "files": null
@@ -1436,7 +1436,7 @@
1436
1436
  },
1437
1437
  {
1438
1438
  "id": "page-keys",
1439
- "behaviour": "Closes on Escape, and takes its keys, by itself"
1439
+ "behaviour": "Takes its keys by itself"
1440
1440
  },
1441
1441
  {
1442
1442
  "id": "focus",
@@ -1927,6 +1927,12 @@
1927
1927
  "required": false,
1928
1928
  "note": "Why the action cannot succeed right now."
1929
1929
  },
1930
+ {
1931
+ "name": "href",
1932
+ "takes": "text",
1933
+ "required": false,
1934
+ "note": "A link that looks like an icon button: an anchor, so the address stays real for a modified click or a new tab, as `Link` keeps it."
1935
+ },
1930
1936
  {
1931
1937
  "name": "children",
1932
1938
  "takes": "anything",
@@ -2477,7 +2483,7 @@
2477
2483
  },
2478
2484
  {
2479
2485
  "id": "page-keys",
2480
- "behaviour": "Closes on Escape, and takes its keys, by itself"
2486
+ "behaviour": "Takes its keys by itself"
2481
2487
  },
2482
2488
  {
2483
2489
  "id": "follow",
@@ -3019,7 +3025,7 @@
3019
3025
  },
3020
3026
  {
3021
3027
  "id": "page-keys",
3022
- "behaviour": "Closes on Escape, and takes its keys, by itself"
3028
+ "behaviour": "Takes its keys by itself"
3023
3029
  },
3024
3030
  {
3025
3031
  "id": "follow",
@@ -3076,7 +3082,13 @@
3076
3082
  "name": "className",
3077
3083
  "takes": "text",
3078
3084
  "required": false,
3079
- "note": "On the card's surface: its width, its padding, a max height."
3085
+ "note": "On the card's surface: its width."
3086
+ },
3087
+ {
3088
+ "name": "contentClassName",
3089
+ "takes": "text",
3090
+ "required": false,
3091
+ "note": "On the scrolling content: its padding (default 12px) and rhythm."
3080
3092
  },
3081
3093
  {
3082
3094
  "name": "wrapperClassName",
@@ -3587,6 +3599,12 @@
3587
3599
  "required": false,
3588
3600
  "note": "`hover` reveals the actions while the row is hovered or focused; `always` keeps them."
3589
3601
  },
3602
+ {
3603
+ "name": "hover",
3604
+ "takes": "fill | none",
3605
+ "required": false,
3606
+ "note": "`fill` lights the row under the pointer when it does something (a toggle, actions); `none` keeps it still — a heading whose actions are always shown, where the buttons light up on their own (UIG-14, Katerina, 19 September)."
3607
+ },
3590
3608
  {
3591
3609
  "name": "render",
3592
3610
  "takes": "useRender.RenderProp",
@@ -3607,6 +3625,13 @@
3607
3625
  "hover",
3608
3626
  "always"
3609
3627
  ]
3628
+ },
3629
+ {
3630
+ "prop": "hover",
3631
+ "values": [
3632
+ "fill",
3633
+ "none"
3634
+ ]
3610
3635
  }
3611
3636
  ],
3612
3637
  "ownsBehaviours": [],
@@ -3719,6 +3744,12 @@
3719
3744
  "required": false,
3720
3745
  "note": null
3721
3746
  },
3747
+ {
3748
+ "name": "disabledReason",
3749
+ "takes": "text",
3750
+ "required": false,
3751
+ "note": "Why it cannot be changed right now (UIG-14, Katerina, 19 September)."
3752
+ },
3722
3753
  {
3723
3754
  "name": "className",
3724
3755
  "takes": "text",
@@ -3750,7 +3781,7 @@
3750
3781
  },
3751
3782
  {
3752
3783
  "id": "page-keys",
3753
- "behaviour": "Closes on Escape, and takes its keys, by itself"
3784
+ "behaviour": "Takes its keys by itself"
3754
3785
  },
3755
3786
  {
3756
3787
  "id": "follow",
@@ -3964,7 +3995,7 @@
3964
3995
  },
3965
3996
  {
3966
3997
  "id": "page-keys",
3967
- "behaviour": "Closes on Escape, and takes its keys, by itself"
3998
+ "behaviour": "Takes its keys by itself"
3968
3999
  },
3969
4000
  {
3970
4001
  "id": "walking",
@@ -4204,7 +4235,7 @@
4204
4235
  },
4205
4236
  {
4206
4237
  "id": "page-keys",
4207
- "behaviour": "Closes on Escape, and takes its keys, by itself"
4238
+ "behaviour": "Takes its keys by itself"
4208
4239
  },
4209
4240
  {
4210
4241
  "id": "walking",
@@ -4279,6 +4310,12 @@
4279
4310
  "required": false,
4280
4311
  "note": "Why the action cannot succeed right now."
4281
4312
  },
4313
+ {
4314
+ "name": "href",
4315
+ "takes": "text",
4316
+ "required": false,
4317
+ "note": "A link that looks like an icon button: an anchor, so the address stays real for a modified click or a new tab, as `Link` keeps it."
4318
+ },
4282
4319
  {
4283
4320
  "name": "children",
4284
4321
  "takes": "anything",
@@ -4576,6 +4613,12 @@
4576
4613
  "required": false,
4577
4614
  "note": "Extra classes on the wrapper — e.g. `min-w-0 shrink` so a truncating label keeps truncating inside it."
4578
4615
  },
4616
+ {
4617
+ "name": "inline",
4618
+ "takes": "true/false",
4619
+ "required": false,
4620
+ "note": "For a trigger inside a line of text — a reference, a name in a sentence: the wrapper is a `<span>`, because a `<div>` is not allowed inside a paragraph (UIG-14, Katerina, 19 September)."
4621
+ },
4579
4622
  {
4580
4623
  "name": "children",
4581
4624
  "takes": "anything",
package/src/AppShell.mdx CHANGED
@@ -75,6 +75,16 @@ import { AppShell, Banner, IdentityMenu, Rail, RailItem } from '@estiva-app/ui'
75
75
  - In the floating manner the card owns `overflow-hidden` — your content
76
76
  manages its own scrolling inside it.
77
77
  - Desktop only for now — no narrow-screen behaviour is provided.
78
+ - In the Signal theme the floating frame draws the control-room dot grid on
79
+ its ground, behind the card. Nothing to add: the preset makes it.
80
+
81
+ ## What it owns
82
+
83
+ It does these by itself. Write none of them around it.
84
+
85
+ | It does | So you never write | Refused when written by hand |
86
+ |---|---|---|
87
+ | Scrolls the page's content column, in a ScrollArea | `overflow-auto` on the page or on `main` | |
78
88
 
79
89
  ## Props
80
90
 
@@ -36,4 +36,20 @@ describe('AppShell', () => {
36
36
  expect(main.className).toContain('overflow-hidden')
37
37
  expect(main.closest('[style*="overflow: scroll"]')).toBeNull()
38
38
  })
39
+
40
+ it('carries the Signal canvas on the floating frame, and only there', () => {
41
+ const { container, unmount } = render(
42
+ <AppShell variant="floating" nav={<nav>Nav</nav>}>
43
+ <p>Page</p>
44
+ </AppShell>,
45
+ )
46
+ expect((container.firstElementChild as HTMLElement).className).toContain('signal-canvas')
47
+ unmount()
48
+ const solid = render(
49
+ <AppShell nav={<nav>Nav</nav>}>
50
+ <p>Page</p>
51
+ </AppShell>,
52
+ )
53
+ expect(solid.container.querySelector('.signal-canvas')).toBeNull()
54
+ })
39
55
  })
package/src/AppShell.tsx CHANGED
@@ -45,7 +45,10 @@ export function AppShell({ variant = 'solid', menu, logo, search, identity, bann
45
45
 
46
46
  if (variant === 'floating') {
47
47
  return (
48
- <div className="relative h-full min-h-0 overflow-hidden bg-bg-base">
48
+ // `signal-canvas`: in the Signal theme, the preset draws the control-room dot
49
+ // grid behind everything in the frame (Peek's, moved here with the frame:
50
+ // UIG-14, Katerina, 19 September — the canvas is the theme's, not an app's).
51
+ <div className="signal-canvas relative h-full min-h-0 overflow-hidden bg-bg-base">
49
52
  {bar}
50
53
  <div className="flex h-full pb-4 pr-4 pt-[52px]">
51
54
  {nav}
@@ -57,6 +57,15 @@ import { AttachmentCard } from '@estiva-app/ui'
57
57
  on Base UI's `Button` like InputChip's ✕ — not an `IconButton`, the 24px
58
58
  square that fills on hover.
59
59
 
60
+ ## What it owns
61
+
62
+ It does these by itself. Write none of them around it.
63
+
64
+ | It does | So you never write | Refused when written by hand |
65
+ |---|---|---|
66
+ | Stops the clicks on its picture, its download and its remove control, so they never also click the card or row around it | `stopPropagation` | |
67
+ | Shows the full name on hover when the name is cut off | a tooltip of your own | |
68
+
60
69
  ## Props
61
70
 
62
71
  <Controls of={AttachmentCardStories.Document} />
@@ -169,4 +169,14 @@ describe('AttachmentCard', () => {
169
169
  expect(onRemove).toHaveBeenCalledTimes(1)
170
170
  expect(outside).not.toHaveBeenCalled()
171
171
  })
172
+
173
+ it('the ✕ shows when the keyboard reaches it, not only on hover (C4)', async () => {
174
+ render(<AttachmentCard pending name="a.png" size={1} onRemove={() => {}} />)
175
+ const remove = screen.getByRole('button', { name: 'Remove a.png' })
176
+ // Hidden at rest; each of these reveals it. jsdom draws nothing, so the classes are the contract.
177
+ expect(remove.className).toContain('opacity-0')
178
+ for (const reveal of ['group-hover:opacity-100', 'group-focus-within:opacity-100', 'focus-visible:opacity-100']) expect(remove.className).toContain(reveal)
179
+ await userEvent.tab()
180
+ expect(document.activeElement).toBe(remove)
181
+ })
172
182
  })
@@ -20,7 +20,7 @@ import {
20
20
  import { Card } from './Card'
21
21
  import { IconButton } from './IconButton'
22
22
  import { Link } from './Link'
23
- import { WithTooltip } from './Tooltip'
23
+ import { WithTooltip, type WithTooltipProps } from './Tooltip'
24
24
  import { cn } from './cn'
25
25
 
26
26
  /**
@@ -98,7 +98,7 @@ function TypeIcon({ name }: { name: string }) {
98
98
  * the fallback face can fit and then not, in a line whose box never changes. The wrapper is `flex-col` so the
99
99
  * text stretches across it, as wide as it is without one.
100
100
  */
101
- function Truncating({ text, hint, className, wrapperClassName }: { text: string; hint?: string; className: string; wrapperClassName?: string }) {
101
+ function Truncating({ text, hint, className, wrapperClassName, placement }: { text: string; hint?: string; className: string; wrapperClassName?: string; placement?: WithTooltipProps['placement'] }) {
102
102
  const ref = useRef<HTMLSpanElement>(null)
103
103
  const [cut, setCut] = useState(false)
104
104
  useLayoutEffect(() => {
@@ -127,7 +127,7 @@ function Truncating({ text, hint, className, wrapperClassName }: { text: string;
127
127
  )
128
128
  if (!hint && !cut) return line
129
129
  return (
130
- <WithTooltip label={hint ?? text} wrapperClassName={cn('min-w-0 flex-col', wrapperClassName)}>
130
+ <WithTooltip label={hint ?? text} placement={placement} wrapperClassName={cn('min-w-0 flex-col', wrapperClassName)}>
131
131
  {line}
132
132
  </WithTooltip>
133
133
  )
@@ -206,7 +206,8 @@ export function AttachmentCard({
206
206
  )}
207
207
  </div>
208
208
  <div className="flex flex-col gap-[1px] min-w-0">
209
- <Truncating text={name} className={NAME_CLASSES} />
209
+ {/* Below, not above: above, a cut name's tooltip covered the ✕ on the corner (UIG-14, C4). */}
210
+ <Truncating text={name} className={NAME_CLASSES} placement={onRemove ? 'bottom' : undefined} />
210
211
  <Truncating
211
212
  text={(failed || warning ? note : state === 'uploading' ? (note ?? 'Uploading…') : (note ?? sizeText)) ?? ''}
212
213
  hint={noteHint}
@@ -219,7 +220,9 @@ export function AttachmentCard({
219
220
  <BaseButton
220
221
  type="button"
221
222
  aria-label={`Remove ${name}`}
222
- className="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-bg-elevated border border-border-strong flex items-center justify-center text-text-secondary hover:text-text-primary opacity-0 group-hover:opacity-100 transition-opacity"
223
+ // Shows with the card's hover and with keyboard focus, as Download does: hover-only left it invisible to
224
+ // the keyboard that had reached it (UIG-14, C4, Katerina 19 September).
225
+ className="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-bg-elevated border border-border-strong flex items-center justify-center text-text-secondary hover:text-text-primary opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 transition-opacity"
223
226
  onClick={(event) => {
224
227
  event.stopPropagation()
225
228
  onRemove()
package/src/Avatar.mdx CHANGED
@@ -13,13 +13,15 @@ theirs, or — when there is no name to take initials from — a silhouette.
13
13
  ## When
14
14
 
15
15
  - Anywhere a person appears at a glance: a row, a card, a picker, a byline.
16
- - The scale is 16 · 24 · 32 · 36 (default) pick from it rather than
17
- inventing sizes.
16
+ - The scale is 16 · 18 · 20 · 24 · 32 · 36 (default): 18 in a group's reply
17
+ row, 20 in a **Person**. Pick from it rather than inventing sizes.
18
18
 
19
19
  ## When not
20
20
 
21
21
  - Several people at once → **AvatarGroup**.
22
- - The face beside the name → **Person**.
22
+ - The face beside a name, with nothing else on the line → **Person**. A byline
23
+ that sets the name its own way — bold, with a time — is an Avatar beside its
24
+ own text.
23
25
  - The face as a button that opens a menu → **PersonTrigger**.
24
26
 
25
27
  ## How
@@ -66,6 +68,14 @@ a stack of members — and the tile becomes one image with one name.
66
68
 
67
69
  <Canvas of={AvatarStories.Palette} />
68
70
 
71
+ ## What it owns
72
+
73
+ It does these by itself. Write none of them around it.
74
+
75
+ | It does | So you never write | Refused when written by hand |
76
+ |---|---|---|
77
+ | Shows initials from the name when there is no picture, or when it fails to load | an `onError`, or initials of your own | |
78
+
69
79
  ## Props
70
80
 
71
81
  <Controls of={AvatarStories.Initials} />
@@ -34,6 +34,10 @@ import { AvatarGroup } from '@estiva-app/ui'
34
34
  - The ring rides on the Avatar itself (one box); don't wrap each face in a
35
35
  clipping window — a window minus a border crops the initials.
36
36
 
37
+ ## What it owns
38
+
39
+ Nothing. It only draws. Clicks and keys are yours.
40
+
37
41
  ## Props
38
42
 
39
43
  <Controls of={AvatarGroupStories.ThreeMembers} />
package/src/Banner.mdx CHANGED
@@ -12,8 +12,8 @@ line, one of four tones, gone when there is nothing to say.
12
12
 
13
13
  ## When
14
14
 
15
- - The outcome or state of the whole surface: "Public key copied.", the
16
- reason the last read failed, a read-only notice.
15
+ - The state of the whole surface: the reason the last read failed, a read-only
16
+ notice.
17
17
  - **`ok`** confirms, **`info`** informs, **`warning`** cautions,
18
18
  **`error`** reports a failure — and only `error` announces itself
19
19
  (`role="alert"`); the others are polite (`role="status"`).
@@ -22,7 +22,8 @@ line, one of four tones, gone when there is nothing to say.
22
22
 
23
23
  ## When not
24
24
 
25
- - Feedback about one action, briefly → **Toast**.
25
+ - Feedback about one action, briefly → **Toast**. A failure is the exception:
26
+ it stays, in the Banner, until the person acts on it.
26
27
  - Something that floats over the page and must **not** fade → still
27
28
  **Toast**, with `durationMs: 0` and an action. A banner is the strip
28
29
  under the header at the app's full width; a floating pill is not one, even
@@ -61,6 +62,15 @@ import { Banner } from '@estiva-app/ui'
61
62
  is 40px rather than 36px with it, because the button is taller than the
62
63
  line of text; without it, nothing about the strip changes.
63
64
 
65
+ ## What it owns
66
+
67
+ It does these by itself. Write none of them around it.
68
+
69
+ | It does | So you never write | Refused when written by hand |
70
+ |---|---|---|
71
+ | Says what it is to assistive technology: an alert for an error, a status for the rest | `role="alert"` or `role="status"` | ✓ |
72
+ | Draws its own ✕ when you pass `onDismiss`, and its one small action when you pass `action` | a close button or an action button of your own | |
73
+
64
74
  ## Props
65
75
 
66
76
  <Controls of={BannerStories.Ok} />
@@ -56,6 +56,16 @@ import { Breadcrumb } from '@estiva-app/ui'
56
56
  `min-w-0` container so it can truncate at all.
57
57
  - The separators are `aria-hidden` — the trail reads as its places.
58
58
 
59
+ ## What it owns
60
+
61
+ It does these by itself. Write none of them around it.
62
+
63
+ | It does | So you never write | Refused when written by hand |
64
+ |---|---|---|
65
+ | Shortens a trail that does not fit, and shows a cut crumb in full on hover | truncation code, or a tooltip | |
66
+ | Names itself as a navigation, and marks the last crumb as the current page | `<nav>`, `aria-label` or `aria-current` | |
67
+ | Hides the separators from screen readers, so the trail reads as its places | `aria-hidden` on each separator | |
68
+
59
69
  ## Props
60
70
 
61
71
  <Controls of={BreadcrumbStories.OnAnItem} />
package/src/Button.mdx CHANGED
@@ -27,12 +27,13 @@ The push-button for an action: a verb, clicked once — "Create", "Save",
27
27
  ## When not
28
28
 
29
29
  - An icon with no label → **IconButton**.
30
- - Going somewhere rather than doing something → **Tabs**, **Breadcrumb**,
31
- or a plain link.
30
+ - Going somewhere rather than doing something → **Link**; **Tabs** or
31
+ **Breadcrumb** when it is a view or a trail.
32
32
  - Choosing a value → **Select**. Acting from a popover list → **MenuItem**.
33
33
  - A control the person may never be allowed to use is **absent, not
34
34
  disabled**. `disabled` is for *momentarily* unavailable — and it says why,
35
- with `disabledReason` ("Available after sign-in").
35
+ with `disabledReason` ("Available after sign-in"). A send button waiting for
36
+ its fields needs no reason.
36
37
 
37
38
  ## How
38
39
 
@@ -68,6 +69,16 @@ import { IconPlus } from '@tabler/icons-react'
68
69
  flex and grid, and position; a colour, a text size, a border, a corner or a
69
70
  shadow is refused by the lint (`estiva/no-restyled-part`, UIG-9).
70
71
 
72
+ ## What it owns
73
+
74
+ It does these by itself. Write none of them around it.
75
+
76
+ | It does | So you never write | Refused when written by hand |
77
+ |---|---|---|
78
+ | Says what it is to assistive technology: a button | `role="button"` | ✓ |
79
+ | Is reachable with Tab, also when it is disabled with a reason | a `tabIndex` | ✓ |
80
+ | Presses on Enter and Space, and does nothing while disabled | a key handler | |
81
+
71
82
  ## Keys
72
83
 
73
84
  | Key | Does |
package/src/Card.mdx CHANGED
@@ -76,6 +76,15 @@ import { Card } from '@estiva-app/ui'
76
76
  shadow is refused by the lint (`estiva/no-restyled-part`, UIG-9). What is inside the card is yours: its padding and its words go on
77
77
  your own element inside.
78
78
 
79
+ ## What it owns
80
+
81
+ It does these by itself. Write none of them around it.
82
+
83
+ | It does | So you never write | Refused when written by hand |
84
+ |---|---|---|
85
+ | With `href`, becomes a link: Link's anchor, with a real address | an anchor around the card | |
86
+ | Without `href`, shows it can be clicked when you pass `onClick` | `cursor-pointer` | |
87
+
79
88
  ## Props
80
89
 
81
90
  <Controls of={CardStories.Surface} />
package/src/Checkbox.mdx CHANGED
@@ -79,6 +79,16 @@ import { Checkbox } from '@estiva-app/ui'
79
79
  - It does not move when it toggles: the tick is always in the box, hidden
80
80
  when unchecked, so both states hang on a line of text the same way.
81
81
 
82
+ ## What it owns
83
+
84
+ It does these by itself. Write none of them around it.
85
+
86
+ | It does | So you never write | Refused when written by hand |
87
+ |---|---|---|
88
+ | Says what it is to assistive technology: a checkbox, ticked or not | `role="checkbox"` or `aria-checked` | ✓ |
89
+ | Toggles on Space and on a click; Enter is left to the form | a click or key handler | |
90
+ | Is one Tab stop, in a `row` too | a `tabIndex` on the row | |
91
+
82
92
  ## Keys
83
93
 
84
94
  | Key | Does |
package/src/Chip.mdx CHANGED
@@ -38,6 +38,10 @@ import { Chip } from '@estiva-app/ui'
38
38
  it never rides the text baseline.
39
39
  - A count alone works too — `label="3"`, no icon.
40
40
 
41
+ ## What it owns
42
+
43
+ Nothing. It only draws. Clicks and keys are yours.
44
+
41
45
  ## Props
42
46
 
43
47
  <Controls of={ChipStories.Neutral} />
package/src/ChipInput.mdx CHANGED
@@ -74,6 +74,18 @@ import { ChipInput } from '@estiva-app/ui'
74
74
 
75
75
  <Canvas of={ChipInputStories.ALongLabelCut} />
76
76
 
77
+ ## What it owns
78
+
79
+ It does these by itself. Write none of them around it.
80
+
81
+ | It does | So you never write | Refused when written by hand |
82
+ |---|---|---|
83
+ | Floats on top of the page: its suggestions | a portal, or a `z-index` to win | ✓ |
84
+ | Moves through its items with the arrow keys, through the suggestions | an arrow-key handler | ✓ |
85
+ | Says what it is to assistive technology: a combobox and its options | `role="combobox"`, `listbox` or `option` | ✓ |
86
+ | Filters as you type; Enter adds the highlighted entry, Backspace on an empty query removes the last chip | a filter, or key handlers | |
87
+ | Escape clears the query first, and only then lets the surface around it act | an Escape handler | |
88
+
77
89
  ## Keys
78
90
 
79
91
  | Input | What happens |
@@ -60,6 +60,15 @@ import { CollapsibleSection, NavItem } from '@estiva-app/ui'
60
60
  with `open`.
61
61
  - A browser that blocks storage still gets a working section; it forgets.
62
62
 
63
+ ## What it owns
64
+
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
+ | Opens and closes from its title, on a click, Enter or Space | open state, or a key handler | |
70
+ | Tells assistive technology whether it is open | `aria-expanded` | |
71
+
63
72
  ## Keys
64
73
 
65
74
  | Key | Does |
@@ -49,8 +49,8 @@ import { CommandPalette, CommandPaletteSearch } from '@estiva-app/ui'
49
49
  onQueryChange={setQuery}
50
50
  placeholder="Search, or choose a place"
51
51
  groups={[
52
- { label: 'Recent', rows: [{ id: 'item-one', label: 'Item one', icon: <IconSquareRounded size={16} stroke={1.5} />, onSelect: open, onForget: forget }] },
53
- { label: 'Places', rows: [{ id: 'place-one', label: 'Place one', icon, onSelect: goIn, onGoIn: goIn }] },
52
+ { label: 'Recent', rows: [{ id: 'item-one', label: 'Item one', icon: <IconSquareRounded size={16} stroke={1.5} />, onSelect: openItem, onForget: forget }] },
53
+ { label: 'Places', rows: [{ id: 'place-one', label: 'Place one', icon: <IconSquareRounded size={16} stroke={1.5} />, onSelect: goIn, onGoIn: goIn }] },
54
54
  ]}
55
55
  />
56
56
  </CommandPalette>
@@ -102,6 +102,18 @@ import { CommandPalette, CommandPaletteSearch } from '@estiva-app/ui'
102
102
  - **Focus never falls out of a level.** When the element that had it goes — a
103
103
  row picked, a level swapped — focus goes to the level's first control.
104
104
 
105
+ ## What it owns
106
+
107
+ It does these by itself. Write none of them around it.
108
+
109
+ | It does | So you never write | Refused when written by hand |
110
+ |---|---|---|
111
+ | Floats on top of the page | a portal, or a `z-index` to win | ✓ |
112
+ | Holds focus inside while open, and gives it back to whatever opened it | focus code, or a Tab handler | ✓ |
113
+ | Moves through its items with the arrow keys, through the rows, stopping at the ends | an arrow-key handler | ✓ |
114
+ | Says what it is to assistive technology: a list of options you search | `role="combobox"`, `listbox` or `option` | ✓ |
115
+ | Closes on Esc or a click outside; Esc closes an open list first | close handlers | |
116
+
105
117
  ## Keys
106
118
 
107
119
  | Where | Input | What happens |
@@ -53,6 +53,17 @@ import { ConfirmDialog } from '@estiva-app/ui'
53
53
  - Focus is trapped in the card and returns to whatever opened it, as for
54
54
  every dialog since stage 3.
55
55
 
56
+ ## What it owns
57
+
58
+ It does these by itself. Write none of them around it.
59
+
60
+ | It does | So you never write | Refused when written by hand |
61
+ |---|---|---|
62
+ | Floats on top of the page | a portal, or a `z-index` to win | ✓ |
63
+ | Says what it is to assistive technology: a dialog that must be answered | `role="alertdialog"` | ✓ |
64
+ | Keeps focus inside while open, as DialogShell does, and gives it back to whatever opened it | focus code | |
65
+ | Ignores a press outside, because the question has to be answered; Escape cancels | a backdrop handler | |
66
+
56
67
  ## Keys
57
68
 
58
69
  Different from `DialogShell`'s in one row, and it is the row that matters.
@@ -51,6 +51,10 @@ import { ContainerHeader, IconButton } from '@estiva-app/ui'
51
51
  - Only offer a button that does something: a button with nothing behind it is
52
52
  a promise the column cannot keep.
53
53
 
54
+ ## What it owns
55
+
56
+ Nothing. It only draws. Clicks and keys are yours.
57
+
54
58
  ## Props
55
59
 
56
60
  <Controls of={ContainerHeaderStories.Default} />
@@ -28,6 +28,7 @@ footer for the buttons. A dialog is just what goes in the three slots.
28
28
  dialog rebuilt by hand, which is the one thing this bullet exists to stop.
29
29
  Its canvases and its nine tests are on that page.
30
30
  - Telling someone what happened → **Toast**.
31
+ - Searching for something and acting on it → **CommandPalette**.
31
32
 
32
33
  ## How
33
34
 
@@ -73,6 +74,19 @@ import { DialogShell, Button } from '@estiva-app/ui'
73
74
  stops closing it (Katerina, D20). Escape and the ✕ still do.
74
75
  `ConfirmDialog` sets it; a form or a roster should not.
75
76
 
77
+ ## What it owns
78
+
79
+ It does these by itself. Write none of them around it.
80
+
81
+ | It does | So you never write | Refused when written by hand |
82
+ |---|---|---|
83
+ | Floats on top of the page | a portal, or a `z-index` to win | ✓ |
84
+ | Closes on a press outside, except with `alert` | a click listener on the page | ✓ |
85
+ | Takes its keys by itself: Escape closes it | a key listener on the page | ✓ |
86
+ | Holds focus inside while open, and gives it back to whatever opened it | focus code, or a Tab handler | ✓ |
87
+ | Stops the page behind it scrolling | `overflow: hidden` on the page | ✓ |
88
+ | Says what it is to assistive technology: a dialog | `role="dialog"` | ✓ |
89
+
76
90
  ## Keys
77
91
 
78
92
  | Input | What happens |
package/src/Divider.mdx CHANGED
@@ -16,7 +16,7 @@ orientation for assistive tech.
16
16
 
17
17
  - Between groups in a **Menu** (`className="my-1"` gives it its breathing
18
18
  room there).
19
- - Between sections of a panel or rail.
19
+ - Between sections of a panel, a rail or a card.
20
20
  - Vertical: between clusters in a toolbar or a dense row.
21
21
 
22
22
  <Canvas of={DividerStories.Vertical} />
@@ -49,6 +49,14 @@ import { Divider } from '@estiva-app/ui'
49
49
  - **Inside a `Menu` or a `Popover` it has no inset**: the hairline runs the
50
50
  width of the rows. The panel tells it so; nothing is passed in.
51
51
 
52
+ ## What it owns
53
+
54
+ It does these by itself. Write none of them around it.
55
+
56
+ | It does | So you never write | Refused when written by hand |
57
+ |---|---|---|
58
+ | Says what it is to assistive technology: a separator | `role="separator"` | ✓ |
59
+
52
60
  ## Props
53
61
 
54
62
  <Controls of={DividerStories.Default} />
@@ -23,6 +23,8 @@ edit is never silently lost.
23
23
  - A field in a form or dialog → **TextInput** / **Textarea** in a
24
24
  **Field**.
25
25
  - Composing something new → the field, not an edit-in-place of nothing.
26
+ - An edit that publishes something, like a message → **Textarea** with Save
27
+ and Cancel. EditableText saves when you leave it.
26
28
 
27
29
  ## How
28
30
 
@@ -55,6 +57,15 @@ import { EditableText } from '@estiva-app/ui'
55
57
  the page's font). `className` places it — `-mx-2` to line its text up with
56
58
  a header's inset — and a size passed there is refused by the lint (UIG-9).
57
59
 
60
+ ## What it owns
61
+
62
+ It does these by itself. Write none of them around it.
63
+
64
+ | It does | So you never write | Refused when written by hand |
65
+ |---|---|---|
66
+ | Opens its field on a click, with the value selected | an edit mode of your own | |
67
+ | Enter and leaving the field save; Escape cancels and puts the value back (`multiline`: Shift+Enter is a new line) | key or blur handlers | |
68
+
58
69
  ## Keys
59
70
 
60
71
  | Input | What happens |