@estiva-app/ui 0.23.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.
- package/dist/AppShell.d.ts.map +1 -1
- package/dist/AttachmentCard.d.ts.map +1 -1
- package/dist/Form.d.ts.map +1 -1
- package/dist/IconButton.d.ts +9 -1
- package/dist/IconButton.d.ts.map +1 -1
- package/dist/ListColumn.d.ts +41 -0
- package/dist/ListColumn.d.ts.map +1 -0
- package/dist/PreviewCard.d.ts +9 -2
- package/dist/PreviewCard.d.ts.map +1 -1
- package/dist/SectionHeader.d.ts +7 -1
- package/dist/SectionHeader.d.ts.map +1 -1
- package/dist/Select.d.ts +12 -3
- package/dist/Select.d.ts.map +1 -1
- package/dist/Toolbar.d.ts +18 -0
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Tooltip.d.ts +13 -1
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/eslint/index.js +2 -1
- package/dist/eslint/index.js.map +2 -2
- package/dist/eslint/no-restyled-part.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +189 -143
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/registry.json +214 -11
- package/src/AppShell.mdx +18 -1
- package/src/AppShell.test.tsx +16 -0
- package/src/AppShell.tsx +4 -1
- package/src/AttachmentCard.mdx +12 -1
- package/src/AttachmentCard.test.tsx +10 -0
- package/src/AttachmentCard.tsx +8 -5
- package/src/Avatar.mdx +20 -5
- package/src/AvatarGroup.mdx +4 -0
- package/src/Banner.mdx +17 -8
- package/src/Breadcrumb.mdx +12 -0
- package/src/Button.mdx +17 -5
- package/src/Button.tsx +2 -2
- package/src/Card.mdx +14 -1
- package/src/Checkbox.mdx +11 -0
- package/src/Chip.mdx +4 -0
- package/src/ChipInput.mdx +13 -1
- package/src/CollapsibleSection.mdx +12 -3
- package/src/CommandPalette.mdx +14 -2
- package/src/ConfirmDialog.mdx +13 -2
- package/src/ContainerHeader.mdx +15 -2
- package/src/DialogShell.mdx +15 -1
- package/src/Divider.mdx +12 -1
- package/src/EditableText.mdx +17 -1
- package/src/EmptyState.mdx +17 -6
- package/src/Field.mdx +13 -5
- package/src/FieldLine.mdx +9 -1
- package/src/FilePicker.mdx +8 -0
- package/src/Form.mdx +11 -1
- package/src/Form.test.tsx +26 -0
- package/src/Form.tsx +7 -0
- package/src/IconButton.mdx +17 -3
- package/src/IconButton.stories.tsx +3 -0
- package/src/IconButton.test.tsx +25 -0
- package/src/IconButton.tsx +61 -46
- package/src/IdentityMenu.mdx +10 -1
- package/src/InlineChip.mdx +11 -0
- package/src/Kbd.mdx +6 -2
- package/src/Link.mdx +13 -1
- package/src/ListColumn.mdx +87 -0
- package/src/ListColumn.stories.tsx +136 -0
- package/src/ListColumn.test.tsx +50 -0
- package/src/ListColumn.tsx +63 -0
- package/src/Menu.mdx +18 -1
- package/src/MenuItem.mdx +15 -2
- package/src/NavItem.mdx +12 -0
- package/src/Person.mdx +7 -0
- package/src/PersonTrigger.mdx +11 -1
- package/src/PersonTrigger.tsx +1 -1
- package/src/Popover.mdx +23 -0
- package/src/PreviewCard.mdx +19 -4
- package/src/PreviewCard.tsx +11 -4
- package/src/ProgressBar.mdx +8 -0
- package/src/Property.mdx +4 -0
- package/src/Rail.mdx +12 -1
- package/src/RailItem.mdx +12 -0
- package/src/RailItem.tsx +1 -1
- package/src/Reaction.mdx +10 -0
- package/src/Reaction.tsx +1 -1
- package/src/ReactionPicker.mdx +8 -0
- package/src/ScrollArea.mdx +16 -2
- package/src/SearchInput.mdx +9 -0
- package/src/SectionHeader.mdx +18 -1
- package/src/SectionHeader.stories.tsx +9 -0
- package/src/SectionHeader.test.tsx +9 -0
- package/src/SectionHeader.tsx +9 -3
- package/src/SectionLabel.mdx +10 -2
- package/src/Select.mdx +24 -4
- package/src/Select.stories.tsx +4 -1
- package/src/Select.test.tsx +29 -0
- package/src/Select.tsx +36 -14
- package/src/Sidebar.mdx +8 -0
- package/src/Skeleton.mdx +8 -0
- package/src/Skeleton.tsx +1 -1
- package/src/Tabs.mdx +12 -1
- package/src/TextInput.mdx +9 -0
- package/src/TextInput.tsx +1 -1
- package/src/Textarea.mdx +8 -0
- package/src/Toast.mdx +12 -1
- package/src/Toolbar.mdx +20 -2
- package/src/Toolbar.stories.tsx +19 -2
- package/src/Toolbar.test.tsx +24 -1
- package/src/Toolbar.tsx +22 -0
- package/src/Tooltip.mdx +27 -2
- package/src/Tooltip.stories.tsx +26 -0
- package/src/Tooltip.test.tsx +51 -0
- package/src/Tooltip.tsx +17 -4
- package/src/TopBar.mdx +11 -0
- package/src/eslint/no-rebuilt-behaviour.ts +1 -1
- package/src/eslint/no-restyled-part.ts +1 -0
- package/src/heights.test.tsx +78 -0
- package/src/index.ts +2 -1
- package/src/pages.test.ts +142 -0
- package/src/registry/registry.test.ts +16 -8
- package/tailwind-preset.js +22 -1
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
|
-
-
|
|
29
|
-
|
|
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,18 @@ 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
|
+
- **Planned:** one part for the pop-up lists while typing in an editor (UIG-31).
|
|
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 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 | |
|
|
63
|
+
| Opens its `submenu` with → and closes it with ← | a submenu of your own | |
|
|
64
|
+
| Keeps its row's height in a scrolling column | `shrink-0` | |
|
|
65
|
+
|
|
53
66
|
## Props
|
|
54
67
|
|
|
55
68
|
<Controls of={MenuItemStories.Default} />
|
package/src/NavItem.mdx
CHANGED
|
@@ -48,6 +48,18 @@ 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
|
+
- A row is 32px tall, 8px in on each side, 8px between the icon and the label,
|
|
52
|
+
the label 14px regular, 6px corners.
|
|
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
|
+
| Marks the current place for assistive technology when `active` | `aria-current` | |
|
|
61
|
+
| Keeps its 32px in a scrolling column | `shrink-0` | |
|
|
62
|
+
|
|
51
63
|
## Props
|
|
52
64
|
|
|
53
65
|
<Controls of={NavItemStories.Default} />
|
package/src/Person.mdx
CHANGED
|
@@ -12,6 +12,7 @@ and name is one rule, 8px, wherever a person appears.
|
|
|
12
12
|
|
|
13
13
|
## When
|
|
14
14
|
|
|
15
|
+
- **The name takes the size of the text around it**; `size` sets the face.
|
|
15
16
|
- A person named inline: an assignee, an author, a participant, a value in
|
|
16
17
|
a property row.
|
|
17
18
|
- Someone with no published name shows the `fallback` — an em dash by
|
|
@@ -39,6 +40,12 @@ import { Person } from '@estiva-app/ui'
|
|
|
39
40
|
and words are always set together (the Sizes story shows the pairings).
|
|
40
41
|
- The name truncates; give the container `min-w-0` room to let it.
|
|
41
42
|
|
|
43
|
+
- **Planned:** a Byline part — face · name · time — in migration stage 7.
|
|
44
|
+
|
|
45
|
+
## What it owns
|
|
46
|
+
|
|
47
|
+
Nothing. It only draws. Clicks and keys are yours.
|
|
48
|
+
|
|
42
49
|
## Props
|
|
43
50
|
|
|
44
51
|
<Controls of={PersonStories.Named} />
|
package/src/PersonTrigger.mdx
CHANGED
|
@@ -47,12 +47,22 @@ import { Menu, PersonTrigger } from '@estiva-app/ui'
|
|
|
47
47
|
- **The accessible name comes from the person, in both shapes.** The row is
|
|
48
48
|
named by its visible name, and the `compact` face — which has no visible
|
|
49
49
|
text — takes the same name through an `aria-label` of its own, so nothing
|
|
50
|
-
is owed.
|
|
50
|
+
is owed. Without it that face would announce **"AD"**: the initials, read
|
|
51
51
|
as text. Pass an `aria-label` only to name what it *opens* rather than who
|
|
52
52
|
it shows; `IdentityMenu`'s compact shape says "Account menu".
|
|
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
|
+
| Keeps its 32px in a scrolling column | `shrink-0` | |
|
|
65
|
+
|
|
56
66
|
## Keys
|
|
57
67
|
|
|
58
68
|
| Key | Does |
|
package/src/PersonTrigger.tsx
CHANGED
|
@@ -65,7 +65,7 @@ export function PersonTrigger({ name, picture, fallback, size, open = false, com
|
|
|
65
65
|
aria-haspopup="menu"
|
|
66
66
|
aria-expanded={open}
|
|
67
67
|
className={cn(
|
|
68
|
-
'flex h-8 cursor-pointer items-center gap-1.5 rounded-md pl-1.5 pr-1.5 text-body-2 text-text-primary transition-colors hover:bg-bg-hover',
|
|
68
|
+
'flex h-8 min-h-8 cursor-pointer items-center gap-1.5 rounded-md pl-1.5 pr-1.5 text-body-2 text-text-primary transition-colors hover:bg-bg-hover',
|
|
69
69
|
open && 'bg-bg-hover',
|
|
70
70
|
// Under a `Menu` the open state is Base UI's, not a prop: the trigger
|
|
71
71
|
// carries `data-popup-open` while its menu is up, and sets its own
|
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,21 @@ something belongs on a `trigger`.
|
|
|
116
124
|
|
|
117
125
|
<Canvas of={PopoverStories.FlippedForRoom} />
|
|
118
126
|
|
|
127
|
+
- **Planned:** one part for the pop-up lists while typing in an editor (UIG-31).
|
|
128
|
+
|
|
129
|
+
## What it owns
|
|
130
|
+
|
|
131
|
+
It does these by itself. Write none of them around it.
|
|
132
|
+
|
|
133
|
+
| It does | So you never write | Refused when written by hand |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| Floats on top of the page | a portal, or a `z-index` to win | ✓ |
|
|
136
|
+
| 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 | ✓ |
|
|
137
|
+
| Closes on a press outside | a click listener on the page | ✓ |
|
|
138
|
+
| Takes its keys by itself: Escape closes it | a key listener on the page | ✓ |
|
|
139
|
+
| Moves focus in when it opens, and back to the trigger when it closes | focus code | |
|
|
140
|
+
| Keeps its own open state | `useState` for open — unless you must know it (`open` / `onOpenChange`) | |
|
|
141
|
+
|
|
119
142
|
## Keys
|
|
120
143
|
|
|
121
144
|
| Input | What happens |
|
package/src/PreviewCard.mdx
CHANGED
|
@@ -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
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
package/src/PreviewCard.tsx
CHANGED
|
@@ -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
|
|
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-
|
|
90
|
+
className={cn('w-[360px] p-0 outline-none', className)}
|
|
84
91
|
render={<MenuPanel />}
|
|
85
92
|
>
|
|
86
|
-
<ScrollArea viewportClassName="max-h-[
|
|
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>
|
package/src/ProgressBar.mdx
CHANGED
|
@@ -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 →
|
|
28
|
+
- A strip of *actions* rather than places → **Toolbar**.
|
|
29
29
|
|
|
30
30
|
<Canvas of={RailStories.ManyItems} />
|
|
31
31
|
|
|
@@ -52,6 +52,17 @@ import { Rail, RailItem } from '@estiva-app/ui'
|
|
|
52
52
|
|
|
53
53
|
<Canvas of={RailStories.OneItem} />
|
|
54
54
|
|
|
55
|
+
- The column is 64px wide, 12px from the top and bottom, 8px at the sides,
|
|
56
|
+
8px between tiles.
|
|
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
|
+
| Is a navigation, named "Navigation" unless you name it | `<nav>` or `aria-label` | |
|
|
65
|
+
|
|
55
66
|
## Keys
|
|
56
67
|
|
|
57
68
|
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,18 @@ import { Rail, RailItem } from '@estiva-app/ui'
|
|
|
45
45
|
click, navigate.
|
|
46
46
|
- Icons are 16px at stroke 1.5.
|
|
47
47
|
|
|
48
|
+
- A tile is 64px wide and 48px tall: the 16px icon in its 32px square, over its
|
|
49
|
+
9px label.
|
|
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
|
+
| Keeps its 48px in a scrolling column | `shrink-0` | |
|
|
59
|
+
|
|
48
60
|
## Props
|
|
49
61
|
|
|
50
62
|
<Controls of={RailItemStories.Default} />
|
package/src/RailItem.tsx
CHANGED
|
@@ -27,7 +27,7 @@ export function RailItem({ href, icon, label, active = false, className, ...prop
|
|
|
27
27
|
<a
|
|
28
28
|
href={href}
|
|
29
29
|
aria-current={active ? 'page' : undefined}
|
|
30
|
-
className={cn('group flex w-full shrink-0 flex-col items-center gap-0.5 px-2 py-0.5', className)}
|
|
30
|
+
className={cn('group flex h-12 w-full shrink-0 flex-col items-center gap-0.5 px-2 py-0.5', className)}
|
|
31
31
|
{...props}
|
|
32
32
|
>
|
|
33
33
|
<div
|
package/src/Reaction.mdx
CHANGED
|
@@ -50,6 +50,16 @@ 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
|
+
| Keeps its 24px in a scrolling column | `shrink-0` | |
|
|
62
|
+
|
|
53
63
|
## Keys
|
|
54
64
|
|
|
55
65
|
| Key | What happens |
|
package/src/Reaction.tsx
CHANGED
|
@@ -70,7 +70,7 @@ export function Reaction({ emoji, count, pressed = false, className, ...props }:
|
|
|
70
70
|
className={cn(
|
|
71
71
|
// Chip's pill at a control's height, so a reaction and a status chip
|
|
72
72
|
// read as the same family — 24px matches Button `small`.
|
|
73
|
-
'inline-flex h-6 items-center justify-center gap-1.5 rounded-full px-2',
|
|
73
|
+
'inline-flex h-6 min-h-6 items-center justify-center gap-1.5 rounded-full px-2',
|
|
74
74
|
'border transition-colors',
|
|
75
75
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
76
76
|
pressed
|
package/src/ReactionPicker.mdx
CHANGED
|
@@ -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 |
|
package/src/ScrollArea.mdx
CHANGED
|
@@ -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
|
|
17
|
-
|
|
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,18 @@ 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
|
+
- Text inside can look a touch lighter than outside: that is Chrome drawing a
|
|
59
|
+
scrolling layer, not a colour. Nothing to fix.
|
|
60
|
+
|
|
61
|
+
## What it owns
|
|
62
|
+
|
|
63
|
+
It does these by itself. Write none of them around it.
|
|
64
|
+
|
|
65
|
+
| It does | So you never write | Refused when written by hand |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| Scrolls with our scrollbar | `overflow-auto` or `overflow-scroll` | ✓ |
|
|
68
|
+
| Scrolls with the keyboard when it has focus, like any scrolling box | key handlers | |
|
|
69
|
+
|
|
56
70
|
## Keys
|
|
57
71
|
|
|
58
72
|
| Key | Does |
|
package/src/SearchInput.mdx
CHANGED
|
@@ -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} />
|
package/src/SectionHeader.mdx
CHANGED
|
@@ -39,7 +39,7 @@ import { IconPlus } from '@tabler/icons-react'
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
- `chevron`, `isExpanded` and `onToggle` are CollapsibleSection's to set
|
|
42
|
-
(
|
|
42
|
+
(one component folds, and it is that one). With
|
|
43
43
|
them, the title is a button that fills the row up to the actions — the
|
|
44
44
|
whole row is the hit target, the keyboard can toggle it, and it says its
|
|
45
45
|
state (`aria-expanded`).
|
|
@@ -53,6 +53,23 @@ 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
|
+
- The row is 32px tall, 8px in on each side, 4px between the title and the
|
|
61
|
+
actions, with 8px corners.
|
|
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
|
+
| 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` | |
|
|
70
|
+
| Shows its actions while one of them has focus | a focus style of your own | |
|
|
71
|
+
| Keeps its 32px in a scrolling column | `shrink-0` | |
|
|
72
|
+
|
|
56
73
|
## Keys
|
|
57
74
|
|
|
58
75
|
| 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()
|
package/src/SectionHeader.tsx
CHANGED
|
@@ -59,6 +59,12 @@ export interface SectionHeaderProps {
|
|
|
59
59
|
actions?: SectionAction[]
|
|
60
60
|
/** `hover` reveals the actions while the row is hovered or focused; `always` keeps them. */
|
|
61
61
|
showActions?: 'hover' | 'always'
|
|
62
|
+
/**
|
|
63
|
+
* `fill` lights the row under the pointer when it does something (a toggle,
|
|
64
|
+
* actions); `none` keeps it still — a heading whose actions are always shown,
|
|
65
|
+
* where the buttons light up on their own (UIG-14, Katerina, 19 September).
|
|
66
|
+
*/
|
|
67
|
+
hover?: 'fill' | 'none'
|
|
62
68
|
/**
|
|
63
69
|
* What the title renders as, in Base UI's manner. A plain button with
|
|
64
70
|
* `onToggle` by default; `CollapsibleSection` hands in `Collapsible.Trigger`.
|
|
@@ -67,7 +73,7 @@ export interface SectionHeaderProps {
|
|
|
67
73
|
className?: string
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
export function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, trailing, actions, showActions = 'hover', render, className }: SectionHeaderProps) {
|
|
76
|
+
export function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, trailing, actions, showActions = 'hover', hover = 'fill', render, className }: SectionHeaderProps) {
|
|
71
77
|
const titleElement = useRender({
|
|
72
78
|
render: render ?? (chevron ? <button type="button" onClick={onToggle} aria-expanded={isExpanded} /> : <span />),
|
|
73
79
|
props: {
|
|
@@ -93,11 +99,11 @@ export function SectionHeader({ title, chevron = false, isExpanded = true, onTog
|
|
|
93
99
|
return (
|
|
94
100
|
<div
|
|
95
101
|
className={cn(
|
|
96
|
-
'group flex h-[32px] items-center gap-1 rounded-lg px-2 transition-colors',
|
|
102
|
+
'group flex h-[32px] shrink-0 items-center gap-1 rounded-lg px-2 transition-colors',
|
|
97
103
|
// The fill says "this does something": a row with a toggle or actions
|
|
98
104
|
// lights up, a fixed heading over rows does not (2026-09-09, the
|
|
99
105
|
// Sidebar's fixed group).
|
|
100
|
-
(chevron || (actions && actions.length > 0)) && 'hover:bg-bg-hover',
|
|
106
|
+
hover === 'fill' && (chevron || (actions && actions.length > 0)) && 'hover:bg-bg-hover',
|
|
101
107
|
className,
|
|
102
108
|
)}
|
|
103
109
|
>
|
package/src/SectionLabel.mdx
CHANGED
|
@@ -14,6 +14,7 @@ how).
|
|
|
14
14
|
|
|
15
15
|
## When
|
|
16
16
|
|
|
17
|
+
- **It is a bare label**: the row it sits in, and the room around it, are yours.
|
|
17
18
|
- Titling a group of rows in a sidebar, a panel, a rail.
|
|
18
19
|
- Inside a menu it arrives via **MenuSection** — don't place it there by
|
|
19
20
|
hand.
|
|
@@ -35,8 +36,15 @@ import { SectionLabel } from '@estiva-app/ui'
|
|
|
35
36
|
|
|
36
37
|
It is a bare span: the caller owns the row it sits in and the spacing
|
|
37
38
|
around it. Read it `tone="secondary"` where it labels rather than leads — menu
|
|
38
|
-
headings and the command palette's groups do
|
|
39
|
-
passed in `className` is refused by the lint
|
|
39
|
+
headings and the command palette's groups do. A colour
|
|
40
|
+
passed in `className` is refused by the lint.
|
|
41
|
+
|
|
42
|
+
- Import it — never copy its classes. A copied class list drifts the first time
|
|
43
|
+
the label changes.
|
|
44
|
+
|
|
45
|
+
## What it owns
|
|
46
|
+
|
|
47
|
+
Nothing. It only draws. Clicks and keys are yours.
|
|
40
48
|
|
|
41
49
|
## Props
|
|
42
50
|
|
package/src/Select.mdx
CHANGED
|
@@ -29,8 +29,10 @@ corners and scrolling panels.
|
|
|
29
29
|
things.
|
|
30
30
|
- Several values → **ChipInput**.
|
|
31
31
|
- Switching views of one thing → **Tabs**.
|
|
32
|
-
- `disabled` explains nothing by itself —
|
|
33
|
-
|
|
32
|
+
- `disabled` explains nothing by itself — give it `disabledReason`, which says
|
|
33
|
+
why and keeps it reachable by Tab. Busy for a moment while an action runs
|
|
34
|
+
needs no reason.
|
|
35
|
+
- Ticking several on and off in a list → **Checkbox** with `row`.
|
|
34
36
|
|
|
35
37
|
## How
|
|
36
38
|
|
|
@@ -53,16 +55,34 @@ import { Select } from '@estiva-app/ui'
|
|
|
53
55
|
- **The list hangs under the trigger**, 4px below it, and flips above when
|
|
54
56
|
the room below is worse. Base UI would rather lay it *over* the trigger
|
|
55
57
|
with the chosen option covering the trigger's own text, the way macOS
|
|
56
|
-
does; that is off
|
|
57
|
-
below one today.
|
|
58
|
+
does; that is off, so every Select opens below its trigger.
|
|
58
59
|
- It caps at 288px, or at the room the screen actually has, whichever is
|
|
59
60
|
smaller — and scrolls inside that. Measured in a bottom-right corner:
|
|
60
61
|
the list flips above the trigger, slides left to stay on screen, and
|
|
61
62
|
stands at its full 288.
|
|
63
|
+
- **`disabledReason="Read only"`** holds it shut and keeps it reachable by Tab,
|
|
64
|
+
showing the reason on hover and on keyboard focus — as `Button` does. Plain
|
|
65
|
+
`disabled` is a native disabled control: Tab skips it, so it cannot say why.
|
|
66
|
+
Given both, the reason wins, as on `Button`.
|
|
62
67
|
- **The trigger is a `combobox`, not a `button`.** That is the ARIA pattern
|
|
63
68
|
for this control and Base UI's doing; a test looking for it by role has
|
|
64
69
|
to ask for `combobox`.
|
|
65
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
|
+
| Floats on top of the page | a portal, or a `z-index` to win | ✓ |
|
|
78
|
+
| Stays attached to its anchor on scroll and resize, and hides when its trigger scrolls out of sight | a scroll or resize listener, or placement arithmetic | ✓ |
|
|
79
|
+
| Closes on a press outside | a click listener on the page | ✓ |
|
|
80
|
+
| Takes its keys by itself: Enter and Space pick, Escape and Tab close | a key listener on the page | ✓ |
|
|
81
|
+
| Moves through its items with the arrow keys, and jumps to an option by its first letters | an arrow-key handler | ✓ |
|
|
82
|
+
| Says what it is to assistive technology: a list of options | `role="listbox"` or `role="option"` | ✓ |
|
|
83
|
+
| Keeps the highlighted option in view, and gives focus back to the trigger | scroll or focus code | |
|
|
84
|
+
| Keeps its height in a scrolling column | `shrink-0` | |
|
|
85
|
+
|
|
66
86
|
## Keys
|
|
67
87
|
|
|
68
88
|
| Input | What happens |
|
package/src/Select.stories.tsx
CHANGED
|
@@ -89,9 +89,12 @@ export const InAField: Story = {
|
|
|
89
89
|
),
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/** Unavailable — the same disabled treatment as every other control.
|
|
92
|
+
/** Unavailable — the same disabled treatment as every other control. To say why, `WithAReason`. */
|
|
93
93
|
export const Disabled: Story = { render: (args) => <Demo {...args} initial="done" disabled /> }
|
|
94
94
|
|
|
95
|
+
/** `disabledReason`: it looks disabled and will not open, but Tab reaches it and the reason shows on hover and on focus. */
|
|
96
|
+
export const WithAReason: Story = { render: (args) => <Demo {...args} initial="done" disabledReason="Read only: you are a guest here" /> }
|
|
97
|
+
|
|
95
98
|
/** Longer labels than the trigger is wide: the trigger truncates rather than growing; the menu takes the trigger's width as a minimum and grows past it. */
|
|
96
99
|
export const LongLabels: Story = {
|
|
97
100
|
render: (args) => (
|
package/src/Select.test.tsx
CHANGED
|
@@ -57,6 +57,35 @@ describe('Select', () => {
|
|
|
57
57
|
expect(trigger.getAttribute('aria-expanded')).toBe('false')
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
+
it('disabledReason: held shut, reachable by Tab, and it says why', async () => {
|
|
61
|
+
const user = userEvent.setup()
|
|
62
|
+
const onChange = vi.fn()
|
|
63
|
+
render(<Select value="todo" onChange={onChange} options={STATUSES} ariaLabel="Status" disabledReason="Read only: you are a guest" />)
|
|
64
|
+
const trigger = screen.getByRole('combobox', { name: 'Status' })
|
|
65
|
+
expect(trigger.getAttribute('aria-disabled')).toBe('true')
|
|
66
|
+
expect(trigger.hasAttribute('disabled')).toBe(false)
|
|
67
|
+
await user.click(trigger)
|
|
68
|
+
await user.keyboard('{ArrowDown}{Enter}')
|
|
69
|
+
expect(screen.queryByRole('listbox')).toBeNull()
|
|
70
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
71
|
+
await user.tab()
|
|
72
|
+
await user.tab({ shift: true })
|
|
73
|
+
expect(document.activeElement).toBe(trigger)
|
|
74
|
+
expect((await screen.findByRole('tooltip')).textContent).toBe('Read only: you are a guest')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// F2: as on Button, a reason wins over plain `disabled` — or the reason is never seen.
|
|
78
|
+
it('disabled with a reason: the reason wins', async () => {
|
|
79
|
+
const user = userEvent.setup()
|
|
80
|
+
render(<Select value="todo" onChange={vi.fn()} options={STATUSES} ariaLabel="Status" disabled disabledReason="Read only: you are a guest" />)
|
|
81
|
+
const trigger = screen.getByRole('combobox', { name: 'Status' })
|
|
82
|
+
expect(trigger.getAttribute('aria-disabled')).toBe('true')
|
|
83
|
+
expect(trigger.hasAttribute('disabled')).toBe(false)
|
|
84
|
+
await user.tab()
|
|
85
|
+
expect(document.activeElement).toBe(trigger)
|
|
86
|
+
expect((await screen.findByRole('tooltip')).textContent).toBe('Read only: you are a guest')
|
|
87
|
+
})
|
|
88
|
+
|
|
60
89
|
it('shows the placeholder when nothing matches', () => {
|
|
61
90
|
render(<Controlled initial="" placeholder="Choose" />)
|
|
62
91
|
expect(screen.getByRole('combobox', { name: 'Status' }).textContent).toContain('Choose')
|