@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/Button.mdx
CHANGED
|
@@ -6,7 +6,7 @@ import * as ButtonStories from './Button.stories'
|
|
|
6
6
|
# Button
|
|
7
7
|
|
|
8
8
|
The push-button for an action: a verb, clicked once — "Create", "Save",
|
|
9
|
-
"Delete
|
|
9
|
+
"Delete". Four variants on one geometry: 32px default / 24px small,
|
|
10
10
|
6px radius, medium weight.
|
|
11
11
|
|
|
12
12
|
<Canvas of={ButtonStories.AllVariants} />
|
|
@@ -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
|
|
31
|
-
or a
|
|
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
|
|
|
@@ -66,7 +67,18 @@ import { IconPlus } from '@tabler/icons-react'
|
|
|
66
67
|
has the same look.
|
|
67
68
|
- **Placed from outside, never restyled.** `className` takes space, size,
|
|
68
69
|
flex and grid, and position; a colour, a text size, a border, a corner or a
|
|
69
|
-
shadow is refused by the lint (`estiva/no-restyled-part
|
|
70
|
+
shadow is refused by the lint (`estiva/no-restyled-part`).
|
|
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
|
+
| Keeps its 32px (24px small) in a scrolling column | `shrink-0` | |
|
|
70
82
|
|
|
71
83
|
## Keys
|
|
72
84
|
|
package/src/Button.tsx
CHANGED
|
@@ -86,8 +86,8 @@ export function Button({
|
|
|
86
86
|
// this package's own story frames, each of which asks for the top.
|
|
87
87
|
// A control does not get to decide where its caller puts it.
|
|
88
88
|
'inline-flex items-center justify-center gap-1 rounded-md transition-colors font-sans font-medium',
|
|
89
|
-
size === 'default' && 'h-8 text-btn-default',
|
|
90
|
-
size === 'small' && 'h-6 text-btn-small',
|
|
89
|
+
size === 'default' && 'h-8 min-h-8 text-btn-default',
|
|
90
|
+
size === 'small' && 'h-6 min-h-6 text-btn-small',
|
|
91
91
|
// Extra right padding beside a leading icon, for optical balance.
|
|
92
92
|
size === 'default' && (hasLeadingIcon ? 'pl-2 pr-3' : 'px-2'),
|
|
93
93
|
size === 'small' && (hasLeadingIcon ? 'pl-1.5 pr-2' : 'px-1.5'),
|
package/src/Card.mdx
CHANGED
|
@@ -12,6 +12,8 @@ hairline. What goes inside, and the space around it, is yours.
|
|
|
12
12
|
|
|
13
13
|
## When
|
|
14
14
|
|
|
15
|
+
- **It draws no padding and no layout**: both are yours.
|
|
16
|
+
|
|
15
17
|
A box is a card when it stands for **one thing you could name**, and there are
|
|
16
18
|
many of it or you treat the whole box as that thing — open it, point at it,
|
|
17
19
|
move it.
|
|
@@ -73,9 +75,20 @@ import { Card } from '@estiva-app/ui'
|
|
|
73
75
|
picture that runs edge to edge. Without it their square corners show.
|
|
74
76
|
- **Placed from outside, never restyled.** `className` takes space, size,
|
|
75
77
|
flex and grid, and position; a colour, a text size, a border, a corner or a
|
|
76
|
-
shadow is refused by the lint (`estiva/no-restyled-part
|
|
78
|
+
shadow is refused by the lint (`estiva/no-restyled-part`). What is inside the card is yours: its padding and its words go on
|
|
77
79
|
your own element inside.
|
|
78
80
|
|
|
81
|
+
- **Planned:** the inside layout of a linked object's card, in migration stage 7.
|
|
82
|
+
|
|
83
|
+
## What it owns
|
|
84
|
+
|
|
85
|
+
It does these by itself. Write none of them around it.
|
|
86
|
+
|
|
87
|
+
| It does | So you never write | Refused when written by hand |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| With `href`, becomes a link: Link's anchor, with a real address | an anchor around the card | |
|
|
90
|
+
| Without `href`, shows it can be clicked when you pass `onClick` | `cursor-pointer` | |
|
|
91
|
+
|
|
79
92
|
## Props
|
|
80
93
|
|
|
81
94
|
<Controls of={CardStories.Surface} />
|
package/src/Checkbox.mdx
CHANGED
|
@@ -79,6 +79,17 @@ 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
|
+
| Keeps a `row`'s 40px in a scrolling column | `shrink-0` | |
|
|
92
|
+
|
|
82
93
|
## Keys
|
|
83
94
|
|
|
84
95
|
| 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
|
@@ -66,7 +66,7 @@ import { ChipInput } from '@estiva-app/ui'
|
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
- **`InputChip` on its own** names its ✕ `Remove <label>`. Where the ✕ does
|
|
69
|
-
something else, say what with `removeLabel` (a
|
|
69
|
+
something else, say what with `removeLabel` (a chip for a group you belong to: "Leave Design").
|
|
70
70
|
- To cap a long label, give the chip a `max-w-*` and `truncate`: the label is
|
|
71
71
|
cut and the ✕ keeps its size. `truncate` is off unless asked, because
|
|
72
72
|
cutting clips a letter's soft edge by up to 4px at 1x even when the label
|
|
@@ -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 |
|
|
@@ -14,11 +14,11 @@ toggle, and the rows under it, sliding shut and open.
|
|
|
14
14
|
## When
|
|
15
15
|
|
|
16
16
|
- A group of rows in a sidebar or a panel that the reader may want out of
|
|
17
|
-
the way —
|
|
17
|
+
the way — starred items, recent places.
|
|
18
18
|
- `defaultOpen={false}` for a group that starts folded.
|
|
19
19
|
- `storageKey` when the section should stay how the reader left it across
|
|
20
20
|
reloads: this browser remembers, under that key. The app prefixes the
|
|
21
|
-
key (`
|
|
21
|
+
key (`app.sidebar.starred`).
|
|
22
22
|
- `open` + `onOpenChange` when the app owns the state.
|
|
23
23
|
- `actions` for add, sort, filter beside the title — SectionHeader's,
|
|
24
24
|
revealed on hover or focus.
|
|
@@ -41,7 +41,7 @@ composition lives:
|
|
|
41
41
|
```tsx
|
|
42
42
|
import { CollapsibleSection, NavItem } from '@estiva-app/ui'
|
|
43
43
|
|
|
44
|
-
<CollapsibleSection title="
|
|
44
|
+
<CollapsibleSection title="Starred" storageKey="app.sidebar.starred" className="mt-2 shrink-0" contentClassName="gap-px">
|
|
45
45
|
<NavItem href="/p/1" label="Item one" />
|
|
46
46
|
<NavItem href="/p/2" label="Item two" />
|
|
47
47
|
</CollapsibleSection>
|
|
@@ -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 |
|
package/src/CommandPalette.mdx
CHANGED
|
@@ -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:
|
|
53
|
-
{ label: 'Places', rows: [{ id: 'place-one', label: 'Place one', icon
|
|
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 |
|
package/src/ConfirmDialog.mdx
CHANGED
|
@@ -45,7 +45,7 @@ import { ConfirmDialog } from '@estiva-app/ui'
|
|
|
45
45
|
dialog open — your surface has said why; a resolved `void`/`true`
|
|
46
46
|
closes it.
|
|
47
47
|
- **Escape cancels; a press on the backdrop does not.** This is Base UI's
|
|
48
|
-
`AlertDialog`, through the shell's `alert
|
|
48
|
+
`AlertDialog`, through the shell's `alert`: a
|
|
49
49
|
destructive question is answered rather than clicked away. The backdrop
|
|
50
50
|
used to cancel, and that is the one behaviour stage 3 deliberately changed.
|
|
51
51
|
- It announces itself as an alert (`role="alertdialog"`), so a screen reader
|
|
@@ -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.
|
|
@@ -60,7 +71,7 @@ Different from `DialogShell`'s in one row, and it is the row that matters.
|
|
|
60
71
|
| Input | What happens |
|
|
61
72
|
|---|---|
|
|
62
73
|
| Escape | cancels |
|
|
63
|
-
| backdrop click | **nothing** — the question has to be answered
|
|
74
|
+
| backdrop click | **nothing** — the question has to be answered |
|
|
64
75
|
| close button | cancels |
|
|
65
76
|
| Tab / Shift+Tab | cycles the controls inside the card, and cannot leave it |
|
|
66
77
|
| on close | focus returns to whatever opened it |
|
package/src/ContainerHeader.mdx
CHANGED
|
@@ -7,8 +7,7 @@ import * as ContainerHeaderStories from './ContainerHeader.stories'
|
|
|
7
7
|
|
|
8
8
|
The bar across the top of a column — a list, a thread, a side panel: its
|
|
9
9
|
title, and the buttons that act on the whole column at the right edge. 48px,
|
|
10
|
-
with a hairline under it.
|
|
11
|
-
looks (Katerina, 2026-09-18).
|
|
10
|
+
with a hairline under it.
|
|
12
11
|
|
|
13
12
|
<Canvas of={ContainerHeaderStories.WithActions} />
|
|
14
13
|
|
|
@@ -26,6 +25,11 @@ looks (Katerina, 2026-09-18).
|
|
|
26
25
|
- The top of the whole app → **TopBar**.
|
|
27
26
|
- A dialog's title → **DialogShell** draws its own.
|
|
28
27
|
|
|
28
|
+
- A row you draw yourself at the top of a column — your own padding and a line
|
|
29
|
+
under it → this part. Its numbers are every column's.
|
|
30
|
+
- A whole list column, header and scrolling list → **ListColumn**, which draws
|
|
31
|
+
this bar at its top.
|
|
32
|
+
|
|
29
33
|
## How
|
|
30
34
|
|
|
31
35
|
```tsx
|
|
@@ -51,6 +55,15 @@ import { ContainerHeader, IconButton } from '@estiva-app/ui'
|
|
|
51
55
|
- Only offer a button that does something: a button with nothing behind it is
|
|
52
56
|
a promise the column cannot keep.
|
|
53
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
|
+
| Draws the column's bar: 48px tall, the title 20px in at 14px medium, the actions 16px from the right, a hairline under it | a row of your own with padding and a border | |
|
|
65
|
+
| Keeps its 48px in a scrolling column | `shrink-0` | |
|
|
66
|
+
|
|
54
67
|
## Props
|
|
55
68
|
|
|
56
69
|
<Controls of={ContainerHeaderStories.Default} />
|
package/src/DialogShell.mdx
CHANGED
|
@@ -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
|
|
|
@@ -70,9 +71,22 @@ import { DialogShell, Button } from '@estiva-app/ui'
|
|
|
70
71
|
worth setting where typing is the point — the trap decides where focus
|
|
71
72
|
*cannot* go, not where it starts.
|
|
72
73
|
- `alert` for a question that has to be answered: a press on the backdrop
|
|
73
|
-
stops closing it
|
|
74
|
+
stops closing it. 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
|
|
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,17 @@ 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
|
+
- The line is 1px; the room around it is yours (`my-1` in a Menu).
|
|
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
|
+
| Says what it is to assistive technology: a separator | `role="separator"` | ✓ |
|
|
61
|
+
| Keeps its 1px line in a scrolling column | `shrink-0` | |
|
|
62
|
+
|
|
52
63
|
## Props
|
|
53
64
|
|
|
54
65
|
<Controls of={DividerStories.Default} />
|
package/src/EditableText.mdx
CHANGED
|
@@ -13,6 +13,9 @@ edit is never silently lost.
|
|
|
13
13
|
|
|
14
14
|
## When
|
|
15
15
|
|
|
16
|
+
- **It has no size of its own.** Give it the size of the text it stands in for —
|
|
17
|
+
a pane title's `text-body-2-strong` — on your element around it; the text and
|
|
18
|
+
the field both take it.
|
|
16
19
|
- A title or description edited where it is shown — no dialog, no separate
|
|
17
20
|
form.
|
|
18
21
|
- A reader who cannot write gets `readOnly` — the value without the edit
|
|
@@ -23,6 +26,8 @@ edit is never silently lost.
|
|
|
23
26
|
- A field in a form or dialog → **TextInput** / **Textarea** in a
|
|
24
27
|
**Field**.
|
|
25
28
|
- Composing something new → the field, not an edit-in-place of nothing.
|
|
29
|
+
- An edit that publishes something, like a message → **Textarea** with Save
|
|
30
|
+
and Cancel. EditableText saves when you leave it.
|
|
26
31
|
|
|
27
32
|
## How
|
|
28
33
|
|
|
@@ -53,7 +58,18 @@ import { EditableText } from '@estiva-app/ui'
|
|
|
53
58
|
- **Its text size is where it sits**, like `Person`'s: put `text-h2` on your own
|
|
54
59
|
element around it, and the text and the field both take it (a field inherits
|
|
55
60
|
the page's font). `className` places it — `-mx-2` to line its text up with
|
|
56
|
-
a header's inset — and a size passed there is refused by the lint
|
|
61
|
+
a header's inset — and a size passed there is refused by the lint.
|
|
62
|
+
|
|
63
|
+
- **Planned:** rich text edited where it is shown (UIG-30).
|
|
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
|
+
| Opens its field on a click, with the value selected | an edit mode of your own | |
|
|
72
|
+
| Enter and leaving the field save; Escape cancels and puts the value back (`multiline`: Shift+Enter is a new line) | key or blur handlers | |
|
|
57
73
|
|
|
58
74
|
## Keys
|
|
59
75
|
|
package/src/EmptyState.mdx
CHANGED
|
@@ -36,10 +36,12 @@ need a height from the caller (`className="h-full"`).
|
|
|
36
36
|
|
|
37
37
|
## When not
|
|
38
38
|
|
|
39
|
-
- Data is still on its way → **Skeleton**; empty and loading are
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
- Data is still on its way → **Skeleton**; empty and loading are different
|
|
40
|
+
truths. When the shape of what arrives is unknown, an EmptyState that says so
|
|
41
|
+
— "Opening…" — is fine.
|
|
42
|
+
- Something went wrong → say what failed, in words. An EmptyState can carry
|
|
43
|
+
it, in its usual look — never red. An empty state that hides a failure
|
|
44
|
+
misleads.
|
|
43
45
|
|
|
44
46
|
## How
|
|
45
47
|
|
|
@@ -55,14 +57,14 @@ import { EmptyState } from '@estiva-app/ui'
|
|
|
55
57
|
|
|
56
58
|
**When there is one thing to do about it, give it as `action`.** It is drawn
|
|
57
59
|
16px under the line as the package's Button, outlined — the same look wherever
|
|
58
|
-
it appears
|
|
60
|
+
it appears. Never put a Button beside an EmptyState in a
|
|
59
61
|
box of your own: the box places it, and no two boxes place it alike. A `page`
|
|
60
62
|
only; a section's line takes no action.
|
|
61
63
|
|
|
62
64
|
<Canvas of={EmptyStateStories.WithAction} />
|
|
63
65
|
|
|
64
66
|
```tsx
|
|
65
|
-
<EmptyState icon={<IconMessagePlus size={16} stroke={1.5} />} message="
|
|
67
|
+
<EmptyState icon={<IconMessagePlus size={16} stroke={1.5} />} message="Nothing here yet." action={{ label: 'Create', onClick: create }} />
|
|
66
68
|
```
|
|
67
69
|
|
|
68
70
|
**Put a section's empty state inside the box its rows live in, and give it no
|
|
@@ -83,6 +85,15 @@ The message is the caller's — a shared component has no words of its own
|
|
|
83
85
|
for what is missing. A hand-written "Nothing here" paragraph or span in a
|
|
84
86
|
section is the `section` manner, and becomes it.
|
|
85
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
|
+
| As a `page`, fills the room it is given and centres itself in it | centring, or padding around it | |
|
|
95
|
+
| Draws its one action, an outlined Button, under the line when you pass `action` | a button of your own under the line | |
|
|
96
|
+
|
|
86
97
|
## Props
|
|
87
98
|
|
|
88
99
|
<Controls of={EmptyStateStories.Page} />
|
package/src/Field.mdx
CHANGED
|
@@ -19,7 +19,7 @@ token; `required` marks it with the error-coloured asterisk.
|
|
|
19
19
|
|
|
20
20
|
- Displaying a label–value pair → **Property**; Field is for editing.
|
|
21
21
|
- The line belongs to a **group** of controls rather than to one — a value
|
|
22
|
-
with Save beside it, a row of action controls → **
|
|
22
|
+
with Save beside it, a row of action controls → **FieldLine**, the same
|
|
23
23
|
small line on its own.
|
|
24
24
|
|
|
25
25
|
## How
|
|
@@ -43,18 +43,26 @@ import { Field, TextInput } from '@estiva-app/ui'
|
|
|
43
43
|
reversed at stage 3: the Field used to win and override it.
|
|
44
44
|
- `helper` is the line under the control — the format, or what happens if
|
|
45
45
|
it is left empty. `error` **replaces** it and marks the control invalid,
|
|
46
|
-
so you no longer pass `aria-invalid` yourself. Both are announced;
|
|
47
|
-
|
|
48
|
-
those was.
|
|
46
|
+
so you no longer pass `aria-invalid` yourself. Both are announced; a line
|
|
47
|
+
written by hand beside the control is not.
|
|
49
48
|
- With neither, the control stays a direct child of the field, so nothing
|
|
50
49
|
that predates the two props moved a pixel.
|
|
51
50
|
- **`required` marks the control as well as the label.** The asterisk is a
|
|
52
51
|
picture of the requirement; `aria-required` on the control is the word for
|
|
53
|
-
it
|
|
52
|
+
it. Base UI's
|
|
54
53
|
`Field` has no `required` of its own, so the Field puts it there. A control
|
|
55
54
|
that says so already is left alone, and a `children` of several elements
|
|
56
55
|
keeps the asterisk and owes its own.
|
|
57
56
|
|
|
57
|
+
## What it owns
|
|
58
|
+
|
|
59
|
+
It does these by itself. Write none of them around it.
|
|
60
|
+
|
|
61
|
+
| It does | So you never write | Refused when written by hand |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| Ties the label, the description and the error to the control inside it | `aria-label`, an `id` / `htmlFor` pair, or `aria-describedby` | |
|
|
64
|
+
| Marks the control invalid when it has an `error`, and announces it | `aria-invalid` | |
|
|
65
|
+
|
|
58
66
|
## Props
|
|
59
67
|
|
|
60
68
|
<Controls of={FieldStories.Default} />
|
package/src/FieldLine.mdx
CHANGED
|
@@ -32,7 +32,7 @@ import { FieldLine } from '@estiva-app/ui'
|
|
|
32
32
|
|
|
33
33
|
<div className="flex flex-col gap-1.5">
|
|
34
34
|
<div className="flex items-center gap-2">
|
|
35
|
-
<TextInput value={draft} onChange={
|
|
35
|
+
<TextInput value={draft} onChange={(e) => setDraft(e.target.value)} />
|
|
36
36
|
<Button variant="primary" size="small">Save</Button>
|
|
37
37
|
</div>
|
|
38
38
|
{outcome && <FieldLine tone={outcome.kind}>{outcome.message}</FieldLine>}
|
|
@@ -52,6 +52,14 @@ import { FieldLine } from '@estiva-app/ui'
|
|
|
52
52
|
- The three tones are the same two classes Field draws, from one map in
|
|
53
53
|
`Field.tsx`, so they cannot drift.
|
|
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
|
+
| Says what it is to assistive technology: an alert for an error, a status for the rest | `role="alert"` or `role="status"` | ✓ |
|
|
62
|
+
|
|
55
63
|
## Props
|
|
56
64
|
|
|
57
65
|
<Controls of={FieldLineStories.Helper} />
|
package/src/FilePicker.mdx
CHANGED
|
@@ -44,6 +44,14 @@ const picker = useRef<HTMLInputElement>(null)
|
|
|
44
44
|
nothing to reset afterwards.
|
|
45
45
|
- `data-*` attributes pass on to the input, for a test to find it.
|
|
46
46
|
|
|
47
|
+
## What it owns
|
|
48
|
+
|
|
49
|
+
It does these by itself. Write none of them around it.
|
|
50
|
+
|
|
51
|
+
| It does | So you never write | Refused when written by hand |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| Opens the system's file picker from its button, and keeps the real file input out of Tab and screen readers | a hidden `<input type="file">` | |
|
|
54
|
+
|
|
47
55
|
## Props
|
|
48
56
|
|
|
49
57
|
<Controls of={FilePickerStories.Default} />
|
package/src/Form.mdx
CHANGED
|
@@ -58,9 +58,19 @@ import { Button, Field, Form, TextInput } from '@estiva-app/ui'
|
|
|
58
58
|
- `className` places the fields: the form draws no box, and neither does the
|
|
59
59
|
`<fieldset>` inside it.
|
|
60
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
|
+
| Sends on Enter in a one-line field, and on Ctrl+Enter anywhere inside | a key handler that submits | |
|
|
68
|
+
| Leaves Enter alone where it means something else: a Textarea, a list, a people picker | exceptions of your own | |
|
|
69
|
+
| Does nothing while it is sending: no second send, and nothing inside takes focus | a busy flag on every control | |
|
|
70
|
+
|
|
61
71
|
## Keys
|
|
62
72
|
|
|
63
|
-
The same in every form
|
|
73
|
+
The same in every form.
|
|
64
74
|
|
|
65
75
|
| Key | Does |
|
|
66
76
|
|---|---|
|
package/src/Form.test.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import { Checkbox } from './Checkbox'
|
|
|
18
18
|
import { IconButton } from './IconButton'
|
|
19
19
|
import { Field } from './Field'
|
|
20
20
|
import { Form } from './Form'
|
|
21
|
+
import { Popover } from './Popover'
|
|
21
22
|
import { TextInput } from './TextInput'
|
|
22
23
|
import { Textarea } from './Textarea'
|
|
23
24
|
|
|
@@ -46,6 +47,31 @@ describe('Form', () => {
|
|
|
46
47
|
expect(onSubmit).toHaveBeenCalledTimes(1)
|
|
47
48
|
})
|
|
48
49
|
|
|
50
|
+
it('a Form in a Popover inside a Form sends only itself (C1)', async () => {
|
|
51
|
+
// A link field in a pop-up over a composer: Enter there adds the link, and
|
|
52
|
+
// must not also send the message around it. React carries the submit
|
|
53
|
+
// through the Popover's portal to the outer form.
|
|
54
|
+
const user = userEvent.setup()
|
|
55
|
+
const outer = vi.fn()
|
|
56
|
+
const inner = vi.fn()
|
|
57
|
+
render(
|
|
58
|
+
<Form onSubmit={outer}>
|
|
59
|
+
<TextInput aria-label="Message" />
|
|
60
|
+
<Popover trigger={<Button>Link</Button>} open ariaLabel="Link">
|
|
61
|
+
<Form onSubmit={inner}>
|
|
62
|
+
<TextInput aria-label="Address" />
|
|
63
|
+
</Form>
|
|
64
|
+
</Popover>
|
|
65
|
+
</Form>,
|
|
66
|
+
)
|
|
67
|
+
await user.type(screen.getByRole('textbox', { name: 'Address' }), 'example.com{Enter}')
|
|
68
|
+
expect(inner).toHaveBeenCalledTimes(1)
|
|
69
|
+
expect(outer).not.toHaveBeenCalled()
|
|
70
|
+
await user.type(screen.getByRole('textbox', { name: 'Message' }), 'hi{Enter}')
|
|
71
|
+
expect(outer).toHaveBeenCalledTimes(1)
|
|
72
|
+
expect(inner).toHaveBeenCalledTimes(1)
|
|
73
|
+
})
|
|
74
|
+
|
|
49
75
|
it('sends from its submit button', async () => {
|
|
50
76
|
const user = userEvent.setup()
|
|
51
77
|
const onSubmit = vi.fn()
|
package/src/Form.tsx
CHANGED
|
@@ -160,6 +160,13 @@ export function Form({ onSubmit, busy: ownBusy = false, enterSends = true, class
|
|
|
160
160
|
}}
|
|
161
161
|
onKeyDown={onKeyDown}
|
|
162
162
|
onSubmit={(event) => {
|
|
163
|
+
/*
|
|
164
|
+
A form's own send only. React carries a submit up the component tree,
|
|
165
|
+
through a portal too, so a Form in a Popover inside this Form sent both:
|
|
166
|
+
Enter in a link field sent the whole message around it (UIG-14, C1,
|
|
167
|
+
Katerina 19 September). The inner Form has already handled its own.
|
|
168
|
+
*/
|
|
169
|
+
if (event.target !== event.currentTarget) return
|
|
163
170
|
event.preventDefault()
|
|
164
171
|
if (busyNow.current) return
|
|
165
172
|
void onSubmit()
|
package/src/IconButton.mdx
CHANGED
|
@@ -22,8 +22,8 @@ variants. For the actions a surface repeats too often to label every time.
|
|
|
22
22
|
- The action deserves a visible label → **Button** (words are cheaper than
|
|
23
23
|
a guessed icon).
|
|
24
24
|
- Inside a menu → **MenuItem** with `leading`.
|
|
25
|
-
- A strip of these
|
|
26
|
-
persist state, see **Checkbox**.
|
|
25
|
+
- A strip of these acting on one thing → **Toolbar**. The moment rows toggle
|
|
26
|
+
and persist state, see **Checkbox**.
|
|
27
27
|
|
|
28
28
|
## How
|
|
29
29
|
|
|
@@ -48,17 +48,31 @@ import { IconPencil } from '@tabler/icons-react'
|
|
|
48
48
|
- `glow` lights it in Signal — the send arrow while there is something to send.
|
|
49
49
|
- **Placed from outside, never restyled.** `className` takes space, size,
|
|
50
50
|
flex and grid, and position; a colour, a text size, a border, a corner or a
|
|
51
|
-
shadow is refused by the lint (`estiva/no-restyled-part
|
|
51
|
+
shadow is refused by the lint (`estiva/no-restyled-part`).
|
|
52
52
|
- `type` defaults to `"button"`; it is a native `<button>` on Base UI's
|
|
53
53
|
Button, and every native prop passes through.
|
|
54
54
|
- `disabledReason="Read only"` disables it, keeps it reachable by Tab,
|
|
55
55
|
and shows the reason in place of the tooltip — **on keyboard focus as
|
|
56
56
|
well as on hover**.
|
|
57
|
+
- **`href`** makes it a link that looks like the button: an anchor, so the
|
|
58
|
+
address stays real for a modified click or a new tab. A router app passes
|
|
59
|
+
`onClick`, prevents the default and navigates. While it cannot be used it
|
|
60
|
+
is the button — a link cannot be disabled.
|
|
57
61
|
- `tooltip` costs no wrapper: the button is the tooltip's trigger, so
|
|
58
62
|
this component's root is the `<button>` whether it carries one or not,
|
|
59
63
|
and both sit at the same height. A `Dialog.Close` or a `Menu.Trigger`
|
|
60
64
|
can be an IconButton that has a tooltip.
|
|
61
65
|
|
|
66
|
+
## What it owns
|
|
67
|
+
|
|
68
|
+
It does these by itself. Write none of them around it.
|
|
69
|
+
|
|
70
|
+
| It does | So you never write | Refused when written by hand |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| Says what it is to assistive technology: a button | `role="button"` | ✓ |
|
|
73
|
+
| Is reachable with Tab, also when it is disabled with a reason | a `tabIndex` | ✓ |
|
|
74
|
+
| Presses on Enter and Space, and does nothing while disabled | a key handler | |
|
|
75
|
+
|
|
62
76
|
## Keys
|
|
63
77
|
|
|
64
78
|
| Key | Does |
|
|
@@ -26,6 +26,9 @@ export const WithTooltip: Story = { args: { tooltip: 'Settings', tooltipPlacemen
|
|
|
26
26
|
/** Disabled with its reason in place of the tooltip; Tab still reaches it. */
|
|
27
27
|
export const WithAReason: Story = { args: { tooltip: 'Settings', disabledReason: 'Sign in to change settings' } }
|
|
28
28
|
|
|
29
|
+
/** `href`: a link that looks like the button — the same box, an anchor underneath. */
|
|
30
|
+
export const AsALink: Story = { args: { href: '#settings', tooltip: 'Settings' } }
|
|
31
|
+
|
|
29
32
|
/** Every variant × enabled/disabled. */
|
|
30
33
|
export const AllVariants: Story = {
|
|
31
34
|
// axe color-contrast is off here until PLAN.md stage 0.10 is ruled:
|