@estiva-app/ui 0.22.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 (173) hide show
  1. package/README.md +21 -0
  2. package/dist/AppShell.d.ts.map +1 -1
  3. package/dist/AttachmentCard.d.ts.map +1 -1
  4. package/dist/Banner.d.ts +20 -2
  5. package/dist/Banner.d.ts.map +1 -1
  6. package/dist/ContainerHeader.d.ts +25 -0
  7. package/dist/ContainerHeader.d.ts.map +1 -0
  8. package/dist/EmptyState.d.ts +10 -1
  9. package/dist/EmptyState.d.ts.map +1 -1
  10. package/dist/Form.d.ts.map +1 -1
  11. package/dist/IconButton.d.ts +9 -1
  12. package/dist/IconButton.d.ts.map +1 -1
  13. package/dist/PreviewCard.d.ts +9 -2
  14. package/dist/PreviewCard.d.ts.map +1 -1
  15. package/dist/SectionHeader.d.ts +7 -1
  16. package/dist/SectionHeader.d.ts.map +1 -1
  17. package/dist/Select.d.ts +11 -3
  18. package/dist/Select.d.ts.map +1 -1
  19. package/dist/Tooltip.d.ts +13 -1
  20. package/dist/Tooltip.d.ts.map +1 -1
  21. package/dist/eslint/index.js +2 -1
  22. package/dist/eslint/index.js.map +2 -2
  23. package/dist/eslint/no-restyled-part.d.ts.map +1 -1
  24. package/dist/gates/app-checks.d.ts.map +1 -1
  25. package/dist/gates/{chunk-ZGJ2J5NU.js → chunk-EA33NP5B.js} +275 -11
  26. package/dist/gates/chunk-EA33NP5B.js.map +7 -0
  27. package/dist/gates/{chunk-AUXD4GCY.js → chunk-GTQZEHPC.js} +15 -1
  28. package/dist/gates/chunk-GTQZEHPC.js.map +7 -0
  29. package/dist/gates/cli.js +1 -1
  30. package/dist/gates/create-app.d.ts +2 -0
  31. package/dist/gates/create-app.d.ts.map +1 -1
  32. package/dist/gates/create-app.js +1 -1
  33. package/dist/gates/index.js +25 -3
  34. package/dist/gates/index.js.map +2 -2
  35. package/dist/gates/status.d.ts +2 -0
  36. package/dist/gates/status.d.ts.map +1 -1
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +196 -156
  40. package/dist/index.js.map +4 -4
  41. package/dist/registry/app-DQI6WLHR.js +10 -0
  42. package/dist/registry/app.d.ts +21 -0
  43. package/dist/registry/app.d.ts.map +1 -0
  44. package/dist/registry/build-LGFCCOLR.js +27 -0
  45. package/dist/registry/build-LGFCCOLR.js.map +7 -0
  46. package/dist/registry/build.d.ts +68 -2
  47. package/dist/registry/build.d.ts.map +1 -1
  48. package/dist/registry/{chunk-MRSBS5OP.js → chunk-E4JNV7PC.js} +68 -12
  49. package/dist/registry/chunk-E4JNV7PC.js.map +7 -0
  50. package/dist/registry/{chunk-QDYGB3QN.js → chunk-NJN4MQAM.js} +60 -15
  51. package/dist/registry/chunk-NJN4MQAM.js.map +7 -0
  52. package/dist/registry/{chunk-IJNCYVH4.js → chunk-W2B2G7OE.js} +34 -9
  53. package/dist/registry/chunk-W2B2G7OE.js.map +7 -0
  54. package/dist/registry/chunk-WMFF3MPP.js +648 -0
  55. package/dist/registry/chunk-WMFF3MPP.js.map +7 -0
  56. package/dist/registry/cli.js +81 -26
  57. package/dist/registry/cli.js.map +3 -3
  58. package/dist/registry/find.d.ts +12 -1
  59. package/dist/registry/find.d.ts.map +1 -1
  60. package/dist/registry/index.d.ts +5 -2
  61. package/dist/registry/index.d.ts.map +1 -1
  62. package/dist/registry/index.js +11 -3
  63. package/dist/registry/schema.d.ts +83 -6
  64. package/dist/registry/schema.d.ts.map +1 -1
  65. package/package.json +2 -2
  66. package/registry.json +283 -95
  67. package/src/AppShell.mdx +10 -0
  68. package/src/AppShell.test.tsx +16 -0
  69. package/src/AppShell.tsx +4 -1
  70. package/src/AttachmentCard.mdx +9 -0
  71. package/src/AttachmentCard.test.tsx +10 -0
  72. package/src/AttachmentCard.tsx +8 -5
  73. package/src/Avatar.mdx +13 -3
  74. package/src/AvatarGroup.mdx +4 -0
  75. package/src/Banner.mdx +30 -7
  76. package/src/Banner.stories.tsx +22 -0
  77. package/src/Banner.test.tsx +35 -0
  78. package/src/Banner.tsx +34 -9
  79. package/src/Breadcrumb.mdx +10 -0
  80. package/src/Button.mdx +14 -3
  81. package/src/Card.mdx +9 -0
  82. package/src/Checkbox.mdx +10 -0
  83. package/src/Chip.mdx +4 -0
  84. package/src/ChipInput.mdx +12 -0
  85. package/src/CollapsibleSection.mdx +9 -0
  86. package/src/CommandPalette.mdx +14 -2
  87. package/src/ConfirmDialog.mdx +11 -0
  88. package/src/ContainerHeader.mdx +60 -0
  89. package/src/ContainerHeader.stories.tsx +62 -0
  90. package/src/ContainerHeader.test.tsx +47 -0
  91. package/src/ContainerHeader.tsx +45 -0
  92. package/src/DialogShell.mdx +14 -0
  93. package/src/Divider.mdx +9 -1
  94. package/src/EditableText.mdx +11 -0
  95. package/src/EmptyState.mdx +27 -4
  96. package/src/EmptyState.stories.tsx +6 -1
  97. package/src/EmptyState.test.tsx +18 -1
  98. package/src/EmptyState.tsx +14 -1
  99. package/src/Field.mdx +10 -1
  100. package/src/FieldLine.mdx +9 -1
  101. package/src/FilePicker.mdx +8 -0
  102. package/src/Form.mdx +10 -0
  103. package/src/Form.test.tsx +26 -0
  104. package/src/Form.tsx +7 -0
  105. package/src/IconButton.mdx +16 -2
  106. package/src/IconButton.stories.tsx +3 -0
  107. package/src/IconButton.test.tsx +25 -0
  108. package/src/IconButton.tsx +61 -46
  109. package/src/IdentityMenu.mdx +9 -0
  110. package/src/InlineChip.mdx +8 -0
  111. package/src/Kbd.mdx +4 -0
  112. package/src/Link.mdx +10 -0
  113. package/src/Menu.mdx +16 -1
  114. package/src/MenuItem.mdx +12 -2
  115. package/src/NavItem.mdx +8 -0
  116. package/src/Person.mdx +4 -0
  117. package/src/PersonTrigger.mdx +9 -0
  118. package/src/Popover.mdx +21 -0
  119. package/src/PreviewCard.mdx +19 -4
  120. package/src/PreviewCard.tsx +11 -4
  121. package/src/ProgressBar.mdx +8 -0
  122. package/src/Property.mdx +4 -0
  123. package/src/Rail.mdx +9 -1
  124. package/src/RailItem.mdx +8 -0
  125. package/src/Reaction.mdx +9 -0
  126. package/src/ReactionPicker.mdx +8 -0
  127. package/src/ScrollArea.mdx +13 -2
  128. package/src/SearchInput.mdx +9 -0
  129. package/src/SectionHeader.mdx +13 -0
  130. package/src/SectionHeader.stories.tsx +9 -0
  131. package/src/SectionHeader.test.tsx +9 -0
  132. package/src/SectionHeader.tsx +8 -2
  133. package/src/SectionLabel.mdx +4 -0
  134. package/src/Select.mdx +21 -2
  135. package/src/Select.stories.tsx +4 -1
  136. package/src/Select.test.tsx +17 -0
  137. package/src/Select.tsx +34 -13
  138. package/src/Sidebar.mdx +8 -0
  139. package/src/Skeleton.mdx +4 -0
  140. package/src/Tabs.mdx +12 -1
  141. package/src/TextInput.mdx +8 -0
  142. package/src/Textarea.mdx +8 -0
  143. package/src/Toast.mdx +12 -1
  144. package/src/Toolbar.mdx +11 -0
  145. package/src/Tooltip.mdx +26 -2
  146. package/src/Tooltip.stories.tsx +26 -0
  147. package/src/Tooltip.test.tsx +29 -0
  148. package/src/Tooltip.tsx +17 -4
  149. package/src/TopBar.mdx +4 -0
  150. package/src/eslint/no-rebuilt-behaviour.ts +1 -1
  151. package/src/eslint/no-restyled-part.ts +1 -0
  152. package/src/gates/app-checks.ts +23 -1
  153. package/src/gates/create-app.test.ts +77 -6
  154. package/src/gates/create-app.ts +288 -15
  155. package/src/gates/status.ts +16 -0
  156. package/src/index.ts +1 -0
  157. package/src/pages.test.ts +142 -0
  158. package/src/registry/app.test.ts +562 -0
  159. package/src/registry/app.ts +854 -0
  160. package/src/registry/build.ts +114 -25
  161. package/src/registry/cli.ts +105 -42
  162. package/src/registry/find.ts +45 -8
  163. package/src/registry/index.ts +8 -1
  164. package/src/registry/registry.test.ts +5 -5
  165. package/src/registry/schema.ts +163 -12
  166. package/tailwind-preset.js +22 -1
  167. package/dist/gates/chunk-AUXD4GCY.js.map +0 -7
  168. package/dist/gates/chunk-ZGJ2J5NU.js.map +0 -7
  169. package/dist/registry/build-GOVLABI6.js +0 -13
  170. package/dist/registry/chunk-IJNCYVH4.js.map +0 -7
  171. package/dist/registry/chunk-MRSBS5OP.js.map +0 -7
  172. package/dist/registry/chunk-QDYGB3QN.js.map +0 -7
  173. /package/dist/registry/{build-GOVLABI6.js.map → app-DQI6WLHR.js.map} +0 -0
