altitude-design 0.3.2 → 0.4.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/README.md +1 -0
- package/dist/cli/altitude.mjs +120 -20
- package/dist/guidance/README.md +4 -0
- package/dist/guidance/agent-setup.md +3 -2
- package/dist/guidance/components/AILabel/README.md +25 -0
- package/dist/guidance/components/Accordion/README.md +7 -0
- package/dist/guidance/components/AgentFleet/README.md +34 -0
- package/dist/guidance/components/AgentLanes/README.md +21 -0
- package/dist/guidance/components/AgentRequests/README.md +29 -0
- package/dist/guidance/components/Alert/README.md +22 -0
- package/dist/guidance/components/AltitudeProvider/README.md +48 -0
- package/dist/guidance/components/AppShell/README.md +51 -0
- package/dist/guidance/components/AreaChart/README.md +43 -0
- package/dist/guidance/components/Artifact/README.md +30 -0
- package/dist/guidance/components/Avatar/README.md +34 -0
- package/dist/guidance/components/Await/README.md +80 -0
- package/dist/guidance/components/Badge/README.md +22 -0
- package/dist/guidance/components/BarChart/README.md +47 -0
- package/dist/guidance/components/BarList/README.md +29 -0
- package/dist/guidance/components/Board/README.md +31 -0
- package/dist/guidance/components/Breadcrumb/README.md +35 -0
- package/dist/guidance/components/Button/README.md +17 -0
- package/dist/guidance/components/Card/README.md +14 -0
- package/dist/guidance/components/ChartBrush/README.md +41 -0
- package/dist/guidance/components/ChartContainer/README.md +152 -0
- package/dist/guidance/components/ChartLegend/README.md +35 -0
- package/dist/guidance/components/ChartReference/README.md +41 -0
- package/dist/guidance/components/ChartTooltip/README.md +38 -0
- package/dist/guidance/components/Checkbox/README.md +11 -0
- package/dist/guidance/components/Checklist/README.md +71 -0
- package/dist/guidance/components/CodeBlock/README.md +28 -0
- package/dist/guidance/components/ColorPicker/README.md +30 -0
- package/dist/guidance/components/Combobox/README.md +16 -0
- package/dist/guidance/components/CommandPalette/README.md +27 -0
- package/dist/guidance/components/CommentThread/README.md +30 -0
- package/dist/guidance/components/Confirmation/README.md +34 -0
- package/dist/guidance/components/ContextMenu/README.md +30 -0
- package/dist/guidance/components/Conversation/README.md +45 -0
- package/dist/guidance/components/CopyButton/README.md +23 -0
- package/dist/guidance/components/DataGrid/README.md +82 -0
- package/dist/guidance/components/DatePicker/README.md +67 -0
- package/dist/guidance/components/Dialog/README.md +29 -0
- package/dist/guidance/components/DiffView/README.md +30 -0
- package/dist/guidance/components/DotPlot/README.md +38 -0
- package/dist/guidance/components/Drilldown/README.md +15 -0
- package/dist/guidance/components/DropZone/README.md +21 -0
- package/dist/guidance/components/DropdownMenu/README.md +19 -0
- package/dist/guidance/components/EditRegion/README.md +104 -0
- package/dist/guidance/components/EmptyState/README.md +17 -0
- package/dist/guidance/components/FeatureGate/README.md +28 -0
- package/dist/guidance/components/Field/README.md +10 -0
- package/dist/guidance/components/FilterBar/README.md +69 -0
- package/dist/guidance/components/FormattedNumber/README.md +37 -0
- package/dist/guidance/components/Heatmap/README.md +45 -0
- package/dist/guidance/components/HoldButton/README.md +16 -0
- package/dist/guidance/components/HoverCard/README.md +18 -0
- package/dist/guidance/components/InlineCitation/README.md +24 -0
- package/dist/guidance/components/InlineEdit/README.md +133 -0
- package/dist/guidance/components/Input/README.md +27 -0
- package/dist/guidance/components/InputOTP/README.md +30 -0
- package/dist/guidance/components/ItemGroup/README.md +93 -0
- package/dist/guidance/components/Kbd/README.md +9 -0
- package/dist/guidance/components/LineChart/README.md +47 -0
- package/dist/guidance/components/Link/README.md +28 -0
- package/dist/guidance/components/Markdown/README.md +32 -0
- package/dist/guidance/components/Meter/README.md +29 -0
- package/dist/guidance/components/NotificationCenter/README.md +28 -0
- package/dist/guidance/components/NumberInput/README.md +29 -0
- package/dist/guidance/components/PageHeader/README.md +29 -0
- package/dist/guidance/components/Pagination/README.md +25 -0
- package/dist/guidance/components/PasswordInput/README.md +34 -0
- package/dist/guidance/components/PermissionMatrix/README.md +28 -0
- package/dist/guidance/components/Pile/README.md +12 -0
- package/dist/guidance/components/PlanPicker/README.md +25 -0
- package/dist/guidance/components/Popover/README.md +12 -0
- package/dist/guidance/components/Progress/README.md +10 -0
- package/dist/guidance/components/PromptInput/README.md +40 -0
- package/dist/guidance/components/PropertyList/README.md +57 -0
- package/dist/guidance/components/Question/README.md +27 -0
- package/dist/guidance/components/RadioGroup/README.md +10 -0
- package/dist/guidance/components/RelativeTime/README.md +26 -0
- package/dist/guidance/components/RemovalReceipt/README.md +34 -0
- package/dist/guidance/components/Resizable/README.md +36 -0
- package/dist/guidance/components/SaveBar/README.md +27 -0
- package/dist/guidance/components/SavedViews/README.md +24 -0
- package/dist/guidance/components/ScatterChart/README.md +41 -0
- package/dist/guidance/components/Schedule/README.md +76 -0
- package/dist/guidance/components/ScrollArea/README.md +7 -0
- package/dist/guidance/components/SecretField/README.md +28 -0
- package/dist/guidance/components/Select/README.md +27 -0
- package/dist/guidance/components/Separator/README.md +26 -0
- package/dist/guidance/components/Sheet/README.md +32 -0
- package/dist/guidance/components/Skeleton/README.md +14 -0
- package/dist/guidance/components/Slider/README.md +25 -0
- package/dist/guidance/components/SortableList/README.md +11 -0
- package/dist/guidance/components/Sparkline/README.md +29 -0
- package/dist/guidance/components/SplitButton/README.md +26 -0
- package/dist/guidance/components/Stat/README.md +31 -0
- package/dist/guidance/components/Stepper/README.md +29 -0
- package/dist/guidance/components/Suggestions/README.md +29 -0
- package/dist/guidance/components/Surface/README.md +20 -0
- package/dist/guidance/components/Switch/README.md +9 -0
- package/dist/guidance/components/SyncIndicator/README.md +24 -0
- package/dist/guidance/components/Table/README.md +76 -0
- package/dist/guidance/components/Tabs/README.md +18 -0
- package/dist/guidance/components/TagInput/README.md +66 -0
- package/dist/guidance/components/Terminal/README.md +27 -0
- package/dist/guidance/components/ThreadList/README.md +37 -0
- package/dist/guidance/components/TimeInput/README.md +26 -0
- package/dist/guidance/components/Timeline/README.md +32 -0
- package/dist/guidance/components/Toaster/README.md +16 -0
- package/dist/guidance/components/Toggle/README.md +33 -0
- package/dist/guidance/components/Toolbar/README.md +37 -0
- package/dist/guidance/components/Tooltip/README.md +12 -0
- package/dist/guidance/components/Tour/README.md +29 -0
- package/dist/guidance/components/Trace/README.md +37 -0
- package/dist/guidance/components/Tree/README.md +86 -0
- package/dist/guidance/evidence.json +18 -0
- package/dist/guidance/index.json +811 -7
- package/dist/guidance/llms.txt +153 -2
- package/dist/guidance/manifest.json +133 -12
- package/dist/guidance/patterns/screen-layout.md +1 -1
- package/dist/guidance/references/build/checks/agent-setup.mjs +7 -0
- package/dist/guidance/references/build/checks/guidance.mjs +8 -2
- package/dist/guidance/references/build/checks/source-check.mjs +101 -0
- package/dist/guidance/rules.json +155 -0
- package/dist/guidance/visual/ActionsInPlace.md +463 -0
- package/dist/guidance/visual/Charts.md +119 -0
- package/dist/guidance/visual/Distinctiveness.md +106 -0
- package/dist/guidance/visual/Elevation.md +1 -1
- package/dist/guidance/visual/Mobile.md +64 -0
- package/dist/guidance/visual/Motion.md +225 -0
- package/dist/guidance/visual/README.md +98 -0
- package/dist/guidance/visual/Research.md +1 -1
- package/dist/guidance/visual/Theming.md +349 -0
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# PageHeader
|
|
2
|
+
|
|
3
|
+
The block every page opens with: breadcrumb, title, a line of description, the page's actions, and sometimes tabs.
|
|
4
|
+
|
|
5
|
+
**Elevation.** The page's name is the record, so the block lies on the ground. Only its actions are live: secondaries at level 1, ghosts at 0, and **one primary at level 2, in its own slot**. "The primary action rests at level 2" (law 1) becomes structural: `primary` takes one element, and a `variant="primary"` button put in `actions` logs a development warning naming law 1. Actions that don't need to be seen go in `overflow`, a "More actions" menu (`DropdownMenu`, level 3). With `sticky`, the header stays at the top and **rises one level when content passes under it** (`surface-raised`, `elev-1`), as `ScrollArea`'s header does.
|
|
6
|
+
|
|
7
|
+
**Layout.** The breadcrumb sits above, the title and description on the left, and the actions on the right (the primary last, nearest the reading edge). Tabs go underneath. Everything wraps on narrow screens. Set `--alt-page-pad` to the page's side padding (`AppShell` does) and the header reaches through it, so a sticky header's rise spans the page.
|
|
8
|
+
|
|
9
|
+
**Accessibility.** A `<header>` with the page's `h1` (`PageHeaderTitle`). The tab order is breadcrumb, actions, tabs. "More actions" is a [menu button](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/). Under forced colours a risen sticky header gets a bottom border.
|
|
10
|
+
|
|
11
|
+
**Props.** `PageHeader`: `breadcrumb`, `primary`, `actions`, `overflow` (`DropdownMenu` entries), `tabs`, `sticky`. Inside it go `PageHeaderTitle` (an `h1`) and `PageHeaderDescription`.
|
|
12
|
+
|
|
13
|
+
**You provide:** the page's name, what it's for in one line, and its actions, with the one that matters most as `primary`.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<PageHeader sticky
|
|
17
|
+
breadcrumb={<Breadcrumb items={[{ label: 'Settings', href: '/settings' }, { label: 'Members' }]} />}
|
|
18
|
+
primary={<Button variant="primary">Invite members</Button>}
|
|
19
|
+
actions={<Button variant="secondary">Export list</Button>}
|
|
20
|
+
overflow={[{ label: 'Download audit log', onSelect: download }]}>
|
|
21
|
+
<PageHeaderTitle>Members</PageHeaderTitle>
|
|
22
|
+
<PageHeaderDescription>People who can see and edit this workspace.</PageHeaderDescription>
|
|
23
|
+
</PageHeader>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- Do give a page at most one primary action; put the rest in `actions` or `overflow`.
|
|
27
|
+
- Do keep the description to one line about what the page is for.
|
|
28
|
+
- Don't put a PageHeader inside a `Card`; that's `CardHeader`.
|
|
29
|
+
- Don't make the whole header a raised card. It lies on the ground, and only rises while content passes under it.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Pagination
|
|
2
|
+
|
|
3
|
+
Moves through pages of a collection the server pages: invoices, members, audit events.
|
|
4
|
+
|
|
5
|
+
**Elevation: one thumb that travels.** The page numbers sit in a trench (`surface-well`), and **the current page is one raised thumb that travels to it** on `ease-glide`, as `RadioGroup`'s does. That reads as "you are here, among these", where a row of buttons reads as "any of these does something". Pointing at another page shows a ghost of the thumb there (feedforward). While the next page loads (`loading`), the thumb is held pressed until the rows arrive. Previous and Next are ghost buttons, flush and `ink-faint` at the ends.
|
|
6
|
+
|
|
7
|
+
**Pages.** It shows the first page, the last, and the current one with `siblings` either side, with "…" where pages are skipped. A gap of a single page shows that page instead, because "…" would take as much room. `pageRange()` gives the same list. **Cursor paging:** leave out `pageCount` and you get Previous, Next (disabled once `hasNext` is false) and "Page 3".
|
|
8
|
+
|
|
9
|
+
**Summary.** With `total` and `pageSize` it says "41–60 of 214 invoices" (`noun`) in a polite live region, so the change is announced after a page change.
|
|
10
|
+
|
|
11
|
+
**Accessibility.** A `nav` labelled "Pagination", with links when `hrefFor` gives pages URLs, or buttons otherwise. **They stay in plain Tab order:** these are links drawn like a radio group, not a radio group, so there's no roving focus. The current page has `aria-current="page"`. Pages are named "Page 3", and the steps "Previous page" and "Next page", with `aria-disabled` at the ends. Below 480px the steps show only their chevrons. Under forced colours the trench is dashed and the thumb has a 1px border. Under `contrast="more"` the thumb gets a 2px slab. Under reduced motion the thumb appears in place.
|
|
12
|
+
|
|
13
|
+
**Props:** `page` (from 1); `pageCount`; `onPageChange`; `total`, `pageSize`, `noun`; `hrefFor`; `hasNext`; `siblings` (default 1); `loading`; `locale`; `aria-label`.
|
|
14
|
+
|
|
15
|
+
**You provide:** the current page, how many there are (or whether there's a next one), and what happens on a change.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<Pagination page={page} pageCount={11} onPageChange={setPage} total={214} pageSize={20} noun="invoices" />
|
|
19
|
+
<Pagination page={page} onPageChange={setPage} hasNext={!!cursor.next} /> // cursor paging
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- Do use `hrefFor` when pages have URLs, so they can be opened in a new tab.
|
|
23
|
+
- Do show the summary; "Page 3 of 11" says less than "41–60 of 214".
|
|
24
|
+
- Don't page short lists; show them whole.
|
|
25
|
+
- Don't use it for steps in a process; that's a `Stepper`.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# PasswordInput
|
|
2
|
+
|
|
3
|
+
A password field: signing in, signing up, a reset, "change password" in settings.
|
|
4
|
+
|
|
5
|
+
**Elevation.** It's an `Input`, so it's an opening that fills. In its end cap sits a reveal `Toggle`. While the password shows, the toggle is on: sunk and filled, as every Toggle is. Its name ("Show password") never changes; `aria-pressed` says whether it's on.
|
|
6
|
+
|
|
7
|
+
**Strength is a reading.** When you pass `strength`, the score (0–4, from your scoring: zxcvbn or your own rules) is shown as a `Meter`: a flush track with flat ink, never `Progress`'s raised fill, because a password's strength is a reading, not a task moving toward done (see [Charts](../../visual/Charts.md)). The word ("Weak", "Good", "Strong") sits beside the label and carries the meaning. The fill stays the meter's own colour: Meter's thresholds mean "higher is worse", which is backwards for strength.
|
|
8
|
+
|
|
9
|
+
**Requirements** are stamped as they're met, like a done `Checklist` item: muted, with a check pressed into a small well. Unmet ones keep an empty ring.
|
|
10
|
+
|
|
11
|
+
**Caps Lock.** While the field is focused and Caps Lock is on, a caption with an icon says "Caps Lock is on".
|
|
12
|
+
|
|
13
|
+
**Accessibility.** A plain `<input type="password">`, with `autocomplete="new-password"` when there's a strength meter or requirements, and `"current-password"` otherwise (pass `autoComplete` to say otherwise). The reveal toggle is a separate tab stop after the field, and revealing doesn't move focus. Strength, requirements and the caps-lock notice describe the field (`aria-describedby`). Strength and unmet requirements are announced politely once typing pauses (0.9s), not on every key. The meter's value text is the word. Under forced colours the requirement stamps and the caps-lock icon are `CanvasText`.
|
|
14
|
+
|
|
15
|
+
**Props:** `strength` (`(value) => 0–4`); `strengthLabels`; `requirements` (`{ label, test }[]`); `revealLabel` (default "Show password"); and every `Input` prop except `type` and `trailing`.
|
|
16
|
+
|
|
17
|
+
**You provide:** the scoring and the rules, when people are creating a password. For signing in, nothing more than a label.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<Field label="New password">
|
|
21
|
+
<PasswordInput
|
|
22
|
+
strength={(v) => zxcvbn(v).score}
|
|
23
|
+
requirements={[
|
|
24
|
+
{ label: 'At least 12 characters', test: (v) => v.length >= 12 },
|
|
25
|
+
{ label: 'Not a common password', test: notCommon },
|
|
26
|
+
]}
|
|
27
|
+
/>
|
|
28
|
+
</Field>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- Do let people reveal what they typed; masking alone causes errors.
|
|
32
|
+
- Do say your rules before someone breaks them, as requirements.
|
|
33
|
+
- Don't draw strength as a progress bar.
|
|
34
|
+
- Don't show a strength meter when signing in; it's for creating a password.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# PermissionMatrix
|
|
2
|
+
|
|
3
|
+
Roles against permissions: "Admins can manage billing, Members can invite, Viewers can only read". The same grid serves notification preferences (events against channels).
|
|
4
|
+
|
|
5
|
+
**Elevation.** The grid is settled: the record of who can do what, on the ground with `line` dividers.
|
|
6
|
+
- **An editable grant is a choice**, built like a `Checkbox`: an empty trench that the accent pours into when granted.
|
|
7
|
+
- **An inherited or fixed grant is already decided**, so it's a flush stamped check (or a dash when not granted), muted, like a done `Checklist` item. It's pressed into the record, not drawn as a disabled checkbox, because it isn't unavailable. A `locked` role (an owner) has every permission, stamped.
|
|
8
|
+
- **The active cell** is a 2px `accent-ink` ring, never a height, as in `DataGrid`.
|
|
9
|
+
- **Saving:** a toggled cell whose `onChange` returns a promise hovers as pending until it settles, then takes the afterglow. A rejection reverts it.
|
|
10
|
+
|
|
11
|
+
**Accessibility.** The [grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/) with checkbox cells. Arrow keys move, Home and End go along the row, Ctrl+Home and Ctrl+End go to the corners, and Space or Enter toggles. Each cell reads "Manage billing, Admin, checked". A stamped cell is `aria-readonly` and says why ("Granted, inherited from Everyone", "Always granted to Owner"): it's reachable, and toggling it says it can't be changed here. Changes are announced ("Create projects granted to Viewer"). Group headings head their rows. Under forced colours the ring is `Highlight` and stamped cells keep their glyph. Under `contrast="more"` the ring is 3px.
|
|
12
|
+
|
|
13
|
+
**Props:** `roles` (`{ id, name, locked? }[]`); `permissions` (`{ id, name, group? }[]`); `value` (`{ [roleId]: { [permissionId]: boolean } }`); `inherited(roleId, permissionId)` (→ `{ granted, from }` or undefined); `onChange(roleId, permissionId, granted)` (may return a promise); `aria-label`.
|
|
14
|
+
|
|
15
|
+
**You provide:** the roles, the permissions, who has what, and where inherited grants come from.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<PermissionMatrix aria-label="Role permissions"
|
|
19
|
+
roles={[{ id: 'owner', name: 'Owner', locked: true }, { id: 'admin', name: 'Admin' }]}
|
|
20
|
+
permissions={[{ id: 'billing.manage', name: 'Manage billing', group: 'Billing' }]}
|
|
21
|
+
value={grants} inherited={(role, perm) => inheritFrom(role, perm)}
|
|
22
|
+
onChange={(role, perm, granted) => saveGrant(role, perm, granted)} />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Do say where an inherited grant comes from.
|
|
26
|
+
- Do return a promise from `onChange` so a cell shows it's saving.
|
|
27
|
+
- Don't draw inherited grants as disabled checkboxes.
|
|
28
|
+
- Don't lift the active cell; it's a ring.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Pile
|
|
2
|
+
|
|
3
|
+
Shows a group of items as a stack. Its thickness says roughly how many; click it and the items fan out over the page.
|
|
4
|
+
|
|
5
|
+
**Closed.** The top card is raised (level 1). Beneath it sit 0–3 sheets: none for one item, one for 2–3, two for 4–9, three for 10 or more. Thickness is read in bins, so the count badge carries the number. On hover or focus the top lifts and the sheets beneath fan a little, a preview of what's underneath; nothing moves before the pointer is on it.
|
|
6
|
+
|
|
7
|
+
**Open.** Give the pile `items` and `renderItem` and its top card becomes a toggle. Clicking it (or Enter/Space) slides the items out from under the top card, one after another, into a column that **floats over the page below**: nothing on the page moves. Each item is its own card with its own shadow (`elev-2`, `elev-3` under the pointer); the scroll area itself has none, and its top and bottom edges fade out rather than cutting the shadows off. Click an item to choose it (`onSelect`). Click the top again, press Escape or click anywhere else and the items slide back under. Long piles scroll after `maxHeight`.
|
|
8
|
+
|
|
9
|
+
**You provide:** `items`, `renderItem(item, { index })`, `onSelect`, the top card's content as children, optionally `getKey`, `maxHeight` and `open`/`onOpenChange` (for example, to close it after a choice). For a pile that only shows quantity, pass `count` instead of `items`.
|
|
10
|
+
|
|
11
|
+
- Leave room below a pile, or near the bottom of the viewport the list may run off-screen.
|
|
12
|
+
- Use piles for collections people triage: review queues, inboxes, stacks of drafts.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# PlanPicker
|
|
2
|
+
|
|
3
|
+
Choose a plan (Free, Pro, Business) in sign-up, upgrade and downgrade flows, with a Monthly / Annually switch, prices, and what each plan includes.
|
|
4
|
+
|
|
5
|
+
**Elevation: one choice, one mark.** "A radio group is one switch with several positions, so its single thumb slides to the chosen option." The plan cards are positions in one group: they lie flush on the ground with a `line` rim, and **one raised mark travels to the chosen card's corner** on `ease-glide`. The chosen card also takes an `accent` rim. Pointing at another card shows a ghost of the mark there.
|
|
6
|
+
|
|
7
|
+
**No lifted "most popular" card.** Pricing pages usually raise and glow the plan they want you to pick. Marketing emphasis isn't importance *right now*, so it isn't height. **"Recommended" and "Current plan" are flush `Badge` plates.** The only level-2 slab is your Continue button beside the picker (law 1), and its label should name the outcome ("Continue to Pro").
|
|
8
|
+
|
|
9
|
+
**Prices** come in minor units and are formatted by `FormattedNumber`, so changing the period cross-fades them and never counts. Annual prices are shown per month, "billed annually". A plan with `price: null` says "Contact sales". A plan with `unavailable` can't be chosen and says why.
|
|
10
|
+
|
|
11
|
+
**Accessibility.** A [radio group](https://www.w3.org/WAI/ARIA/apg/patterns/radio/): one tab stop, and arrows move and choose (skipping unavailable plans). Each plan reads its name, price and period ("Pro, €20.00 per seat per month, billed annually, recommended"), with its features as the description. The period switch is a single-choice `ToggleGroup`. Under forced colours the chosen card has a 2px border and the mark keeps a `CanvasText` outline. Under `contrast="more"` the mark gets a hard slab. Under reduced motion the mark appears in place.
|
|
12
|
+
|
|
13
|
+
**Props:** `plans` (`{ id, name, description?, price ({ month, year } | number | null), currency?, unit?, features?, unavailable? }[]`); `value`, `defaultValue`, `onValueChange`; `current`; `recommended`; `period`, `onPeriodChange`; `locale`; `aria-label`.
|
|
14
|
+
|
|
15
|
+
**You provide:** the plans and their prices, the current plan, and the Continue button. The flow around it (proration, downgrade consequences) is the billing pattern.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<PlanPicker plans={plans} value={plan} onValueChange={setPlan} current="free" recommended="pro" period={period} onPeriodChange={setPeriod} />
|
|
19
|
+
<Button variant="primary" onClick={next}>Continue to {planName}</Button>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- Do name the outcome on the Continue button.
|
|
23
|
+
- Do say why a plan can't be chosen.
|
|
24
|
+
- Don't lift or glow a recommended plan.
|
|
25
|
+
- Don't animate prices counting up when the period changes.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Popover
|
|
2
|
+
|
|
3
|
+
Shows rich, interactive content anchored to a trigger, floating at level 3.
|
|
4
|
+
|
|
5
|
+
It grows from the trigger's edge (`transform-origin` on that side), rising 6px from 96% scale while its shadow climbs to `elev-3`. Focus moves to the first control inside; Escape or a click outside closes it and returns focus to the trigger.
|
|
6
|
+
|
|
7
|
+
**On a narrow screen.** It opens where its `side` and `align` put it, then shifts sideways to stay 8px inside the screen, and it's never wider than the screen less 16px.
|
|
8
|
+
|
|
9
|
+
**You provide:** `trigger` (a Button), children (padding is `space-4`), `side`, `align`, an `aria-label`, and `open`/`onOpenChange` if you need control.
|
|
10
|
+
|
|
11
|
+
- Do use it for small forms and pickers that belong to one control.
|
|
12
|
+
- Don't use it for a decision that blocks the page; that's a Dialog.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Progress
|
|
2
|
+
|
|
3
|
+
Shows how far along a task is: a raised fill rising out of a trench.
|
|
4
|
+
|
|
5
|
+
Determinate progress glides between values on `ease-glide` (`dur-travel-3`). Without a `value` it's indeterminate: a third-width fill travels across the track every `dur-sweep`, and stops under reduced motion.
|
|
6
|
+
|
|
7
|
+
**You provide:** `value` (0–`max`) or nothing, and an `aria-label` or a visible label nearby.
|
|
8
|
+
|
|
9
|
+
- Do switch from indeterminate to determinate as soon as you know the total.
|
|
10
|
+
- Don't use it for a reading such as storage used of a quota: that's the record, not a task, so it's a [`Meter`](../Meter/README.md).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# PromptInput
|
|
2
|
+
|
|
3
|
+
Where a person writes to an assistant or agent: a textarea that grows, attachments as chips, optional tools or a model picker, Send, and Stop while a response is generating.
|
|
4
|
+
|
|
5
|
+
**Elevation: an opening.** The whole composer is the `Input` well. Empty, its back wall shows and it deepens as the pointer nears; once it holds text its floor rises flush (Completeness and Anticipation, as `Textarea`).
|
|
6
|
+
- **Send is the level-2 primary.** With nothing to send (no text, or a file still uploading) it's flush and faint, but stays focusable (`aria-disabled`).
|
|
7
|
+
- **While generating, Send becomes Stop in the same slot and size**, held as the live action. Its icon cross-fades without travel, so nothing moves.
|
|
8
|
+
- **Attachments are `Tag` plates** above the text. One being uploaded (your `onAttach` returned a promise) hovers as pending, "Uploading". A rejection removes it and says so.
|
|
9
|
+
- **Dragging a file over** drops the well to a trench, "Drop to attach", as `DropZone` does. It opens only while something could be dropped.
|
|
10
|
+
|
|
11
|
+
**Keyboard.** Enter sends. Shift+Enter adds a line. **Enter during IME composition never sends**, because it confirms the composition (Japanese, Chinese, Korean input). Escape stops generating. ⌘U or Ctrl+U attaches. Pasting a file attaches it. After sending, focus stays in the composer.
|
|
12
|
+
|
|
13
|
+
**Accessibility.** Plain HTML: a `<form>` with a labelled `<textarea>` ("Message" unless you name it) and buttons named "Send message", "Stop generating" and "Attach files". Sending isn't announced, because the conversation announces the response. Stopping is ("Generating stopped"), and so are attachments added and removed. Under forced colours the empty well is dashed and a drop target dashed thick. Under reduced motion the composer grows without gliding.
|
|
14
|
+
|
|
15
|
+
**Parts:** `PromptInput` (`onSubmit({ text, files })`, `status` (`'ready' | 'submitted' | 'streaming' | 'error'`), `onStop`, `value`, `onValueChange`, `onAttach`, `accept`); `PromptInputAttachments`; `PromptInputTextarea` (`placeholder`, `maxHeight`); `PromptInputToolbar`; `PromptInputAttach`; `PromptInputSubmit`.
|
|
16
|
+
|
|
17
|
+
**Queueing.** With `onQueue`, Enter while the agent works queues the message instead of doing nothing: the button stays Stop, the composer empties and keeps focus, and "Message queued. It will be sent when the assistant finishes." is announced. Show the queue with `ConversationQueue`, and send the first one when the turn ends.
|
|
18
|
+
|
|
19
|
+
**Context and commands.** With `mentionable(query)`, typing @ opens a list of matches above the composer, grouped by kind (Files, Records, People), ranked as you return them; a promise shows "Searching…". Choosing one puts "@label" in the text and a plate in the composer's context row; Backspace right after it removes it whole, and its plate's remove button does the same. With `commands`, / at the start of an empty composer lists them (best match first). `onSubmit` gets `mentions` and `command` separately; the command is taken out of `text`. The text is a combobox (arrows, Enter or Tab to choose); while generating, the first Escape closes the list and only a second one stops. IME composition never opens or drives it.
|
|
20
|
+
|
|
21
|
+
**Dictation.** `PromptInputDictate` (in the toolbar) is a microphone toggle; your app supplies the speech recognition (the Web Speech API or a server transcriber) and passes `status`, `interim` and `final`. Listening is latched like a Toggle that's on (a shallow well with accent-soft poured in) with "Listening" and a steady lamp: no pulse, no waveform. Interim words are a ghost under the text in `ink-faint`, like a suggestion, never in the value; `final` words go in at the caret. ⌘D / Ctrl+D toggles from the text; Escape stops listening before it stops generating. "Listening" and "Stopped listening" are announced; interim words aren't. Blocked or unavailable stays focusable and says why.
|
|
22
|
+
|
|
23
|
+
**You provide:** sending, stopping, uploading, and the `status` of the response. Leave out `accept` and there are no attachments.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<PromptInput onSubmit={(msg) => send(msg)} status={status} onStop={stop} accept="image/*,.pdf">
|
|
27
|
+
<PromptInputAttachments />
|
|
28
|
+
<PromptInputTextarea placeholder="Ask about this workspace" />
|
|
29
|
+
<PromptInputToolbar>
|
|
30
|
+
<PromptInputAttach />
|
|
31
|
+
<Select size="sm" aria-label="Model" items={models} />
|
|
32
|
+
<PromptInputSubmit />
|
|
33
|
+
</PromptInputToolbar>
|
|
34
|
+
</PromptInput>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- Do keep Stop where Send was.
|
|
38
|
+
- Do return a promise from `onAttach` so an upload shows it's in flight.
|
|
39
|
+
- Don't send on Enter during IME composition (it's handled; don't override `onKeyDown` to send).
|
|
40
|
+
- Don't announce "Message sent".
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# PropertyList
|
|
2
|
+
|
|
3
|
+
One record's properties as label–value pairs, each editable where it sits: an issue's sidebar (status, assignee, priority, estimate), a customer's details, a settings summary, a “Check your answers” page. It's GOV.UK's summary list and Ant Design's Descriptions, with Linear-style editing: each editable value is an [InlineEdit](../InlineEdit/README.md) with any control, and “Edit all” is one [EditRegion](../EditRegion/README.md).
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
<PropertyList aria-label="Issue details" shortcuts>
|
|
7
|
+
<PropertyListItem label="Status" value={issue.status} shortcut="s"
|
|
8
|
+
renderValue={(s) => <Badge tone={toneFor(s)}>{labelFor(s)}</Badge>}
|
|
9
|
+
editor={(control) => <Select {...control} items={statuses} />} commit="on-choose"
|
|
10
|
+
onSave={(draft) => api.patch(issue.id, { status: draft }, issue.revision)}
|
|
11
|
+
onValueChange={(status) => setIssue({ ...issue, status })} />
|
|
12
|
+
<PropertyListItem label="Priority" value={issue.priority} emptyLabel="Add priority" … />
|
|
13
|
+
<PropertyListItem label="Created" value={formatDate(issue.createdAt)} /> {/* read-only: no onSave */}
|
|
14
|
+
</PropertyList>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`onSave` makes an item editable, with InlineEdit's whole contract (`editor`, `commit`, `validate`, `required`, `isEqual`, retained input on failure, conflict latching). Without it the value is read-only text.
|
|
18
|
+
|
|
19
|
+
## Elevation
|
|
20
|
+
|
|
21
|
+
- **Properties** are the record, on the ground: a `dl`, labels `ink-muted` above (`layout="stacked"`, a sidebar) or beside (`layout="inline"`, a summary, with `line` rules) their values.
|
|
22
|
+
- **An editable value** is one plate, the InlineEdit trigger, that lifts one level (`elev-1`) when pointed at, because clicking it opens its editor. Read-only values never lift, so editability shows without a pencil per row (hover is a promise).
|
|
23
|
+
- **An unset value** is a small opening: a dashed well, the empty field's treatment at plate size, naming what to add (“Add priority”), so a half-filled record reads at a glance. Setting it fills it.
|
|
24
|
+
- **Editing** opens InlineEdit's well in the value's place, under its own text, so the value doesn't move; with `commit="on-choose"` the picker's list rises from it and there's no Save. Pending, failed and accepted are InlineEdit's.
|
|
25
|
+
- **Edit all** (`editAll`) lifts the whole list into one raised `EditRegion`, for records with cross-field rules (a last day on or after the first).
|
|
26
|
+
- **A proposed change** (an item's `proposed`: an agent read an email and suggests "Stage: Qualified → Proposal", or a teammate's suggested edit) is status that asks for action, so that field is **raised one level** (as `Confirmation`). It shows the current value struck through beside the proposed one (`DiffView`'s property change), "Proposed by Assistant", and Reject and Accept. **Nothing touches the record until it's accepted.** Accepting saves through `onSave`, hovering as pending ("Saving…") and settling with the afterglow (staggered for Accept all, at most four at once). A failed save drops to a trench with the reason and Retry. A raised summary above the list counts the proposals ("3 proposed changes by Assistant") with Accept all and Reject all.
|
|
27
|
+
|
|
28
|
+
Each editable value is a button named by the label, the value and the verb (“Status: In review. Change status”), as GOV.UK's visually hidden “Change name” guidance asks, so it makes sense out of context. Each change is announced (“Status set to Done”). Proposals are a labelled group (“3 proposed changes”); each Accept and Reject names its field (“Accept new amount”), the old and new values are read as “Current” and “Proposed”, and outcomes are announced (“New amount saved”, “Rejected the new stage”). Forced colours: unset values dashed, the hover plate outlined. `contrast="more"`: the hover plate gets the level-1 ink slab.
|
|
29
|
+
|
|
30
|
+
## Keyboard
|
|
31
|
+
|
|
32
|
+
Tab moves through the editable values; Enter or Space opens one, and the editor behaves as InlineEdit (Escape cancels, focus returns to the value). With `shortcuts` on the list, an item's `shortcut` key opens it while focus is in the list and not in a field (`s` for status, as Linear does), and the value announces it with `aria-keyshortcuts`. Shortcuts are off by default: WCAG 2.1.4 asks that single-key shortcuts can be turned off or remapped, so offer a setting before turning them on.
|
|
33
|
+
|
|
34
|
+
## Props
|
|
35
|
+
|
|
36
|
+
| | |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `PropertyList` | `layout` (`'stacked'` or `'inline'`), `shortcuts`, `editAll` (`{ value, onValueChange, onSave, validate?, renderEditor, label? }`), `proposedBy`, `aria-label`/`aria-labelledby` |
|
|
39
|
+
| `PropertyListItem` | `label`, `value`, `renderValue`, `emptyLabel`; editable with `onSave`, `onValueChange`, `editor`, `commit`, `validate`, `required`, `isEqual`, `shortcut`; `proposed` and `onProposalResolved(accepted, value)` (clear `proposed` there) |
|
|
40
|
+
|
|
41
|
+
InlineEdit gains `trigger="value"` (read mode as one value plate), `emptyLabel` and `shortcut` for this; they're available on their own too.
|
|
42
|
+
|
|
43
|
+
## You provide
|
|
44
|
+
|
|
45
|
+
The record and each property's write, and for Edit all the whole-record write and its cross-field rules.
|
|
46
|
+
|
|
47
|
+
## Do and don't
|
|
48
|
+
|
|
49
|
+
- **Do** use `emptyLabel` to name what's missing (“Add due date”), not just “Not set”.
|
|
50
|
+
- **Do** keep Edit all for records with rules across fields; otherwise edit each property where it sits.
|
|
51
|
+
- **Don't** turn on shortcuts without a way to turn them off.
|
|
52
|
+
|
|
53
|
+
## Demo and verification
|
|
54
|
+
|
|
55
|
+
The `PropertyList` section of `build/previews.txt` shows an issue sidebar (status, assignee, priority unset, estimate, created) with shortcuts on, and a “Check your answers” summary with an unset phone number and Edit all. `node checks/property-list.mjs` (from `build/`; `ALT_FIXTURE_ROOT` selects React 19 fixtures) covers the `dl` structure, value buttons and their names, read-only text, unset openings, on-choose saves and announcements, filling an unset value, validation and Escape, shortcuts, Edit all with a cross-field rule and focus return, and 320/960px layouts in both schemes under reduced motion, forced colours and `contrast="more"`.
|
|
56
|
+
**Narrow.** Under 480px of its own width, an inline list stacks each label over its value, and a proposed change stacks its values, who proposed it and its Reject and Accept.
|
|
57
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Question
|
|
2
|
+
|
|
3
|
+
An agent asks the person something before it carries on: "Which environment should I deploy to?", "Keep the old column or drop it?". The person picks one or several options, or writes their own answer, and the run continues. This is the elicitation half of human-in-the-loop (MCP elicitation, Claude Code's AskUserQuestion); `Confirmation` is the approval half.
|
|
4
|
+
|
|
5
|
+
**Elevation: status that asks, until it's answered.**
|
|
6
|
+
- **Asking** is `Confirmation`'s requested card: raised at level 1 ("The assistant is asking", the question as its title). Nothing has been dispatched, so it's raised, not pending. It arrives with the afterglow.
|
|
7
|
+
- **Options** are trenches: a radio group for one, checkboxes for several, with descriptions. **Your own answer** is a field (a well).
|
|
8
|
+
- **Answer** is the one primary; **Skip** (`skipLabel`) is a ghost button.
|
|
9
|
+
- **Answered**, it presses to the ground as one muted line in the record: "Which layout…? · You chose: Compact · 3 minutes ago". **Expired** says "No longer needed".
|
|
10
|
+
|
|
11
|
+
**Accessibility.** A section labelled by its question, holding a radio group (arrows) or a group of checkboxes; not a dialog. There's no default answer and nothing is focused for you; the composer keeps focus. Asked, it's announced assertively once ("The assistant is asking: …"), because the run is waiting on it; answered, politely ("Answered: Staging"). Enter in your own answer doesn't submit.
|
|
12
|
+
|
|
13
|
+
**Props:** `Question`: `title`, `state` (`'asking' | 'answered' | 'expired'`), `multiple`, `allowOther`, `otherLabel`, `answerLabel`, `skipLabel`, `onSkip`, `onAnswer({ values, other })`, `answer`, `answeredAt`, `requestedBy`, `name` (its key in a set). `QuestionOption`: `value`, `label`, `description`. `QuestionSet`: `state`, `onAnswer(answers by name)`, `answerLabel`, `skipLabel`, `onSkip`: several questions answered by one button.
|
|
14
|
+
|
|
15
|
+
**You provide:** the options, what answering does, and the answer to show once it's answered.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<Question title="Which environment should I deploy to?" state={q.state} answer={q.answer} allowOther
|
|
19
|
+
answerLabel="Deploy" skipLabel="Let the assistant decide" onAnswer={(a) => respond(q.id, a)} onSkip={() => skip(q.id)}>
|
|
20
|
+
<QuestionOption value="staging" label="Staging" description="Resets nightly. Safe to try things." />
|
|
21
|
+
<QuestionOption value="production" label="Production" description="Customers will see it immediately." />
|
|
22
|
+
</Question>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Do describe what each option means for the run.
|
|
26
|
+
- Do offer a way to let the assistant decide when a default exists.
|
|
27
|
+
- Don't preselect an option or move focus to the question.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# RadioGroup
|
|
2
|
+
|
|
3
|
+
Chooses exactly one option. A radio group is built like the Switch with more positions: **one switch, several positions, one thumb**.
|
|
4
|
+
|
|
5
|
+
Each option is a round trench, the same as the switch track. The group owns a single raised thumb; choosing an option slides it to that track on `ease-settle`, and the accent pours into the track from under the thumb as it arrives while the old one drains. Because there's only one thumb, the choice can't be in two places, and its movement shows where it came from. Hovering an unchosen option shows a faint ghost of the thumb where it would land (feedforward); hovering the chosen option lifts the thumb; pressing it stretches the thumb, like a switch. Arrow keys move the choice, as native radios do, and the thumb follows.
|
|
6
|
+
|
|
7
|
+
**You provide:** `Radio` children with `value`, `label` and optional `description`; `value`/`onValueChange` or `defaultValue`; an `aria-label` for the group if there's no visible heading; `orientation="horizontal"` for two or three short options.
|
|
8
|
+
|
|
9
|
+
- Default to a sensible choice; a group with no thumb is rarely what you want.
|
|
10
|
+
- Don't use radios for more than about six options; use a menu.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# RelativeTime
|
|
2
|
+
|
|
3
|
+
A moment shown as "3 minutes ago", for activity feeds, audit logs, comments and "last edited". `formatRelativeTime()` gives the same words as a string.
|
|
4
|
+
|
|
5
|
+
**Elevation.** A timestamp is part of the record, so it's plain text on the ground. **Ticking forward is not a change to the record:** "3 minutes ago" becoming "4 minutes ago" swaps the words with no transition and no afterglow. When the moment itself changes (the record was edited again), that is a change, and the time takes the same ground-level tint as a changed `FormattedNumber`, decaying over `dur-settle`.
|
|
6
|
+
|
|
7
|
+
**Refreshing.** Every RelativeTime on the page shares one timer (every 15 seconds). It stops while the page is hidden and catches up as soon as the page is visible again. Words come from `Intl.RelativeTimeFormat` with `numeric: "auto"`, so you get "now", "yesterday" and "last month", pluralised for the locale.
|
|
8
|
+
|
|
9
|
+
**Formats.** `relative` (default) always says how long ago or until. `auto` is relative within `threshold` (an ISO 8601 duration such as `"P7D"` or `"PT12H"`, or milliseconds; default 7 days), then a date ("Mar 4", or "Mar 4, 2025" in another year). `tense="past"` never says "in 2 minutes", even when a client's clock runs ahead, and `tense="future"` never says "ago".
|
|
10
|
+
|
|
11
|
+
**Accessibility.** It renders a `<time dateTime="…ISO…">`. With `tooltip` (on by default) it's focusable, and the exact date, time and zone ("Wednesday, October 29, 2026 at 3:04:12 PM EDT") is shown in a `Tooltip` on hover and focus and attached with `aria-describedby`. With `tooltip={false}` the exact time is its `title`. Ticking is never a live region. Under forced colours a new moment's tint is a `Highlight` outline.
|
|
12
|
+
|
|
13
|
+
**Props:** `date` (a `Date`, ISO string or epoch ms); `format` (`'relative' | 'auto'`); `threshold`; `tense` (`'auto' | 'past' | 'future'`); `locale`; `tooltip` (default true).
|
|
14
|
+
|
|
15
|
+
**You provide:** the moment, and whether old moments should become dates (`format="auto"`).
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<RelativeTime date={event.createdAt} /> // "3 minutes ago"
|
|
19
|
+
<RelativeTime date={due} format="auto" threshold="P7D" /> // relative within 7 days, then "Mar 4"
|
|
20
|
+
<RelativeTime date={d} locale="fr" tense="past" /> // "il y a 3 minutes"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- Do use `format="auto"` in logs and feeds that reach back further than a week.
|
|
24
|
+
- Do keep the tooltip on wherever the exact time matters (audit logs, billing).
|
|
25
|
+
- Don't put a RelativeTime in a live region, or announce it ticking.
|
|
26
|
+
- Don't format times by hand with string concatenation.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# RemovalReceipt
|
|
2
|
+
|
|
3
|
+
The slot of an item that can leave its list: a file archived, a webhook removed, an invitation revoked. When the item goes, its result shows in its own slot, at the height it had, so nothing on the page moves while it's read. The slot holds, lifts its receipt off, and only then glides the gap closed. Use it instead of removing the row at once and adding an Alert below the list, which reshapes the page twice and puts the feedback away from where the person acted.
|
|
4
|
+
|
|
5
|
+
**Left in place, then closed.** With `removed`, the item's content cross-fades (`dur-travel-2`) to the receipt: a check (or, with `tone="critical"`, an alert icon), your `title`, and your `action`. While `pending`, the write isn't confirmed: the receipt shows a spinner in place of the check, the slot hovers as pending only after `dur-grace` (a fast write never flickers), and the hold hasn't started. Once confirmed, the receipt holds for `dur-linger` (2000ms), then lifts off as an Alert does (8px, in 70% of `dur-travel-3`, `ease-sink`), and the gap glides shut (`dur-travel-3`, `ease-glide`). The rows below are pushed by the height, not animated on their own. `onClosed` is called once the gap has closed: drop the item from your list there.
|
|
6
|
+
|
|
7
|
+
**The hold waits for the person.** It pauses while the receipt is pointed at or focused, and while a modal owns the document. When it's due and a mouse is still over the list (the slot's parent), it waits for the pointer to leave, up to `dur-settle`, so the next row doesn't slide under a pointer reaching for it. Touch has no hover, so on touch it closes when the hold ends. Receipts due within 250ms of each other close together, as one change.
|
|
8
|
+
|
|
9
|
+
**Why it's on the ground.** The receipt is a trace of what happened to the record, not a live surface, so it doesn't rise for attention: muted ink on the ground. Only its exit lifts off. Pending uses the shared pending signal (`surface-lifted`, `elev-2`, breathing over `dur-hover`). An item brought back from elsewhere (`arrive`) glides its slot open and settles with an afterglow; an item restored while its receipt is still held (`removed` back to false) returns in place with an afterglow.
|
|
10
|
+
|
|
11
|
+
**Accessibility.** The receipt's text is a polite live region (`role="status"`) present from the slot's first render, so the result is announced when it appears. Put Undo in `action` and name it fully (`aria-label="Undo archive of Brand guidelines.pdf"`). Before the slot closes, focus inside it moves to the first focusable control of the next slot, else the previous one, else `focusFallback()`. Under reduced motion the receipt still fades and holds, nothing travels, and the gap closes at once. Under forced colours the receipt is outlined in `CanvasText`.
|
|
12
|
+
|
|
13
|
+
**Props:** `removed`; `pending`; `title`; `action`; `tone` (`neutral` | `critical`); `icon`; `hold` (ms, default `dur-linger`); `onClosed`; `focusFallback`; `arrive`; `settle`; `as` (`div` | `li` | `section` | `article`, default `div`). Other props go to the slot's element, and the item's own content (`children`) keeps the slot's layout (its wrapper is `display: contents`).
|
|
14
|
+
|
|
15
|
+
**You provide:** the item, whether it's removed and whether that's confirmed, the words and the actions, and what happens next. Undo has to call your real inverse operation. After the slot closes, offer Undo in a `toast` if you like, but keep a durable recovery route (Archived → Restore), and withdraw a toast's Undo (`toast.dismiss(id)`) once a newer revision makes it invalid. See [Actions in place](../../visual/ActionsInPlace.md) for the full recipe: pending, failure, conflict, late receipts and focus.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<ul>
|
|
19
|
+
{files.map((file) => (
|
|
20
|
+
<RemovalReceipt as="li" key={file.id} removed={!!receipts[file.id]} pending={receipts[file.id]?.pending}
|
|
21
|
+
title={`${file.name} archived.`}
|
|
22
|
+
action={<Button size="sm" aria-label={`Undo archive of ${file.name}`} onClick={() => restore(file)}>Undo</Button>}
|
|
23
|
+
onClosed={() => { dropReceipt(file.id); toast({ title: `${file.name} archived.`, action: { label: 'Undo', onClick: () => restore(file) } }); }}>
|
|
24
|
+
<FileRow file={file} onArchive={() => archive(file)} />
|
|
25
|
+
</RemovalReceipt>
|
|
26
|
+
))}
|
|
27
|
+
</ul>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- Do keep the item in its slot until its removal is confirmed; show a failure in the restored row, not somewhere else.
|
|
31
|
+
- Do put Undo where the removing action was, so a second click under the pointer reverses rather than navigates.
|
|
32
|
+
- Don't hold the receipt for longer to "make sure": the person can stop it by pointing at it or focusing it.
|
|
33
|
+
- Don't use it for a line removed while typing (an empty `Checklist` line); nothing was lost, and the hold would sit in the way. Checklist glides the line shut instead.
|
|
34
|
+
- Don't use it for permanent deletion without saying so: a deletion's receipt has no Undo.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Resizable
|
|
2
|
+
|
|
3
|
+
Panes side by side, with a divider you drag: an inbox, a ticket queue, a file browser, a record beside its list, an agent's chat beside its artifact.
|
|
4
|
+
|
|
5
|
+
**Elevation.** The panes are settled, on the ground. **They never lift**: a detail pane opening beside its list stays on the ground, unlike a `Sheet`, which lays a task over the page. The divider is 1px of `line`. Only the grip is live:
|
|
6
|
+
- At rest it's a small `line-control` plate, flush.
|
|
7
|
+
- Pointed at or focused, it lifts one level.
|
|
8
|
+
- **While held, it floats at level 3** ("whatever is in a hand floats").
|
|
9
|
+
|
|
10
|
+
Collapsing a pane changes its size on `ease-glide`, not its level.
|
|
11
|
+
|
|
12
|
+
**Sizes** are percentages of the group. Panels without a `defaultSize` share what's left. Dragging and the keys keep each pane between its `minSize` (default 10) and `maxSize` (default 90). `autoSaveId` remembers the layout in this browser, and `onLayout` reports it.
|
|
13
|
+
|
|
14
|
+
**Accessibility.** The [window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/). Each handle is `role="separator"`, focusable, with `aria-valuenow`, `aria-valuemin` and `aria-valuemax` (the pane before it, in percent) and `aria-controls` naming that pane. It's named "Resize conversations" from the pane's label. Left and Right (or Up and Down when vertical) resize by 5% (10% with Shift), and Home and End go to the limits. **Enter collapses a `collapsible` pane and restores it.** A collapsed pane is inert and hidden from assistive technology, and its handle says "Conversations collapsed". Each pane is a named `region`. Under forced colours the divider is `CanvasText` and the grip has a 1px border (2px while held). Under `contrast="more"` the divider is `line-control` and a held grip gets a hard slab. Under reduced motion collapsing doesn't animate.
|
|
15
|
+
|
|
16
|
+
**Parts:**
|
|
17
|
+
- `ResizablePanelGroup`: `direction` (`'horizontal' | 'vertical'`), `autoSaveId`, `onLayout`.
|
|
18
|
+
- `ResizablePanel`: `defaultSize`, `minSize`, `maxSize`, `collapsible`, `aria-label`.
|
|
19
|
+
- `ResizableHandle`: `withGrip` (default true), `label`.
|
|
20
|
+
|
|
21
|
+
**Narrow.** Under 480px of its own width, `narrow="stack"` (the default) shows one pane at a time: `narrowPanel` picks it (default the first), and `onNarrowChange` tells you when to show a way back, the list-and-detail pattern on phones. `narrow="vertical"` stacks the panes with a horizontal grip instead, and `narrow={false}` keeps them side by side.
|
|
22
|
+
|
|
23
|
+
**You provide:** the panes and their names, and sensible limits.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<ResizablePanelGroup direction="horizontal" autoSaveId="inbox">
|
|
27
|
+
<ResizablePanel defaultSize={35} minSize={20} collapsible aria-label="Conversations">{list}</ResizablePanel>
|
|
28
|
+
<ResizableHandle />
|
|
29
|
+
<ResizablePanel aria-label="Conversation">{detail}</ResizablePanel>
|
|
30
|
+
</ResizablePanelGroup>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- Do name every pane, so its handle has a name.
|
|
34
|
+
- Do set a `minSize` that keeps a pane usable.
|
|
35
|
+
- Don't use it for a task that sits on top of the page; that's a `Sheet`.
|
|
36
|
+
- Don't raise a pane into a card.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# SaveBar
|
|
2
|
+
|
|
3
|
+
Unsaved changes on a settings page or a long record form, held above the page until they're saved or discarded. People scroll past a Save button, navigate away and lose work, or don't notice there's anything unsaved. Polaris's contextual save bar solves the same problem.
|
|
4
|
+
|
|
5
|
+
**Elevation: an unsaved draft at page scope is something in your hand.** "Editing is temporary local input; it isn't a pending write." So the bar floats at level 3, docked to the bottom edge. That's below a `Sheet` or `Dialog`, so it never competes with them.
|
|
6
|
+
- **It appears once a change has settled into the draft** (600ms without another), not on the first key: "spend rises carefully".
|
|
7
|
+
- **Save** turns it into a commitment: it hovers as a pending write ("Saving…"). Saved, it lowers away while the saved fields settle with the afterglow.
|
|
8
|
+
- **A failed save drops the bar to a trench** ("We couldn't save. Your changes are kept.") with Retry, and keeps the draft.
|
|
9
|
+
- **Invalid** says "2 fields need attention". Save stays enabled, so pressing it shows the errors.
|
|
10
|
+
|
|
11
|
+
**With useEditRecord.** `<SaveBar record={record} />` reads `dirty`, `phase`, `errors` and the changed field count from a `useEditRecord` result, and its buttons call `submit` and `reset`. Leaving while dirty uses `useEditRecord`'s own leave handling. Without a record, pass `dirty`, `saving`, `error`, `invalidCount`, `changedCount`, `onSave` and `onDiscard`.
|
|
12
|
+
|
|
13
|
+
**Accessibility.** A region labelled "Unsaved changes". It doesn't take focus when it appears, and "Unsaved changes" is announced once when the draft first becomes dirty, not on every key. ⌘S or Ctrl+S saves while it's shown. Outcomes ("Saved", or the failure) are announced politely. Under forced colours it has a 2px border, dotted while saving and a thick dash when failed. Under `contrast="more"` it gets a hard slab. Under reduced motion it fades in and out without travelling.
|
|
14
|
+
|
|
15
|
+
**Props:** `record`, or `dirty`, `saving`, `error`, `invalidCount`, `changedCount`, `onSave`, `onDiscard`; `delay` (default 600); `saveLabel`, `discardLabel`.
|
|
16
|
+
|
|
17
|
+
**You provide:** the record or its state, and the save.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
const record = useEditRecord({ initialValue: settings, validate, validationMode: 'submit', save: saveSettings });
|
|
21
|
+
<SaveBar record={record} />
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- Do use it for pages that collect several changes before one save.
|
|
25
|
+
- Do keep the draft when a save fails.
|
|
26
|
+
- Don't use it beside an `EditRegion`, which has its own Save.
|
|
27
|
+
- Don't show it on the first keystroke.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SavedViews and useSavedViews
|
|
2
|
+
|
|
3
|
+
Named combinations of a filter, a sort and the columns, kept as tabs above a table: "My open deals", "Enterprise churn risk", "Unassigned this week".
|
|
4
|
+
|
|
5
|
+
**Elevation.** The views are folder `Tabs`: the active tab and the sheet beneath it, which holds the `FilterBar` and the `DataTable`, are one raised piece. **A view you've changed is a draft.** Its tab carries an `accent` dot and "modified" in its name, and Save view (secondary), Save as new view and Reset appear beside the tabs. That's the `SaveBar`'s reading, local to the tab. Switching views uses what's already there: the table's re-sort glide and the filter's relevance motion.
|
|
6
|
+
|
|
7
|
+
**The hook.** `useSavedViews({ views, defaultViewId, onSave, onCreate })` holds each view's draft (its filters, match, sort, column visibility and order). Switching away and back doesn't lose work. `modified` says when a draft differs from what's saved. Spread `filterBarProps` onto `FilterBar` and `tableProps` onto `DataTable`. `save()` hands `onSave` the view with its changes, `saveAsNew(name)` hands `onCreate` a new view, and `reset()` drops the draft. You store the views.
|
|
8
|
+
|
|
9
|
+
**Accessibility.** The tabs are `Tabs` (`aria-selected`). "Modified" is part of a changed tab's name, and announced once when the view first diverges. Save as new view asks for a name in a `Popover` with a labelled field. Below 640px the actions sit above the tabs. Under forced colours the dot is `CanvasText`.
|
|
10
|
+
|
|
11
|
+
**Props.** `useSavedViews(options)` returns `SavedViewsApi`. `SavedViews`: `views` (the API), `children` (the filters and table), `aria-label`. `SavedView` is `{ id, name, filters?, match?, sort?, columnVisibility?, columnOrder? }`.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
const views = useSavedViews({ views: saved, onSave, onCreate });
|
|
15
|
+
<SavedViews views={views} aria-label="Deal views">
|
|
16
|
+
<FilterBar fields={fields} {...views.filterBarProps} />
|
|
17
|
+
<DataTable columns={columns} rows={rows} getRowId={(r) => r.id} {...views.tableProps} />
|
|
18
|
+
</SavedViews>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- Do let people save a view under their own name.
|
|
22
|
+
- Do keep the default view ("All deals") unfiltered.
|
|
23
|
+
- Don't save a view on every change; a draft is saved when someone says so.
|
|
24
|
+
- Don't use segmented tabs; the views own the table beneath them, so they're folder tabs.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# ScatterChart
|
|
2
|
+
|
|
3
|
+
Two measures per record, to see whether they move together and to spot the ones that don't: response time against payload per endpoint, spend against seats per customer. It's the form for relationships, and nothing else does it. It renders inside a [`ChartContainer`](../ChartContainer/README.md), whose `config` names and formats the two measures.
|
|
4
|
+
|
|
5
|
+
**Why it's flat.** Dots are the record, so they lie on the plot: at least 8px (squares and triangles are drawn a little larger so they're 8px too), a `data-*` fill, and a 2px ring of the plot's own colour, so dots that overlap stay apart. No shadow, no sheen, no glow, no lift.
|
|
6
|
+
|
|
7
|
+
**Groups.** `group` sorts records into at most three groups, each a colour (`data-1` to `data-3`) and a marker shape (circle, square, triangle). It's three because any two dots can be neighbours, so every pair of colours has to stay apart for colour-blind readers, and the palette's first three slots are the ones validated that way. With more than three it warns, and the rest share `data-other`: show them as small multiples instead. A `ChartLegend` keys each group by its marker, and with `toggle` hides and shows groups. A hidden group leaves the plot and the keyboard path.
|
|
8
|
+
|
|
9
|
+
**Both axes are measures**, so neither has to include zero, both start and end on a tick, and each is named: y over its axis, x under its own.
|
|
10
|
+
|
|
11
|
+
**Labels and trend.** `label={(d) => d.p95 > 400 ? d.name : null}` names a few dots in `ink` on a halo of the plot's colour, with a 1px `ink-muted` leader. Each label goes to the first spot that clears the plot's edges and the words already placed: above right, above left, beside, then below. A label with nowhere to go is left out, and is still in the summary and the readout. `trend` draws the least-squares line as 1px `ink-muted`, named "Trend" (or the words you pass) at its right end.
|
|
12
|
+
|
|
13
|
+
**Reading a dot.** There's no crosshair: the pointer reads the nearest dot within 48px, the Voronoi cell it's in, so a dense cloud is still easy to read. The readout sits beside the dot and gives its name, both measures and its group, keyed by the group's colour. The dot under it is drawn again on top, so a dot that's overlapped can be seen whole.
|
|
14
|
+
|
|
15
|
+
**Without colour.** Under forced colours the dots are `CanvasText` ringed in `Canvas`, and the shapes carry the groups. With `contrast="more"` each dot is ringed in 1px `ink`.
|
|
16
|
+
|
|
17
|
+
**Motion.** With a stable `id`, dots glide to their new places when the data changes, at one level on `ease-glide`, after any rescale. Without one, a record has no identity to follow, so the plot cross-fades instead ([Motion](../../visual/Motion.md), "Charts").
|
|
18
|
+
|
|
19
|
+
**Accessibility.** The keyboard path is one Tab stop. Enter goes from the summary to the leftmost dot. ← and → step through the dots in x order, ↑ and ↓ go to the nearest dot above or below, Home and End go to the first and last along x, and Page Up and Page Down move a tenth of the way. That's after Data Navigator (Elavsky, Nadolskis & Moritz, 2023). Each dot is named "Checkout list, Checkout. Payload 120 KB, p95 180 ms." Without a written `summary`, the generated one says how the measures relate ("p95 rises strongly with Payload (r = 0.78)"), their ranges, and the labelled dots. The table view has a row per record: its name, its group and both measures.
|
|
20
|
+
|
|
21
|
+
**Props:** `data`, `x`, `y`; `group`; `id` and `name` (a key or a function); `label`; `trend`; `size`; `tooltip`.
|
|
22
|
+
|
|
23
|
+
**Selecting, drilling and filtering.** `selected` (the record or its id) and `onSelect`, `drill`, and `relevance` (with `rescale`) work as on every chart: the chosen dot keeps its colour, the rest recede to 35%, and the frame says "Filtered to Vandelay". See [ChartContainer](../ChartContainer/README.md).
|
|
24
|
+
|
|
25
|
+
**Focus.** `focus={['Search']}` keeps those groups' colours (and every shape) and draws the rest in `data-context`; `focus={(d) => d.p95 > 250}` picks out dots instead. See [ChartContainer](../ChartContainer/README.md), "Focus".
|
|
26
|
+
|
|
27
|
+
**You provide:** a `ChartContainer` whose `config` names and formats both measures, the records, and usually a `summary` that says what the relationship means.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<ChartContainer config={{ kb: { label: 'Payload', format: (v) => `${v} KB` }, ms: { label: 'p95', format: (v) => `${v} ms` } }}>
|
|
31
|
+
<ScatterChart data={endpoints} x="kb" y="ms" group="service" id="path" name="path" label={(d) => (d.ms > 400 ? d.path : null)} trend>
|
|
32
|
+
<ChartLegend content={<ChartLegendContent toggle label="Services" />} />
|
|
33
|
+
</ScatterChart>
|
|
34
|
+
</ChartContainer>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- Do give records a stable `id` if the data updates, so dots glide instead of blinking.
|
|
38
|
+
- Do label only the dots worth a word; the rest are in the readout and the table.
|
|
39
|
+
- Do say what a correlation means in the `summary`; don't let a trend line stand in for a cause.
|
|
40
|
+
- Don't use more than three groups; split into small multiples.
|
|
41
|
+
- Don't size dots by a third measure; it's two measures per record, read by position.
|