@estiva-app/ui 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +11 -3
- package/dist/Select.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.js +127 -108
- package/dist/index.js.map +2 -2
- package/package.json +1 -1
- package/registry.json +51 -8
- package/src/AppShell.mdx +10 -0
- package/src/AppShell.test.tsx +16 -0
- package/src/AppShell.tsx +4 -1
- package/src/AttachmentCard.mdx +9 -0
- package/src/AttachmentCard.test.tsx +10 -0
- package/src/AttachmentCard.tsx +8 -5
- package/src/Avatar.mdx +13 -3
- package/src/AvatarGroup.mdx +4 -0
- package/src/Banner.mdx +13 -3
- package/src/Breadcrumb.mdx +10 -0
- package/src/Button.mdx +14 -3
- package/src/Card.mdx +9 -0
- package/src/Checkbox.mdx +10 -0
- package/src/Chip.mdx +4 -0
- package/src/ChipInput.mdx +12 -0
- package/src/CollapsibleSection.mdx +9 -0
- package/src/CommandPalette.mdx +14 -2
- package/src/ConfirmDialog.mdx +11 -0
- package/src/ContainerHeader.mdx +4 -0
- package/src/DialogShell.mdx +14 -0
- package/src/Divider.mdx +9 -1
- package/src/EditableText.mdx +11 -0
- package/src/EmptyState.mdx +15 -4
- package/src/Field.mdx +10 -1
- package/src/FieldLine.mdx +9 -1
- package/src/FilePicker.mdx +8 -0
- package/src/Form.mdx +10 -0
- package/src/Form.test.tsx +26 -0
- package/src/Form.tsx +7 -0
- package/src/IconButton.mdx +16 -2
- package/src/IconButton.stories.tsx +3 -0
- package/src/IconButton.test.tsx +25 -0
- package/src/IconButton.tsx +61 -46
- package/src/IdentityMenu.mdx +9 -0
- package/src/InlineChip.mdx +8 -0
- package/src/Kbd.mdx +4 -0
- package/src/Link.mdx +10 -0
- package/src/Menu.mdx +16 -1
- package/src/MenuItem.mdx +12 -2
- package/src/NavItem.mdx +8 -0
- package/src/Person.mdx +4 -0
- package/src/PersonTrigger.mdx +9 -0
- package/src/Popover.mdx +21 -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 +9 -1
- package/src/RailItem.mdx +8 -0
- package/src/Reaction.mdx +9 -0
- package/src/ReactionPicker.mdx +8 -0
- package/src/ScrollArea.mdx +13 -2
- package/src/SearchInput.mdx +9 -0
- package/src/SectionHeader.mdx +13 -0
- package/src/SectionHeader.stories.tsx +9 -0
- package/src/SectionHeader.test.tsx +9 -0
- package/src/SectionHeader.tsx +8 -2
- package/src/SectionLabel.mdx +4 -0
- package/src/Select.mdx +21 -2
- package/src/Select.stories.tsx +4 -1
- package/src/Select.test.tsx +17 -0
- package/src/Select.tsx +34 -13
- package/src/Sidebar.mdx +8 -0
- package/src/Skeleton.mdx +4 -0
- package/src/Tabs.mdx +12 -1
- package/src/TextInput.mdx +8 -0
- package/src/Textarea.mdx +8 -0
- package/src/Toast.mdx +12 -1
- package/src/Toolbar.mdx +11 -0
- package/src/Tooltip.mdx +26 -2
- package/src/Tooltip.stories.tsx +26 -0
- package/src/Tooltip.test.tsx +29 -0
- package/src/Tooltip.tsx +17 -4
- package/src/TopBar.mdx +4 -0
- package/src/eslint/no-rebuilt-behaviour.ts +1 -1
- package/src/eslint/no-restyled-part.ts +1 -0
- package/src/pages.test.ts +142 -0
- package/tailwind-preset.js +22 -1
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
|
|
|
@@ -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
|
|
@@ -55,6 +55,15 @@ import { Field, TextInput } from '@estiva-app/ui'
|
|
|
55
55
|
that says so already is left alone, and a `children` of several elements
|
|
56
56
|
keeps the asterisk and owes its own.
|
|
57
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
|
+
| Ties the label, the description and the error to the control inside it | `aria-label`, an `id` / `htmlFor` pair, or `aria-describedby` | |
|
|
65
|
+
| Marks the control invalid when it has an `error`, and announces it | `aria-invalid` | |
|
|
66
|
+
|
|
58
67
|
## Props
|
|
59
68
|
|
|
60
69
|
<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,6 +58,16 @@ 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
73
|
The same in every form (Katerina, 16 September).
|
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
|
|
|
@@ -54,11 +54,25 @@ import { IconPencil } from '@tabler/icons-react'
|
|
|
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:
|
package/src/IconButton.test.tsx
CHANGED
|
@@ -28,6 +28,31 @@ describe('IconButton', () => {
|
|
|
28
28
|
expect(onClick).toHaveBeenCalledTimes(1)
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
+
it('href: a link that looks like the button, with its tooltip', async () => {
|
|
32
|
+
const user = userEvent.setup()
|
|
33
|
+
render(
|
|
34
|
+
<IconButton href="/documents/12" aria-label="Open" tooltip="Open">
|
|
35
|
+
{icon}
|
|
36
|
+
</IconButton>,
|
|
37
|
+
)
|
|
38
|
+
expect(screen.queryByRole('button')).toBeNull()
|
|
39
|
+
const link = screen.getByRole('link', { name: 'Open' })
|
|
40
|
+
expect(link.getAttribute('href')).toBe('/documents/12')
|
|
41
|
+
expect(link.className).toContain('rounded-lg')
|
|
42
|
+
await user.hover(link)
|
|
43
|
+
expect((await screen.findByRole('tooltip')).textContent).toBe('Open')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('href while it cannot be used: the button, since a link cannot be disabled', () => {
|
|
47
|
+
render(
|
|
48
|
+
<IconButton href="/documents/12" aria-label="Open" disabledReason="Read only">
|
|
49
|
+
{icon}
|
|
50
|
+
</IconButton>,
|
|
51
|
+
)
|
|
52
|
+
expect(screen.queryByRole('link')).toBeNull()
|
|
53
|
+
expect(screen.getByRole('button', { name: 'Open' }).getAttribute('aria-disabled')).toBe('true')
|
|
54
|
+
})
|
|
55
|
+
|
|
31
56
|
it('shows the tooltip on hover, with its shortcut', async () => {
|
|
32
57
|
const user = userEvent.setup()
|
|
33
58
|
render(
|
package/src/IconButton.tsx
CHANGED
|
@@ -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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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) {
|
package/src/IdentityMenu.mdx
CHANGED
|
@@ -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
|
package/src/InlineChip.mdx
CHANGED
|
@@ -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 →
|
|
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
|
-
-
|
|
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,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} />
|
package/src/PersonTrigger.mdx
CHANGED
|
@@ -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 |
|
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.
|