@@ -37,6 +37,14 @@ export interface IconButtonProps extends ComponentPropsWithRef<'button'> {
37
37
  /** Why the action cannot succeed right now. Disables the button, keeps it
38
38
  * reachable by keyboard, and shows the reason as the tooltip. */
39
39
  disabledReason?: string
40
+ /**
41
+ * A link that looks like an icon button: an anchor, so the address stays
42
+ * real for a modified click or a new tab, as `Link` keeps it. A router app
43
+ * passes `onClick`, prevents the default and navigates. A link cannot be
44
+ * disabled, so while it would be — `disabled`, `disabledReason`, a busy
45
+ * Form — it is the button (UIG-14, Katerina, 18 September).
46
+ */
47
+ href?: string
40
48
  /** The icon: 16px, stroke 1.5. */
41
49
  children: ReactNode
42
50
  }
@@ -53,56 +61,63 @@ export function IconButton({
53
61
  tooltipShortcut,
54
62
  tooltipPlacement,
55
63
  type = 'button',
64
+ href,
56
65
  ...props
57
66
  }: IconButtonProps) {
58
67
  // Inside a busy Form: switched off, and looking it (formBusy.ts).
59
68
  const formBusy = useFormBusy()
60
- const button = (
61
- <BaseButton
62
- type={type}
63
- disabled={disabled || !!disabledReason || formBusy}
64
- focusableWhenDisabled={!!disabledReason}
65
- className={(state) =>
66
- cn(
67
- // `shrink-0` stops a flex parent squashing the button; `self-center`
68
- // stops one stretching it. Two different failures, and both have
69
- // happened here: a row with no `items-*` drew this 24 wide and 228
70
- // tall in Peek's TopicMoreMenu story (Katerina, 2026-09-11). A button
71
- // is the size of its icon and its padding, whatever box it lands in.
72
- //
73
- // This is the one of the two that needs it: an IconButton states no
74
- // height, so its cross size is `auto` and a stretching parent takes
75
- // it. Measured in the same 260px row on 2026-09-12: 24px with this
76
- // class, 226px without. `Button` states `h-8`/`h-6`, so it cannot be
77
- // stretched and carries no `self-center` see the note there.
78
- 'flex items-center justify-center p-1 rounded-lg transition-colors shrink-0 self-center cursor-pointer',
79
- !state.disabled && variant === 'primary' && 'bg-accent-primary hover:bg-accent-hover text-text-inverse',
80
- !state.disabled && (variant === 'muted' || variant === 'resolve') && 'text-text-secondary hover:bg-bg-hover hover:text-text-primary',
81
- !state.disabled && variant === 'outlined' && 'border border-border-default hover:bg-bg-hover text-text-secondary',
82
- !state.disabled && variant === 'current' && 'hover:bg-bg-hover',
83
- state.disabled && variant === 'primary' && 'bg-bg-disabled text-text-disabled',
84
- state.disabled && (variant === 'muted' || variant === 'resolve') && 'text-text-disabled',
85
- state.disabled && variant === 'outlined' && 'border border-border-default text-text-disabled',
86
- // Always, disabled too: the colour of where it sits, and Resolve's green.
87
- variant === 'current' && 'text-current',
88
- variant === 'resolve' && 'signal:hover:bg-success-muted signal:hover:text-success-default',
89
- pressed && 'bg-bg-active text-text-primary',
90
- glow && 'signal:shadow-glow-accent',
91
- // `pointer-events-none` only where the button is truly out of reach:
92
- // with a `disabledReason` the button IS the tooltip's trigger, and a
93
- // trigger the pointer cannot land on never opens one. Base UI already
94
- // swallows the click.
95
- state.disabled && !disabledReason && 'pointer-events-none',
96
- state.disabled && 'cursor-not-allowed',
97
- className,
98
- )
99
- }
100
- aria-pressed={pressed}
101
- {...props}
102
- >
103
- {children}
104
- </BaseButton>
105
- )
69
+ const unusable = disabled || !!disabledReason || formBusy
70
+ const look = (off: boolean) =>
71
+ cn(
72
+ // `shrink-0` stops a flex parent squashing the button; `self-center`
73
+ // stops one stretching it. Two different failures, and both have
74
+ // happened here: a row with no `items-*` drew this 24 wide and 228
75
+ // tall in Peek's TopicMoreMenu story (Katerina, 2026-09-11). A button
76
+ // is the size of its icon and its padding, whatever box it lands in.
77
+ //
78
+ // This is the one of the two that needs it: an IconButton states no
79
+ // height, so its cross size is `auto` and a stretching parent takes
80
+ // it. Measured in the same 260px row on 2026-09-12: 24px with this
81
+ // class, 226px without. `Button` states `h-8`/`h-6`, so it cannot be
82
+ // stretched and carries no `self-center` see the note there.
83
+ 'flex items-center justify-center p-1 rounded-lg transition-colors shrink-0 self-center cursor-pointer',
84
+ !off && variant === 'primary' && 'bg-accent-primary hover:bg-accent-hover text-text-inverse',
85
+ !off && (variant === 'muted' || variant === 'resolve') && 'text-text-secondary hover:bg-bg-hover hover:text-text-primary',
86
+ !off && variant === 'outlined' && 'border border-border-default hover:bg-bg-hover text-text-secondary',
87
+ !off && variant === 'current' && 'hover:bg-bg-hover',
88
+ off && variant === 'primary' && 'bg-bg-disabled text-text-disabled',
89
+ off && (variant === 'muted' || variant === 'resolve') && 'text-text-disabled',
90
+ off && variant === 'outlined' && 'border border-border-default text-text-disabled',
91
+ // Always, disabled too: the colour of where it sits, and Resolve's green.
92
+ variant === 'current' && 'text-current',
93
+ variant === 'resolve' && 'signal:hover:bg-success-muted signal:hover:text-success-default',
94
+ pressed && 'bg-bg-active text-text-primary',
95
+ glow && 'signal:shadow-glow-accent',
96
+ // `pointer-events-none` only where the button is truly out of reach:
97
+ // with a `disabledReason` the button IS the tooltip's trigger, and a
98
+ // trigger the pointer cannot land on never opens one. Base UI already
99
+ // swallows the click.
100
+ off && !disabledReason && 'pointer-events-none',
101
+ off && 'cursor-not-allowed',
102
+ className,
103
+ )
104
+ const button =
105
+ href && !unusable ? (
106
+ <a href={href} className={look(false)} {...(props as unknown as ComponentPropsWithRef<'a'>)}>
107
+ {children}
108
+ </a>
109
+ ) : (
110
+ <BaseButton
111
+ type={type}
112
+ disabled={unusable}
113
+ focusableWhenDisabled={!!disabledReason}
114
+ className={(state) => look(state.disabled)}
115
+ aria-pressed={pressed}
116
+ {...props}
117
+ >
118
+ {children}
119
+ </BaseButton>
120
+ )
106
121
 
107
122
  const label = disabledReason ?? tooltip
108
123
  if (label) {
@@ -62,6 +62,15 @@ import { IdentityMenu } from '@estiva-app/ui'
62
62
  row's full height, and the panel hung from the bottom of *that* — measured
63
63
  360px below the face, with a scrollbar it should not have had.
64
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
+ | Everything a Menu owns: it floats, stays attached, closes on a press outside or on Escape, and walks its rows with the arrow keys | a menu of your own around the face | |
72
+ | Gives focus back to the face when it closes | focus code | |
73
+
65
74
  ## Keys
66
75
 
67
76
  Since stage 4 the panel is a Base UI `Menu`, and **the arrow keys walk the
@@ -51,6 +51,14 @@ import { InlineChip } from '@estiva-app/ui'
51
51
  - A rich-text editor that renders chips from strings cannot use a component.
52
52
  `inlineChipClassName(tone)` gives it the same classes.
53
53
 
54
+ ## What it owns
55
+
56
+ It does these by itself. Write none of them around it.
57
+
58
+ | It does | So you never write | Refused when written by hand |
59
+ |---|---|---|
60
+ | With `href`, keeps a real address, so a modified click or a new tab still works | a `<span>` with a click handler | |
61
+
54
62
  ## Props
55
63
 
56
64
  <Controls of={InlineChipStories.Neutral} />
package/src/Kbd.mdx CHANGED
@@ -72,6 +72,10 @@ It renders what it is given and formats nothing. A modifier is called Cmd on
72
72
  Apple platforms and Ctrl elsewhere, and only the caller knows which it is
73
73
  looking at — so decide there and pass the finished label.
74
74
 
75
+ ## What it owns
76
+
77
+ Nothing. It only draws. Clicks and keys are yours.
78
+
75
79
  ## Props
76
80
 
77
81
  <Controls of={KbdStories.Default} />
package/src/Link.mdx CHANGED
@@ -28,6 +28,7 @@ the app decides how it navigates.
28
28
  - An action — something happens, nothing is navigated to → **Button**.
29
29
  - A row of a sidebar → **NavItem**. A tile of a rail → **RailItem**.
30
30
  - The trail back up → **Breadcrumb**.
31
+ - A link that looks like an icon button → **IconButton** with `href`.
31
32
  - A word standing for a person or a thing, drawn in a small box → **InlineChip**
32
33
  with `href`.
33
34
 
@@ -60,6 +61,15 @@ import { Link } from '@estiva-app/ui'
60
61
  sit, so put those on your own element around the link, not on the link —
61
62
  the lint refuses a look passed in `className` (UIG-9).
62
63
 
64
+ ## What it owns
65
+
66
+ It does these by itself. Write none of them around it.
67
+
68
+ | It does | So you never write | Refused when written by hand |
69
+ |---|---|---|
70
+ | Says what it is to assistive technology: a link | `role="link"` | ✓ |
71
+ | Is reachable with Tab | a `tabIndex` | ✓ |
72
+
63
73
  ## Props
64
74
 
65
75
  <Controls of={LinkStories.Text} />
package/src/Menu.mdx CHANGED
@@ -41,7 +41,7 @@ definition either way.
41
41
  - Picking one value for a form field → **Select**.
42
42
  - Rows that toggle and stay open → that is a listbox, not a menu; a Checkbox
43
43
  inside a `menuitem` is invalid. Build it as a list in a **DialogShell**.
44
- - A strip of icon actions → a toolbar of **IconButton**s.
44
+ - A strip of icon actions → **Toolbar**.
45
45
  - Who-you-are and where-you-are → **IdentityMenu**, already built on this.
46
46
 
47
47
  ## How
@@ -76,6 +76,21 @@ the trigger as "close" rather than as a press outside.
76
76
 
77
77
  <Canvas of={MenuStories.OnACard} />
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
+ | Floats on top of the page | a portal, or a `z-index` to win | ✓ |
86
+ | Stays attached to its anchor on scroll and resize | a scroll or resize listener, or placement arithmetic | ✓ |
87
+ | Closes on a press outside | a click listener on the page | ✓ |
88
+ | Takes its keys by itself: Enter and Space choose a row, Escape closes it | a key listener on the page | ✓ |
89
+ | Moves through its items with the arrow keys, and jumps to a row by its first letters | an arrow-key handler | ✓ |
90
+ | Says what it is to assistive technology: a menu, and each row a menu item | `role="menu"` or `role="menuitem"` | ✓ |
91
+ | Keeps its own open state, and closes when a row is chosen | `useState` for open | |
92
+ | Gives focus back to the trigger when it closes | focus code | |
93
+
79
94
  ## Keys
80
95
 
81
96
  | Input | What happens |
package/src/MenuItem.mdx CHANGED
@@ -25,8 +25,9 @@ other two), `destructive` in the error colour, `selected` as the fill.
25
25
 
26
26
  ## When not
27
27
 
28
- - Outside a menu it renders `role="menuitem"` and belongs inside a
29
- `role="menu"` container.
28
+ - A single action that is not a row in a list **Button**. Outside a
29
+ **Menu**, MenuItem is a plain button with a row's look, so a row-shaped
30
+ action at the top of a list — "Add members" — is fine.
30
31
  - Picking a form value → **Select**'s own options.
31
32
  - A row that toggles a checkbox → not a menu at all; see the Menu page.
32
33
 
@@ -50,6 +51,15 @@ import { IconPencil } from '@tabler/icons-react'
50
51
  - A destructive row's `leading` icon takes the error colour itself
51
52
  (`text-error-default`) — the row colours only its label.
52
53
 
54
+ ## What it owns
55
+
56
+ It does these by itself. Write none of them around it.
57
+
58
+ | It does | So you never write | Refused when written by hand |
59
+ |---|---|---|
60
+ | Is a row of its Menu: the Menu highlights it, walks to it with the arrow keys, and closes when it is chosen | a highlight or a key handler on the row | |
61
+ | Opens its `submenu` with → and closes it with ← | a submenu of your own | |
62
+
53
63
  ## Props
54
64
 
55
65
  <Controls of={MenuItemStories.Default} />
package/src/NavItem.mdx CHANGED
@@ -48,6 +48,14 @@ import { NavItem } from '@estiva-app/ui'
48
48
  anchor prop passes through.
49
49
  - The label truncates; the count never gives up its width.
50
50
 
51
+ ## What it owns
52
+
53
+ It does these by itself. Write none of them around it.
54
+
55
+ | It does | So you never write | Refused when written by hand |
56
+ |---|---|---|
57
+ | Marks the current place for assistive technology when `active` | `aria-current` | |
58
+
51
59
  ## Props
52
60
 
53
61
  <Controls of={NavItemStories.Default} />
package/src/Person.mdx CHANGED
@@ -39,6 +39,10 @@ import { Person } from '@estiva-app/ui'
39
39
  and words are always set together (the Sizes story shows the pairings).
40
40
  - The name truncates; give the container `min-w-0` room to let it.
41
41
 
42
+ ## What it owns
43
+
44
+ Nothing. It only draws. Clicks and keys are yours.
45
+
42
46
  ## Props
43
47
 
44
48
  <Controls of={PersonStories.Named} />
@@ -53,6 +53,15 @@ import { Menu, PersonTrigger } from '@estiva-app/ui'
53
53
  - It is a native `<button>` on Base UI's Button: it takes a `ref` and every
54
54
  native button prop.
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
+ | Is a button: Tab reaches it, Enter and Space call your `onClick` | a clickable `<div>` | |
63
+ | Names itself from the person's name, and says whether what it opens is open (`open`) | `aria-label` or `aria-expanded` | |
64
+
56
65
  ## Keys
57
66
 
58
67
  | Key | Does |
package/src/Popover.mdx CHANGED
@@ -83,7 +83,15 @@ so it is the one case the caller drives:
83
83
  open={selectionRect !== null}
84
84
  onOpenChange={(open) => !open && setSelectionRect(null)}
85
85
  ariaLabel="Formatting"
86
+ side="top"
87
+ contentClassName="p-1"
86
88
  >
89
+ <Toolbar aria-label="Formatting">
90
+ <ToolbarButton aria-label="Bold" tooltip="Bold" onClick={bold}>
91
+ <IconBold size={16} stroke={1.5} />
92
+ </ToolbarButton>
93
+ </Toolbar>
94
+ </Popover>
87
95
  ```
88
96
 
89
97
  - **`anchor`** takes an element or a rect. A new rect moves the panel, so
@@ -116,6 +124,19 @@ something belongs on a `trigger`.
116
124
 
117
125
  <Canvas of={PopoverStories.FlippedForRoom} />
118
126
 
127
+ ## What it owns
128
+
129
+ It does these by itself. Write none of them around it.
130
+
131
+ | It does | So you never write | Refused when written by hand |
132
+ |---|---|---|
133
+ | Floats on top of the page | a portal, or a `z-index` to win | ✓ |
134
+ | Stays attached to its anchor on scroll and resize, and flips side when there is no room | a scroll or resize listener, or placement arithmetic | ✓ |
135
+ | Closes on a press outside | a click listener on the page | ✓ |
136
+ | Takes its keys by itself: Escape closes it | a key listener on the page | ✓ |
137
+ | Moves focus in when it opens, and back to the trigger when it closes | focus code | |
138
+ | Keeps its own open state | `useState` for open — unless you must know it (`open` / `onOpenChange`) | |
139
+
119
140
  ## Keys
120
141
 
121
142
  | Input | What happens |
@@ -45,7 +45,7 @@ pointer never sees it.
45
45
  import { PreviewCard } from '@estiva-app/ui'
46
46
 
47
47
  <PreviewCard content={<ItemDetail id={row.id} />} wrapperClassName="block w-full">
48
- <ListRow …/>
48
+ <ListRow item={row} />
49
49
  </PreviewCard>
50
50
  ```
51
51
 
@@ -57,9 +57,12 @@ import { PreviewCard } from '@estiva-app/ui'
57
57
  - **It prefers the right of the trigger and flips** when that side has no room,
58
58
  staying 12px from the trigger and 8px clear of every screen edge. `side`
59
59
  changes the preference, not the promise.
60
- - **Width, padding and height are yours** through `className`. The default is
61
- 360px wide, capped at 300px or the room available, whichever is smaller — a
62
- preview of one kind of thing is not the size of a preview of another.
60
+ - **Width is yours** through `className`; **padding and rhythm** through
61
+ `contentClassName`. The default is 360px wide, 12px of padding, capped at
62
+ 300px or the room available, whichever is smaller a preview of one kind of
63
+ thing is not the size of a preview of another.
64
+ - The padding sits inside the scrolling box, as in `Popover`, so the
65
+ scrollbar hugs the card's edge: its thumb 3px from it, like every panel's.
63
66
  - The trigger wrapper is `inline-flex`; a full-width row wants
64
67
  `wrapperClassName="block w-full"`.
65
68
 
@@ -82,6 +85,18 @@ lands.
82
85
 
83
86
  <Canvas of={PreviewCardStories.Loading} />
84
87
 
88
+ ## What it owns
89
+
90
+ It does these by itself. Write none of them around it.
91
+
92
+ | It does | So you never write | Refused when written by hand |
93
+ |---|---|---|
94
+ | Floats on top of the page | a portal, or a `z-index` to win | ✓ |
95
+ | Stays attached to its anchor on scroll and resize | a scroll or resize listener, or placement arithmetic | ✓ |
96
+ | Closes on a press outside | a click listener on the page | ✓ |
97
+ | Opens after the pointer rests, and stays up while the pointer moves into it | a hover timer | |
98
+ | Closes on Escape | a key handler | |
99
+
85
100
  ## Keys
86
101
 
87
102
  There is no keyboard trigger, by design — see above.
@@ -35,8 +35,15 @@ export interface PreviewCardProps {
35
35
  delay?: number
36
36
  /** After the pointer leaves, in ms — the grace that lets you cross the gap into the card. */
37
37
  closeDelay?: number
38
- /** On the card's surface: its width, its padding, a max height. */
38
+ /** On the card's surface: its width. */
39
39
  className?: string
40
+ /**
41
+ * On the scrolling content: its padding (default 12px) and rhythm. The
42
+ * padding sits inside the scrolling box, as in `Popover`, so the scrollbar
43
+ * hugs the card's edge — its thumb 3px from it, like every panel's
44
+ * (Katerina, 19 September; it sat 12px in, behind the card's padding).
45
+ */
46
+ contentClassName?: string
40
47
  /** Extra classes on the trigger wrapper — e.g. `block w-full` for a row. */
41
48
  wrapperClassName?: string
42
49
  }
@@ -58,7 +65,7 @@ const OPEN_DELAY = 350
58
65
  * here the card can be reached, so it can. */
59
66
  const CLOSE_DELAY = 200
60
67
 
61
- export function PreviewCard({ content, children, side = 'right', delay = OPEN_DELAY, closeDelay = CLOSE_DELAY, className, wrapperClassName }: PreviewCardProps) {
68
+ export function PreviewCard({ content, children, side = 'right', delay = OPEN_DELAY, closeDelay = CLOSE_DELAY, className, contentClassName, wrapperClassName }: PreviewCardProps) {
62
69
  return (
63
70
  <BasePreviewCard.Root>
64
71
  {/* The part renders an `<a>` by default, which a row is not; `render`
@@ -80,10 +87,10 @@ export function PreviewCard({ content, children, side = 'right', delay = OPEN_DE
80
87
  className="z-50 data-[anchor-hidden]:hidden"
81
88
  >
82
89
  <BasePreviewCard.Popup
83
- className={cn('w-[360px] p-3 outline-none', className)}
90
+ className={cn('w-[360px] p-0 outline-none', className)}
84
91
  render={<MenuPanel />}
85
92
  >
86
- <ScrollArea viewportClassName="max-h-[calc(min(300px,var(--available-height))_-_1.5rem)]" contentClassName="flex flex-col gap-3">
93
+ <ScrollArea viewportClassName="max-h-[min(300px,var(--available-height))]" contentClassName={cn('flex flex-col gap-3 p-3', contentClassName)}>
87
94
  {content}
88
95
  </ScrollArea>
89
96
  </BasePreviewCard.Popup>
@@ -42,6 +42,14 @@ import { ProgressBar } from '@estiva-app/ui'
42
42
  of the button's.
43
43
  - `className` is for placement: a width, a margin, `flex-1` in a row.
44
44
 
45
+ ## What it owns
46
+
47
+ It does these by itself. Write none of them around it.
48
+
49
+ | It does | So you never write | Refused when written by hand |
50
+ |---|---|---|
51
+ | Says what it is to assistive technology: a progress bar, with its value | `role="progressbar"` or `aria-valuenow` | ✓ |
52
+
45
53
  ## Props
46
54
 
47
55
  <Controls of={ProgressBarStories.Default} />
package/src/Property.mdx CHANGED
@@ -54,6 +54,10 @@ WithControls story shows the pairings).
54
54
  a child of the row or the column: a control with `flex-1` still fills the
55
55
  row, and two values still sit the row's gap apart.
56
56
 
57
+ ## What it owns
58
+
59
+ Nothing. It only draws. Clicks and keys are yours.
60
+
57
61
  ## Props
58
62
 
59
63
  <Controls of={PropertyStories.Row} />
package/src/Rail.mdx CHANGED
@@ -25,7 +25,7 @@ nothing else: **RailItem** is the tile, and where a tile goes is the app's.
25
25
  needs more than a word needs a row.
26
26
  - More entries than fit down a screen → fewer entries. The rail does not
27
27
  scroll, on purpose.
28
- - A strip of *actions* rather than places → a toolbar of **IconButton**s.
28
+ - A strip of *actions* rather than places → **Toolbar**.
29
29
 
30
30
  <Canvas of={RailStories.ManyItems} />
31
31
 
@@ -52,6 +52,14 @@ import { Rail, RailItem } from '@estiva-app/ui'
52
52
 
53
53
  <Canvas of={RailStories.OneItem} />
54
54
 
55
+ ## What it owns
56
+
57
+ It does these by itself. Write none of them around it.
58
+
59
+ | It does | So you never write | Refused when written by hand |
60
+ |---|---|---|
61
+ | Is a navigation, named "Navigation" unless you name it | `<nav>` or `aria-label` | |
62
+
55
63
  ## Keys
56
64
 
57
65
  The rail has no keyboard of its own: its tiles are links, and Tab walks them in
package/src/RailItem.mdx CHANGED
@@ -45,6 +45,14 @@ import { Rail, RailItem } from '@estiva-app/ui'
45
45
  click, navigate.
46
46
  - Icons are 16px at stroke 1.5.
47
47
 
48
+ ## What it owns
49
+
50
+ It does these by itself. Write none of them around it.
51
+
52
+ | It does | So you never write | Refused when written by hand |
53
+ |---|---|---|
54
+ | Marks the current place for assistive technology when `active` | `aria-current` | |
55
+
48
56
  ## Props
49
57
 
50
58
  <Controls of={RailItemStories.Default} />
package/src/Reaction.mdx CHANGED
@@ -50,6 +50,15 @@ import { Reaction } from '@estiva-app/ui'
50
50
  another client can change the count.
51
51
  - `disabled` takes it out of the Tab order and ignores presses.
52
52
 
53
+ ## What it owns
54
+
55
+ It does these by itself. Write none of them around it.
56
+
57
+ | It does | So you never write | Refused when written by hand |
58
+ |---|---|---|
59
+ | Is a toggle: pressed or not, and says so to assistive technology | `aria-pressed` | |
60
+ | Presses on Enter and Space; Tab skips it while disabled | a key handler | |
61
+
53
62
  ## Keys
54
63
 
55
64
  | Key | What happens |
@@ -68,6 +68,14 @@ and, when it is yours, the accent fill. The picker asks; the pill answers.
68
68
 
69
69
  <Canvas of={ReactionPickerStories.AndWhatItProduces} />
70
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
+ | Everything a Toolbar owns: one Tab stop, arrow keys between the reactions, wrapping at the ends | a key handler, or `tabIndex` on each reaction | |
78
+
71
79
  ## Keys
72
80
 
73
81
  | Input | What happens |
@@ -13,8 +13,10 @@ the content is moving; nothing moves when it appears.
13
13
 
14
14
  ## When
15
15
 
16
- - Any box that can hold more than fits: a menu or a dropdown list, a panel,
17
- a sidebar, the page's content column, a rail.
16
+ - Any box that can hold more than fits — **a list that grows with its data
17
+ counts, even when today's data fits**: a menu or a dropdown list, a panel, a
18
+ sidebar, the page's content column. Not a rail: a rail does not scroll, on
19
+ purpose.
18
20
  - `orientation="horizontal"` for a row wider than its box — a table, a
19
21
  board; `"both"` when a thing can run over either way.
20
22
 
@@ -53,6 +55,15 @@ import { ScrollArea } from '@estiva-app/ui'
53
55
  Chrome: wheel down over a sideways region inside a page, the page moves
54
56
  and the region does not; a sideways swipe, the other way round.
55
57
 
58
+ ## What it owns
59
+
60
+ It does these by itself. Write none of them around it.
61
+
62
+ | It does | So you never write | Refused when written by hand |
63
+ |---|---|---|
64
+ | Scrolls with our scrollbar | `overflow-auto` or `overflow-scroll` | ✓ |
65
+ | Scrolls with the keyboard when it has focus, like any scrolling box | key handlers | |
66
+
56
67
  ## Keys
57
68
 
58
69
  | Key | Does |
@@ -38,6 +38,15 @@ import { SearchInput } from '@estiva-app/ui'
38
38
  and this usually stands alone in a top bar rather than in a `Field`.
39
39
  Inside one, the label names it and you add nothing.
40
40
 
41
+ ## What it owns
42
+
43
+ It does these by itself. Write none of them around it.
44
+
45
+ | It does | So you never write | Refused when written by hand |
46
+ |---|---|---|
47
+ | Inside a `Field`, takes its name from the Field's label | an `id` / `htmlFor` pair | |
48
+ | Shows its focus ring while anything inside it has focus | a focus style of your own | |
49
+
41
50
  ## Props
42
51
 
43
52
  <Controls of={SearchInputStories.Default} />
@@ -53,6 +53,19 @@ import { IconPlus } from '@tabler/icons-react'
53
53
  - `render` swaps the title's element in Base UI's manner; it is how
54
54
  CollapsibleSection makes the title a `Collapsible.Trigger`.
55
55
 
56
+ - `hover="none"` keeps the row still under the pointer: for a heading whose
57
+ actions are always shown (`showActions="always"`), where the buttons light up
58
+ on their own. The default, `fill`, lights a row that toggles or has actions.
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
+ | With `chevron`, its title is a button that calls `onToggle` on a click, Enter or Space, and says whether it is open | a button of your own, or `aria-expanded` | |
67
+ | Shows its actions while one of them has focus | a focus style of your own | |
68
+
56
69
  ## Keys
57
70
 
58
71
  | Key | Does |
@@ -35,6 +35,15 @@ export const PersistentActions: Story = {
35
35
  },
36
36
  }
37
37
 
38
+ /** `hover="none"`: actions always shown, and the row stays still under the pointer — only the buttons light up. */
39
+ export const StillOnHover: Story = {
40
+ args: {
41
+ showActions: 'always',
42
+ hover: 'none',
43
+ actions: [{ icon: <IconPlus size={16} stroke={1.5} />, tooltip: 'Add', onClick: () => {} }],
44
+ },
45
+ }
46
+
38
47
  /** A count beside the title, held on screen while the actions come and go. */
39
48
  export const WithTrailing: Story = {
40
49
  args: {
@@ -23,6 +23,15 @@ describe('SectionHeader', () => {
23
23
  expect(onToggle).toHaveBeenCalledTimes(2)
24
24
  })
25
25
 
26
+ it('fills under the pointer when it does something, and hover="none" keeps it still', () => {
27
+ const actions = [{ icon: <i />, tooltip: 'Add', onClick: () => {} }]
28
+ const lit = render(<SectionHeader title="Section" actions={actions} />)
29
+ expect((lit.container.firstElementChild as HTMLElement).className).toContain('hover:bg-bg-hover')
30
+ cleanup()
31
+ const still = render(<SectionHeader title="Section" actions={actions} hover="none" />)
32
+ expect((still.container.firstElementChild as HTMLElement).className).not.toContain('hover:bg-bg-hover')
33
+ })
34
+
26
35
  it('an action beside the title acts, and never toggles', async () => {
27
36
  const onToggle = vi.fn()
28
37
  const add = vi.fn()