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,35 @@
|
|
|
1
|
+
# ChartLegend
|
|
2
|
+
|
|
3
|
+
Names each series, and optionally lets people hide and show series. Every chart with two or more series needs one, unless its marks are labelled directly (a `LineChart` with up to four series names each line at its end). A single series doesn't need one: the title names it.
|
|
4
|
+
|
|
5
|
+
**Why it's flat, or why it lifts.** A legend that only labels is settled: keys flush on the ground, each a swatch in its series colour (a block for bars, a short line for lines, the group's marker for a `ScatterChart`) and its name in `ink`. A legend you can toggle is a row of choices, and in most chart libraries those are invisible affordances: the key looks like a label and behaves like a button, which is the flat-UI failure Burmistrov et al. (2015) measured. So with `toggle` each key is built like a `Checkbox`. It's a ghost button that lifts one level when pointed at, because clicking it does something. It holds a trench (sunk, `surface-trench`) that the series colour pours into while the series is shown and drains out of when it's hidden. The fill is the series colour rather than `accent`, because the swatch *is* the key.
|
|
6
|
+
|
|
7
|
+
**What toggling does.** The series fades out of (or into) the plot over `dur-travel-1`. If y has to rescale, that's staged first, as every change to a chart is ([Motion](../../visual/Motion.md), "Charts"). Hidden series keep their colour slots, so nothing else repaints. The last shown series can't be hidden; its key says "At least one series stays shown."
|
|
8
|
+
|
|
9
|
+
**Without height.** Under forced colours, swatches take each series' pattern, hidden swatches are dashed, and a hovered key gets a 1px border. With `contrast="more"` the trench gets a solid inner top edge.
|
|
10
|
+
|
|
11
|
+
**Accessibility.** A static legend is a list named "Series". A toggle legend is an APG toolbar named "Series" holding `aria-pressed` buttons, one per series, so a long legend is one Tab stop: ←/→ (or ↑/↓) move between keys, Home and End go to the ends, and Space or Enter toggles. Toggling is announced politely ("Costs hidden. 2 of 3 series shown.").
|
|
12
|
+
|
|
13
|
+
**Props.** `ChartLegend` takes `content`: a `ChartLegendContent` with these options:
|
|
14
|
+
|
|
15
|
+
- `toggle`: make the keys switches;
|
|
16
|
+
- `hidden` and `onHiddenChange`, controlled; or `defaultHidden`, uncontrolled;
|
|
17
|
+
- `label`: the toolbar's name, default "Series".
|
|
18
|
+
|
|
19
|
+
**You provide:** a `ChartLegend` inside the chart type or the container.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<ChartContainer config={config}>
|
|
23
|
+
<AreaChart data={usage} x="day" stack>
|
|
24
|
+
<ChartLegend content={<ChartLegendContent toggle />} />
|
|
25
|
+
</AreaChart>
|
|
26
|
+
</ChartContainer>
|
|
27
|
+
|
|
28
|
+
// controlled
|
|
29
|
+
<ChartLegendContent toggle hidden={hidden} onHiddenChange={setHidden} />
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- Do label lines directly when there are four or fewer, and keep the legend for toggling.
|
|
33
|
+
- Do use `toggle` when people need to compare a few series out of many; otherwise a static legend is quieter.
|
|
34
|
+
- Don't let colour be the only link between a key and its series: under forced colours the keys show each series' pattern.
|
|
35
|
+
- Don't put the legend somewhere the chart's readout or summary can't be read alongside it.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# ChartReference and ChartAnnotation
|
|
2
|
+
|
|
3
|
+
Reading a chart against something: an SLO target, a budget, a normal range, the average, the day you shipped v2. Overlays like these help people read charts (Kong & Agrawala 2012), and most dashboards need at least a target line and an event marker. Put them inside any cartesian chart type, or inside the container:
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
<LineChart data={uptime} x="day">
|
|
7
|
+
<ChartReference y={[0.995, 1]} label="Normal range" />
|
|
8
|
+
<ChartReference y={0.999} label="Target 99.9%" />
|
|
9
|
+
<ChartReference y={0.99} label="Limit 99%" tone="critical" />
|
|
10
|
+
<ChartReference y="mean" label="Average" />
|
|
11
|
+
<ChartAnnotation x="2026-03-14" label="DNS outage" />
|
|
12
|
+
</LineChart>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Why they're flush.** References and annotations are part of the reading, not something to act on, so they lie on the ground with the marks:
|
|
16
|
+
|
|
17
|
+
| Part | Drawn as | Words |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Reference line | 1px `ink-muted`, solid, over the marks. Dashes mean "projected", so a reference line never has them | its label at the right end, above the line, in caption text; below it, at the left end or further along when that spot would cover a mark |
|
|
20
|
+
| Status threshold (`tone`) | 1px `caution` or `critical` | the label in `ink`, with the status icon in its tone |
|
|
21
|
+
| Band (`y={[low, high]}`) | a flat `line` fill under the marks: a decorative token, so it isn't read as data or as an opening | its label inside the band, at whichever corner covers no mark, or beside it when the band is under 20px tall |
|
|
22
|
+
| Annotation | a `Badge` plate flush at the top of the plot, in a row of headroom above the data (a label is part of the record), with a 1px `line-control` leader down to its x | the plate's text |
|
|
23
|
+
| Annotation with `onSelect` | the plate is a button and lifts one level when pointed at | button semantics, named "v2 launch, Mar 4, 2026" |
|
|
24
|
+
|
|
25
|
+
**Labels stay clear, of each other and of the data.** The container draws every reference and annotation itself, so it can lay their words out together with the marks. Plates sit in rows 26px apart at the top of the plot, and the data starts below the last row, so a plate never covers a mark. A reference's label takes the first spot that covers no line, area or bar and no other word: the right end above its line, then below, then the left end, then along the line. On a plot too small for any of those, it takes the spot that covers least. Across a horizontal chart, references stand upright and their labels stack in a row above the bars. All labels are on a halo of the plot's colour.
|
|
26
|
+
|
|
27
|
+
**Values.** A reference's own values count toward the y axis, so a target above every value is still on the plot. `y="mean"` and `y="median"` are read from the settled records of `series` (default: the first shown), and the label adds the value at the data's own precision: "Average 162.5 ms".
|
|
28
|
+
|
|
29
|
+
**Motion.** References are drawn in the plot's coordinates, so they glide with a rescale and the marks ([Motion](../../visual/Motion.md), "Charts"), and a mean or median glides to its new value when the data changes. Under reduced motion they change in place.
|
|
30
|
+
|
|
31
|
+
**Without colour.** Under forced colours lines, labels, icons and leaders are `CanvasText`, bands are hatched in `GrayText` on `Canvas`, and plates get a 1px `CanvasText` border.
|
|
32
|
+
|
|
33
|
+
**Accessibility.** References and annotations are read after the chart's summary, written or generated, and on the keyboard path's summary node: "References: Normal range, 99.5% to 100%; Target 99.9% at 99.9%. 1 annotation: DNS outage, Mar 14, 2026." Plates that only label are hidden from assistive technology, since the summary lists them. Plates with `onSelect` are buttons in the tab order after the plot.
|
|
34
|
+
|
|
35
|
+
**Props.** `ChartReference`: `y` (a value, `[low, high]`, `'mean'` or `'median'`), `label`, `tone` (`'caution'` or `'critical'`), `series`. `ChartAnnotation`: `x` (as the data's x values are written), `label`, `onSelect`.
|
|
36
|
+
|
|
37
|
+
- Do say what a reference is in its label ("Target 99.9%"), not only its value.
|
|
38
|
+
- Do use `tone` only when crossing the line means good or bad.
|
|
39
|
+
- Do keep annotations to a few words; the plate is a label, not a note.
|
|
40
|
+
- Don't dash a reference line; a dash means "projected".
|
|
41
|
+
- Don't use a reference band for data. A range in the data is an uncertainty band (`LineChart`'s `band`).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# ChartTooltip
|
|
2
|
+
|
|
3
|
+
Reading exact values off a chart. Point at a date on a line chart, or at a bar, and a hairline marks that x while a readout lists every series' value there, so you never have to land on a 2px line to get a number. It's on for every chart; `ChartTooltip` only changes what it shows.
|
|
4
|
+
|
|
5
|
+
**Why it floats.** The readout is live: it exists while you're reading, so it's a level-3 floating surface like `Tooltip` (`surface-floating`, `elev-3`, `radius-lg`). The crosshair and the ringed points are marks on the plot, on the ground: a 1px `line-control` rule snapped to the nearest x, and each line's point there ringed in the plot's own surface colour (an outline, not height). Bars don't change at all, because hovering a static bar does nothing.
|
|
6
|
+
|
|
7
|
+
**How it moves.** It rises 4px the first time, as its shadow grows from `elev-1` to `elev-3` over `dur-travel-3`, then doesn't rise again while you move. It steps from one x to the next with no easing: it follows the data, not each pixel of the pointer, so it never reads as a control drifting (E10). It opens on whichever side of the point has room, so it never covers it, and leaves in 70% of its entry time. Coming back within 400ms shows it at once, warm, shared with `Tooltip`. On touch, press and hold shows it, dragging scrubs along x, and lifting your finger leaves it until you tap elsewhere. Under reduced motion it fades with no travel.
|
|
8
|
+
|
|
9
|
+
**Without height.** Under forced colours it has a 2px `CanvasText` border and its keys take each series' dash. With `contrast="more"` it stands on the hard ink slab every level-3 surface gets.
|
|
10
|
+
|
|
11
|
+
**Accessibility.** The readout is `aria-hidden` decoration over data that's reachable elsewhere: the chart's summary, the keyboard path through the data, which opens the same readout on the focused record (#63), and the table view (#64). Its labels come from your data, so they're inserted as text, never as HTML.
|
|
12
|
+
|
|
13
|
+
**Rows.** The heading is the x value (formatted by the container, or by `labelFormat`). Each row puts the value first (`ink`, weight 500, tabular figures) and the series name second (`ink-muted`), keyed by a short stroke of the series colour. Only series that are shown and have a value there are listed.
|
|
14
|
+
|
|
15
|
+
**Props.** `ChartTooltip` takes `content`: a `ChartTooltipContent` with your options. `ChartTooltipContent` takes:
|
|
16
|
+
|
|
17
|
+
- `indicator`: `'line'` (default), `'dot'` or `'dashed'`;
|
|
18
|
+
- `labelFormat`, for the heading;
|
|
19
|
+
- `hideLabel` and `hideIndicator`.
|
|
20
|
+
|
|
21
|
+
Turn the readout off with `tooltip={false}` on the chart type or the container. From your own marks or keyboard code, `useChart().active` is the record being read and `setActive(index)` opens the readout on one.
|
|
22
|
+
|
|
23
|
+
**Pinning a reading.** `pinnable` on a `LineChart`, `BarChart` or `AreaChart` lets people keep a number: with the readout open, a click on the plot (or P on the keyboard path, or a quick tap while the touch readout is open) sets the reading down. The live readout lands on the record as a `Badge` plate at the top of the plot, from level 3 to the ground over `dur-travel-3` on `ease-settle`, then takes the afterglow once. It has a pin icon, so it isn't read as a `ChartAnnotation`, a 1px `line-control` leader to its x, and a ghost remove button named "Remove pin, Mar 9, 2026". With two pins, a strip under the x axis says the difference ("+24% over 28 days", or per series), and pinning the second says it aloud ("Apr 6, 2026 is 52% higher than Mar 2, 2026."). `pinnable` allows two; a number allows that many, and a new pin replaces the oldest. Escape on the chart removes the last pin before it clears a selection. Where a click selects or drills, it keeps doing that and only P pins. `pins` and `onPinsChange` control them, to keep them in the URL. The readout's action line says how to pin ("Click to pin", "P to pin", "Tap to pin").
|
|
24
|
+
|
|
25
|
+
**You provide:** nothing, for the default. To change it, a `ChartTooltip` inside the chart type or the container.
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
<ChartContainer config={config}>
|
|
29
|
+
<LineChart data={months} x="month">
|
|
30
|
+
<ChartTooltip content={<ChartTooltipContent indicator="dot" labelFormat={(m) => monthLong(m)} />} />
|
|
31
|
+
</LineChart>
|
|
32
|
+
</ChartContainer>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- Do keep it on: it's how people get exact numbers without hunting for a line.
|
|
36
|
+
- Do format the heading the way people say the x ("Week of Mon, April 6"), not as stored.
|
|
37
|
+
- Don't put anything in it that can't be reached another way: it's a convenience over the summary, the keyboard path and the table.
|
|
38
|
+
- Don't add buttons or links to it; it doesn't take the pointer, and it moves away as soon as the pointer does.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Checkbox
|
|
2
|
+
|
|
3
|
+
Toggles one independent option. A checkbox is built like the Switch and the RadioGroup, so the three read as one family.
|
|
4
|
+
|
|
5
|
+
- **Off:** an empty 22px trench (`surface-trench`, `trench` shadow, `line-control` rim), like the uncovered part of a switch track or an unchosen radio.
|
|
6
|
+
- **On:** the accent pours in from the centre (on `ease-settle`, as the switch fill follows its thumb), and since the fill covers the rim too, fill and outline are one shape. A **raised white check** is set on it: `on-accent` with a small drop shadow, the checkbox's equivalent of the switch thumb. It draws once the fill has landed. `indeterminate` sets a dash instead.
|
|
7
|
+
- **Hover** is feedforward: an empty box darkens its rim and shows a faint ghost of the check it would get; a checked box lifts its mark half a pixel. **Pressing** deepens the trench and squeezes the mark. **Disabled** matches a disabled switch: a flat track and a grey mark.
|
|
8
|
+
|
|
9
|
+
**You provide:** `label`, optional `description`, and `checked`/`onCheckedChange` (or `defaultChecked`). It's a real `<input type="checkbox">`, so it works in forms with `name` and `value`.
|
|
10
|
+
|
|
11
|
+
- Use a Switch when the change takes effect immediately; a Checkbox is for choices submitted together.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Checklist
|
|
2
|
+
|
|
3
|
+
A list you write into, one line per item: to-dos, acceptance criteria, a packing list, a runbook's steps. Enter makes the next item, Backspace on an empty item removes it, Tab nests it, and each item has a checkbox. It's the keyboard-first counterpart to a list of structured records: here an item is its text, and the list is edited the way people write, with the outliner conventions of Workflowy, Logseq, Things, Notion and GitHub task lists.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
<Checklist<Task>
|
|
7
|
+
aria-label="Launch tasks"
|
|
8
|
+
items={tasks} // { id, text, done, children? }
|
|
9
|
+
getKey={(t) => t.id}
|
|
10
|
+
getText={(t) => t.text}
|
|
11
|
+
isDone={(t) => t.done}
|
|
12
|
+
getChildren={(t) => t.children}
|
|
13
|
+
nested
|
|
14
|
+
onChange={(op) => { setTasks((list) => apply(list, op)); return api.applyChecklistOp(op); }}
|
|
15
|
+
onUndo={(op) => setTasks((list) => revert(list, op))}
|
|
16
|
+
/>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Each action reaches `onChange` as one op (`add`, `edit`, `toggle`, `remove`, `move`, `indent`, `outdent`), so persistence can be granular and collaborators' edits merge per item. Apply the op to `items` synchronously (the line you're writing in must not wait on the network), then return `{ ok: true }`, nothing, or `{ ok: false, error }`, synchronously or as a promise. A failure marks the item and offers Retry on the line; the text stays. New items get ids from `createId` (default a random UUID), so you can insert them at once.
|
|
20
|
+
|
|
21
|
+
## No Save button, on purpose
|
|
22
|
+
|
|
23
|
+
`InlineEdit` and `EditRegion` have an explicit Save, and blur does nothing. A checklist where every line needs Save isn't a checklist, so here **the line is the editor and it commits when you leave it**: Enter, ↑/↓, Tab, Escape, a click elsewhere. Scoped to this component, it's an explicit act on a single field, and a refused change keeps the text in place with Retry, so nothing is lost silently.
|
|
24
|
+
|
|
25
|
+
## Elevation
|
|
26
|
+
|
|
27
|
+
- **Items** are the record, flush on the ground. The line is a plain text input with a focus ring, not a raised editor.
|
|
28
|
+
- **The last line** (“Add an item”) is an opening: a well while it's focused or holds text, dashed in forced colours.
|
|
29
|
+
- **The checkbox** is `Checkbox`: an empty trench, then the accent pours in with a raised mark.
|
|
30
|
+
- **Done** is pressed into the record: the text goes `ink-muted` with a strike drawn left to right, and the item stays in place (or, with `sortDone`, goes after the open ones).
|
|
31
|
+
- **A new item** opens as a well under the line (`ease-sink`). **Pending** writes hover at level 2 after `dur-grace`; a **failed** one drops to a trench with a critical rim and Retry.
|
|
32
|
+
- **Removed**: the line glides shut where it was (`dur-travel-2`, `ease-glide`) while the caret is already on the line above. There's no receipt and no hold: removal only happens from the keyboard, on an empty line or by merging a line's text into the one above, so nothing was lost. Undo is Ctrl+Z (⌘Z on Apple platforms) through `onUndo`, and the polite announcement says so: “Empty item removed. Undo with Ctrl+Z.” Nothing is added after the list. Under reduced motion the line closes at once.
|
|
33
|
+
|
|
34
|
+
Without height: nested `ul`s carry the structure; each checkbox is named by its item; done items say “done”; the list's description lists the keys; a polite status announces indents, moves, removals and errors. `contrast="more"`: the open well gets a solid inner top edge.
|
|
35
|
+
|
|
36
|
+
## Keyboard (while writing a line)
|
|
37
|
+
|
|
38
|
+
| Key | |
|
|
39
|
+
|---|---|
|
|
40
|
+
| Enter | split at the caret: the text after it becomes the next item (at the end: a new empty item) |
|
|
41
|
+
| Backspace at the start | on an empty item: remove it and go to the end of the line above. Otherwise merge into the line above, caret at the join |
|
|
42
|
+
| Tab / Shift+Tab | indent under the item above / outdent (only with `nested`) |
|
|
43
|
+
| Escape | leave the line and focus the item; Tab then moves on, so the list never traps focus |
|
|
44
|
+
| ↑ / ↓ | the line above / below, keeping the caret's position |
|
|
45
|
+
| Alt+↑ / Alt+↓ | move the item among its siblings |
|
|
46
|
+
| Ctrl/Cmd+Enter | check or uncheck it |
|
|
47
|
+
| Ctrl/Cmd+Z | undo the last add, remove, move or indent, through `onUndo` |
|
|
48
|
+
|
|
49
|
+
Nothing acts during IME composition. Pasting several lines adds one item per line; GitHub task-list markers (`- [ ]`, `- [x]`) and, with `nested`, two-space indentation are read.
|
|
50
|
+
|
|
51
|
+
## Props
|
|
52
|
+
|
|
53
|
+
| Prop | |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `items`, `getKey`, `getText`, `isDone`, `getChildren` | the nested data |
|
|
56
|
+
| `nested`, `sortDone` | allow Tab nesting (default false); show done items last (default false) |
|
|
57
|
+
| `onChange`, `onUndo`, `createId` | the ops, their reversal, and ids for new items |
|
|
58
|
+
| `placeholder`, `disabled`, `aria-label`, `aria-labelledby` | |
|
|
59
|
+
|
|
60
|
+
## You provide
|
|
61
|
+
|
|
62
|
+
The items, applying and persisting each op, and reversing ops for Undo.
|
|
63
|
+
|
|
64
|
+
## Do and don't
|
|
65
|
+
|
|
66
|
+
- **Do** use it when items are just text. When each item is a record with several fields, use a list of records with its own editor.
|
|
67
|
+
- **Don't** use it for long-form text; a line is one line.
|
|
68
|
+
|
|
69
|
+
## Demo and verification
|
|
70
|
+
|
|
71
|
+
The `Checklist` section of `build/previews.txt` shows nested launch tasks (a fictional service refuses “TBD”) and a flat packing list with `sortDone`. `node checks/checklist.mjs` (from `build/`; `ALT_FIXTURE_ROOT` selects React 19 fixtures) covers Enter, split and merge at the caret, Backspace on an empty line, Tab and Shift+Tab, Escape then Tab, ↑/↓, Alt+↑, Ctrl+Enter, IME Enter, a refused edit with Retry, remove and Ctrl+Z, the add line, task-list paste, `sortDone`, and 320/960px layouts in both schemes under reduced motion, forced colours and `contrast="more"`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# CodeBlock
|
|
2
|
+
|
|
3
|
+
Code to read and copy: install commands, API examples in several languages, webhook payloads, config, code an agent wrote.
|
|
4
|
+
|
|
5
|
+
**Elevation.** Code shown to read and copy is the record, so it lies flush on the ground: a 1px `line` rim, mono type and no shadow. It isn't a `Card`, because code isn't a live surface. The header carries its name, a segmented `Tabs` switch between forms of the same thing (npm, pnpm; curl, Node, Python), and a ghost `CopyButton` ("Copy code"). Highlighted lines are `accent-soft` bands with an `accent-ink` edge behind the text. Line numbers sit in an `ink-faint` gutter. **Code still arriving** (`streaming`, from an agent) isn't the record yet: it has a dashed `line-control` rule down its left, says "Writing…", is `aria-busy`, and can't be copied until it's done.
|
|
6
|
+
|
|
7
|
+
**Syntax colours are yours.** Altitude ships no highlighter. Pass `syntax={(code, language) => …}` (Shiki, Prism, your own) and it renders whatever you return. The default token classes (`tok-comment`, `tok-keyword`, `tok-string`, and Prism's and highlight.js's equivalents) use `ink-muted`, `accent-ink` and `ink`, never the `data-*` palette. Colours carry nothing on their own.
|
|
8
|
+
|
|
9
|
+
**Overflow.** Code taller than `maxHeight` (default 360) or wider than its box scrolls. Only then does the scroll area become a focusable, named region (`tabIndex=0`, `role="region"`), so keyboard users can scroll it with the arrows.
|
|
10
|
+
|
|
11
|
+
**Accessibility.** A `<figure>` named by its title, holding `<pre><code>`. Line numbers are `aria-hidden`. Highlighted lines are said in words before the code ("Lines 3 and 7 are highlighted."). The form switch is `Tabs`, with `aria-selected`. Under forced colours the rim is `CanvasText` and highlighted lines are `Mark`. Under `contrast="more"` the rim is `line-control`. Switching forms cross-fades the code over `dur-travel-1` and nothing travels.
|
|
12
|
+
|
|
13
|
+
**Props:** `code` and `language` for one block, or `tabs` (`{ label, code, language? }[]`); `title`; `highlight` (line numbers from 1); `syntax`; `lineNumbers`; `streaming`; `maxHeight`; `copyable` (default true).
|
|
14
|
+
|
|
15
|
+
**You provide:** the code, a title (a filename, or what it does), and a highlighter if you want colour.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<CodeBlock title="Install" tabs={[
|
|
19
|
+
{ label: 'npm', language: 'sh', code: 'npm install altitude-design' },
|
|
20
|
+
{ label: 'pnpm', language: 'sh', code: 'pnpm add altitude-design' },
|
|
21
|
+
]} />
|
|
22
|
+
<CodeBlock title="invoice.paid" language="json" code={payload} highlight={[3]} lineNumbers />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Do give every block a title, so it's named and so people know what to paste where.
|
|
26
|
+
- Do use tabs for the same thing in several forms, not for different things.
|
|
27
|
+
- Don't raise a code block into a card or give it a shadow.
|
|
28
|
+
- Don't colour code with the chart palette.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# ColorPicker and ColorSwatchPicker
|
|
2
|
+
|
|
3
|
+
A brand colour for a white-label portal, a label or tag colour, a calendar colour. `AltitudeProvider accent` already takes any colour and derives a scheme from it. This is the settings control that chooses one.
|
|
4
|
+
|
|
5
|
+
**Elevation: choices are switches.** Swatches are built like `RadioGroup`. Each swatch is a trench (with a `line-control` rim) that its colour pours into. **The chosen one carries the raised white mark**, one mark that travels between swatches on `ease-glide`, as the radio thumb does. Pointing at another swatch shows a ghost of the mark where it would land (feedforward).
|
|
6
|
+
|
|
7
|
+
**Any colour.** With `allowCustom`:
|
|
8
|
+
- **The hex field** is an `Input` with a chip of the colour. It fills only when it holds a whole colour; a partial one ("0f8") is still an opening when you leave it.
|
|
9
|
+
- **"Custom…"** opens a `Popover` (level 3) with a saturation/brightness area and a hue strip. Both are wells, and their handles are raised.
|
|
10
|
+
|
|
11
|
+
**Contrast.** `accent` is the product's one hue, and text sits on it, so `checkContrast` says in words, for white text and for dark text, whether each reaches 4.5:1 on the colour ("White text fails: 4.3:1", "Dark text passes: 4.8:1"), as `Badge` plates. Some text nearly always passes, so it says which.
|
|
12
|
+
|
|
13
|
+
**Accessibility.** Swatches are a [radio group](https://www.w3.org/WAI/ARIA/apg/patterns/radio/): one tab stop, and arrows move and choose. **Every swatch has a name** ("Teal"), so colour is never the only way to tell them apart, and the chosen colour's name and hex are announced. The area is a [slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) for saturation and brightness: Left and Right change saturation, Up and Down brightness, and Shift moves 10%. Its value text is "Saturation 80%, brightness 54%". The hue strip is a slider from 0 to 360°. Under forced colours swatches keep their real colour (`forced-color-adjust: none`) with a 1px `CanvasText` border, or 2px for the chosen one. Under `contrast="more"` the chosen swatch gets a hard ring. Under reduced motion the mark appears in place.
|
|
14
|
+
|
|
15
|
+
**Props.**
|
|
16
|
+
- `ColorPicker`: `label`, `value`, `defaultValue`, `onValueChange` (a lower-case `#rrggbb`), `swatches` (`{ value, name }[]`), `allowCustom`, `checkContrast`.
|
|
17
|
+
- `ColorSwatchPicker` (the swatches alone): `swatches`, `value`, `defaultValue`, `onValueChange`, `aria-label`.
|
|
18
|
+
|
|
19
|
+
**You provide:** a named palette (six to eight swatches), and whether any colour is allowed.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<ColorPicker label="Brand colour" value={brand} onValueChange={setBrand}
|
|
23
|
+
swatches={[{ value: '#0f8a6b', name: 'Teal' }, { value: '#2f5fd0', name: 'Blue' }]}
|
|
24
|
+
allowCustom checkContrast />
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- Do name every swatch.
|
|
28
|
+
- Do turn on `checkContrast` wherever the colour will carry text (an accent, a button).
|
|
29
|
+
- Don't offer a free colour where a palette will do; custom colours make labels hard to tell apart.
|
|
30
|
+
- Don't use the `data-*` chart palette as label colours; it means series.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Combobox
|
|
2
|
+
|
|
3
|
+
An editable Select: type to narrow a long list, then choose. What you type is a query, not yet a value.
|
|
4
|
+
|
|
5
|
+
**Elevation.** The field is the same opening as `Select` and `Input`. The difference is what counts as filled: typing doesn't fill a Combobox, choosing does. So while you type, the field keeps its back wall showing under your text (lit and rimmed in accent, because it has focus); only when an option is chosen does its floor rise flush. A form shows which comboboxes still hold a mere query. Invalid drops it to trench depth.
|
|
6
|
+
|
|
7
|
+
**The list** rises from the field's edge at level 3 with one gliding highlight, as in Select, and narrows as you type: options are ranked by the CommandPalette's matching (exact, then prefix, word start, substring, letters in order), the best match is first and highlighted, and the letters that matched are drawn in `accent-ink`. The chosen option wears the Checkbox family's tick. A new value settles in with a brief afterglow.
|
|
8
|
+
|
|
9
|
+
**Keyboard and screen readers.** The WAI-ARIA combobox pattern with a listbox popup: the text field has `role="combobox"`, `aria-expanded`, `aria-autocomplete="list"` and `aria-activedescendant`, so focus stays in the field. Typing opens and filters; ↓ and ↑ move (↓ also opens it, Alt+↓ without moving); Enter chooses; Escape closes and puts back the previous value. Home and End stay with the text. Leaving the field, by Tab or by clicking away, commits what's typed: an exact name chooses that option, an empty field clears the value, anything else goes back to the value it had. The chevron opens the list for pointer users.
|
|
10
|
+
|
|
11
|
+
**On a phone.** The list stays under the field you're typing in by default (`presentation="auto"` or `"anchored"`), so the on-screen keyboard hides neither. `presentation="sheet"` opens it at the bottom edge instead.
|
|
12
|
+
|
|
13
|
+
**You provide:** `items` (`{ value, label, textValue?, description?, disabled? }`), `value`/`onValueChange` or `defaultValue`, a `placeholder` that invites typing ("Search people"), optionally `filter` (return 0 or false to hide, higher to rank first), `empty` (default "No matches") and `name` for forms. Wrap it in `Field`.
|
|
14
|
+
|
|
15
|
+
- Do use it when the list is long enough that scrolling is slower than typing (people, countries, repositories).
|
|
16
|
+
- Don't use it for free text that may not be in the list; it only accepts listed values.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# CommandPalette
|
|
2
|
+
|
|
3
|
+
Finds and runs anything in the product from one field: the summit, at level 5, opened with ⌘K or Ctrl+K.
|
|
4
|
+
|
|
5
|
+
**Why the summit.** A command palette takes over the screen for a moment: whatever you were doing waits until you've chosen. That is what level 5 means (focus of the moment), so it sits where Dialog sits and uses the same machinery: the page recedes 1.5% and desaturates, the `scrim` veils it, and the palette rises 16px from 96% scale while its shadow grows from `elev-1` to `elev-5` (`dur-travel-5`). It's anchored 12vh from the top rather than centred, so the field stays put while the results below it grow and shrink. There is one summit: ⌘K does nothing while a Dialog is open, and choosing an item closes the palette before its action runs, so an action may open a Dialog.
|
|
6
|
+
|
|
7
|
+
**The field is an opening.** It's a real `Input`. An empty query is a hole in the palette with its back wall showing (focus never leaves it, so it never needs the rim a field you tab to gets); the first character raises its floor flush with the palette. Here that floor is `surface-modal`, the surface it sits on, rather than Input's usual `surface-raised`, which in dark schemes is darker than level 5 and would leave a filled query looking sunk.
|
|
8
|
+
|
|
9
|
+
**Results.** Groups with labels; each row has an icon, a label, an optional description and its shortcut as `Kbd` keys. The letters that matched are drawn in `accent-ink`. One `accent-soft` highlight (`critical-soft` on a destructive item) is carried to the active row on `ease-settle`, as in DropdownMenu.
|
|
10
|
+
|
|
11
|
+
- **Best match on top.** Items rank by score within their group and groups rank by their best item, so the best match is always the first row and is already active: Enter runs it.
|
|
12
|
+
- **Carried, not reshuffled.** When the results change, rows that stay travel to their new places (FLIP, `dur-travel-2`, `ease-glide`) instead of blinking, and the list's height glides to fit. New rows rise 4px into place, but rises spend attention: only rows near the top rise, at most four begin together (the rest follow 90ms apart), and everything further down cross-fades.
|
|
13
|
+
- **Predicted first.** On opening, items marked `predicted` (or a whole group, such as Recent) are there at once while the rest fade in over `dur-onset` (500ms): ephemeral adaptation, as in DropdownMenu. Once you type, everything shows at once.
|
|
14
|
+
|
|
15
|
+
**Carrying a query in.** By default the field is empty each time the palette opens. Pass `defaultQuery` to open it on a query (the page's current filter, a deep link), or control it with `query` and `onQueryChange` when something else already holds the text: a persistent search field on the page can open the palette on its first keystroke and hand over what was typed, and a controlled query isn't cleared on opening. Either way the caret lands after the carried text, so typing continues it and the results filter as usual. Focus moves to the palette's field as it rises; keep the outer field controlled by the same state so nothing typed in between is lost. An IME composition in progress can't move between fields, so it ends in the field where it started.
|
|
16
|
+
|
|
17
|
+
**Keyboard and screen readers.** The field is a `combobox` that controls a `listbox` through `aria-activedescendant` (the WAI-ARIA combobox pattern), so focus stays in the field. ↑ and ↓ move and wrap, Enter chooses, Escape closes, and ⌘K / Ctrl+K toggles. Groups are named by their labels, the active option is `aria-selected`, disabled items are skipped, and the number of results is announced once typing pauses. Focus goes back where it was when the palette closes. Under reduced motion nothing travels; rows fade.
|
|
18
|
+
|
|
19
|
+
**On touch.** `hints="auto"` (the default) shows the footer, the item shortcuts and the field's esc where there's likely a keyboard, and hides them on a touch-only device until a key is pressed, so a phone's palette isn't crowded with keys it can't press. `true` or `false` forces them. The shortcuts still work either way.
|
|
20
|
+
|
|
21
|
+
**You provide:** `items`, a list of groups (`label`, `items`, optional `predicted`), each item with a `label` and usually an `icon`, `onSelect`, `shortcut` (display only: bind the key yourself), `keywords` and `description`; `open`/`onOpenChange` when you also open it from a button or a field; `query`/`onQueryChange` or `defaultQuery` when it should open on a query. Optionally `filter` (return 0 to hide, higher to rank nearer the top; `commandScore` is the default), `empty`, `placeholder`, and `shortcut` (another letter, or `false`).
|
|
22
|
+
|
|
23
|
+
- Do start actions with a verb ("Invite people") and name places plainly ("Northwind").
|
|
24
|
+
- Do mark items `predicted` only when you'd be right most of the time; a wrong prediction costs a moment.
|
|
25
|
+
- Don't render two palettes, and don't open a Sheet from one: one summit, and a level-4 sheet can't sit on it.
|
|
26
|
+
|
|
27
|
+
The palette and Dialog share one summit per document, including across providers. Its shortcut cannot take an occupied summit. A simultaneous controlled request is rejected with a development warning; set it to `false` before trying again. Rejected requests do not queue. A selected command can close the palette and open a Dialog synchronously: callback ordering is unchanged, the exiting palette retires, and scroll/isolation ownership transfers without unlocking the page. Only the active modal has `aria-modal`; composition Enter and Escape remain with the input method.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# CommentThread
|
|
2
|
+
|
|
3
|
+
Comments on a task, a document, a design or a deal: write with @mentions, post, retry a failed post, delete your own, and resolve the thread.
|
|
4
|
+
|
|
5
|
+
**Elevation.**
|
|
6
|
+
- **Posted comments are the record**, flush on the ground, each with its author, `RelativeTime` and body. Mentions are flush `accent-soft` plates in the text.
|
|
7
|
+
- **The composer is an opening**, an `Input` well that fills as you write.
|
|
8
|
+
- **A comment being posted is a pending write**: it hovers ("Posting…") until it lands and settles. If it fails, it drops to a trench ("Couldn't post"), keeping its text, with Retry.
|
|
9
|
+
- **"Done is pressed into the record": a resolved thread is stamped.** It's muted and collapsed to one line ("Resolved by Maya"), kept in place rather than removed, like a done `Checklist` item, with Show comments and Reopen.
|
|
10
|
+
- **A deleted comment** leaves its result in its slot ("Comment deleted"), with Undo.
|
|
11
|
+
- **Someone typing** shows their `presence` colour and name. Other people, live, is where presence belongs.
|
|
12
|
+
|
|
13
|
+
**Mentions.** Type `@` and a name: a listbox (level 3) offers people from `mentionable(query)`. Arrows move, Enter or Tab chooses, and Escape closes the list. The body stays plain text with "@Name", and `onPost(body, mentions)` receives the people still mentioned in it.
|
|
14
|
+
|
|
15
|
+
**Accessibility.** Comments are articles in a list. The composer is a [combobox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) textarea while mentioning (`aria-activedescendant`). ⌘Enter or Ctrl+Enter posts. New comments from others are announced politely and batched ("2 new comments"). Typing indicators aren't announced. Under forced colours the composer is dashed, a pending comment dotted and a failed one dashed thick. Under reduced motion the typing dots don't pulse.
|
|
16
|
+
|
|
17
|
+
**Props:** `comments` (`{ id, author: { name, avatar? }, body, createdAt, status?, mentions?, mine?, deleted? }[]`); `resolved` (`{ by }` or false); `onPost`; `onResolve`; `onReopen`; `onRetry`; `onDelete`; `onUndoDelete`; `mentionable`; `typing` (names); `aria-label`.
|
|
18
|
+
|
|
19
|
+
**You provide:** the comments and their states (your app adds a pending comment when it posts, and marks it failed or posted), and who can be mentioned. Rich text is out of scope; a rich editor plugs in through the editor adapter.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<CommentThread comments={comments} resolved={thread.resolved}
|
|
23
|
+
onPost={(body, mentions) => post(body, mentions)} onResolve={resolve} onReopen={reopen}
|
|
24
|
+
mentionable={(q) => searchPeople(q)} typing={othersTyping} />
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- Do keep a failed comment's text where it was.
|
|
28
|
+
- Do keep a resolved thread in place, stamped.
|
|
29
|
+
- Don't announce typing.
|
|
30
|
+
- Don't post on Enter alone; people write several lines.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Confirmation
|
|
2
|
+
|
|
3
|
+
Approve or reject an action an agent proposes, before anything happens: "Send 214 emails to Pro customers", "Delete 3 stale branches". Human-in-the-loop for tool calls (the AI SDK's `approval-requested` state).
|
|
4
|
+
|
|
5
|
+
**Elevation: proposed is raised, not pending.** A pending write hovers because it's already in flight. A proposal hasn't been dispatched, so drawing it hovering would tell the person being asked for permission that it's already happening.
|
|
6
|
+
- **Requested:** a raised card (level 1, `Alert`'s rule for status that asks for action): "Approval needed", what's proposed, **the concrete consequence in words**, optional details (a `DiffView`, a list), "The assistant is asking", and Reject and Approve. Approve names the verb and outcome ("Send 214 emails").
|
|
7
|
+
- **Approved / running:** now it's a write, so it hovers as pending ("Sending…").
|
|
8
|
+
- **Done:** lands with the afterglow ("Sent · approved by you · now").
|
|
9
|
+
- **Failed:** drops to a trench ("Couldn't send"), with Retry.
|
|
10
|
+
- **Rejected / expired:** flush and muted, kept in the record ("Rejected by Maya · 2 minutes ago", "No longer needed").
|
|
11
|
+
|
|
12
|
+
**Escalation.** Consequential proposals use the `consequential-action` contract: `escalate="hold"` makes Approve a `HoldButton`; `escalate="dialog"` asks again in a stationary `Dialog` for irreversible actions.
|
|
13
|
+
|
|
14
|
+
**Accessibility.** A labelled `section` (its title) described by the consequence. Not a dialog unless escalated. **There's no default action**: nothing is approved by an accidental Enter, and nothing is focused automatically. Outcomes are announced ("Send campaign email: Sent."). Under forced colours the card has a 1px border, pending is dotted and failed dashed thick. Under `contrast="more"` the requested card gets a hard slab.
|
|
15
|
+
|
|
16
|
+
**Props:** `state` (`'requested' | 'approved' | 'running' | 'done' | 'failed' | 'rejected' | 'expired'`); `title`; `consequence`; `details`; `approveLabel`, `rejectLabel`, `runningLabel`, `doneLabel`, `failedLabel`; `escalate`; `resolvedBy`, `resolvedAt`; `requestedBy`; `onApprove`, `onReject`, `onRetry`.
|
|
17
|
+
|
|
18
|
+
**Scopes.** For an action that comes up again and again ("Run npm test"), give `scopes`: Approve becomes a SplitButton whose face approves once and whose menu, "More ways to approve", holds the wider scopes ("Allow npm test for this session", "Always allow npm test · Saved to this project's settings"). `onApprove` gets `'once'` or the scope's value; pass it back as `resolvedScope` and the resolved line says it: "Ran · allowed for this session by you". With `escalate`, scopes are ignored: an action that needs a hold or a dialog is never pre-approved.
|
|
19
|
+
|
|
20
|
+
**Editing the proposal.** Give `edit` (fields, e.g. `Field` + `Input`) and the card offers Edit: the fields open in place inside it, as `EditRegion` does, focusing the first. Keep `consequence` and `approveLabel` following the edited values so Approve always names what will happen, and set `edited` when they differ: "Edited" shows by the title, and once done the line reads "Sent · edited and approved by you". Escape or Cancel edit closes the fields and calls `onEditCancel` (put the values back); it doesn't reject the proposal. Reject stays available while editing.
|
|
21
|
+
|
|
22
|
+
**You provide:** the state from your tool call, the consequence in words, and what approving and rejecting do.
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
<Confirmation state={call.state} title="Send campaign email"
|
|
26
|
+
consequence="214 Pro customers will receive “October product update”."
|
|
27
|
+
approveLabel="Send 214 emails" rejectLabel="Don't send" runningLabel="Sending…" doneLabel="Sent"
|
|
28
|
+
onApprove={() => respond(call.id, true)} onReject={() => respond(call.id, false)} />
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- Do state the consequence concretely: who, how many, what.
|
|
32
|
+
- Do name the outcome on Approve.
|
|
33
|
+
- Don't draw a proposal as pending.
|
|
34
|
+
- Don't remove a rejected proposal; keep it in the record.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# ContextMenu
|
|
2
|
+
|
|
3
|
+
Actions on a thing (a file, a row, a board card, a tree node, a canvas object), opened where you right-click or long-press it, or at it with Shift+F10 or the Menu key.
|
|
4
|
+
|
|
5
|
+
**Elevation.** The menu is `DropdownMenu`'s own (both use one implementation): a floating surface at level 3, with the highlight that glides between items, `predicted` items first, and `critical` items in words. **The thing shows it's the subject with a 2px `accent-ink` outline**, a ring and never a height, so a flush table row doesn't lift out of its table.
|
|
6
|
+
|
|
7
|
+
**No submenus.** "Don't stack floating on floating": a menu inside a menu loses the reading of height. Put the choices in the menu (a few) or open a `Dialog` ("Move to…").
|
|
8
|
+
|
|
9
|
+
**Every action needs another way.** A right-click is invisible until you know it's there, and long-press is easy to miss. Put the same actions behind a visible button on the thing (a row's "⋯" `DropdownMenu`), as the preview does.
|
|
10
|
+
|
|
11
|
+
**Accessibility.** The [menu pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu/). The thing gets `aria-haspopup="menu"` and `aria-expanded`, and must be focusable (`tabIndex={0}`) so Shift+F10 and the Menu key can open the menu at it. That opens with the first item active. Arrows, Home, End and Enter work in the menu, and Escape (or choosing) returns focus to the thing. A long press on touch is 500ms without moving more than 10px. The menu stays inside the viewport, flipping left or up at an edge, and closes on scroll, resize or a click outside. Under forced colours the outline is `Highlight`.
|
|
12
|
+
|
|
13
|
+
**Props:** `items` (`DropdownMenu` entries, or a function called each time it opens); `children` (one element: the thing); `aria-label`; `onOpenChange`.
|
|
14
|
+
|
|
15
|
+
**You provide:** the thing (focusable), its actions, and another way to reach them.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<ContextMenu items={() => [
|
|
19
|
+
{ label: 'Rename', onSelect: () => rename(file) },
|
|
20
|
+
{ type: 'separator' },
|
|
21
|
+
{ label: 'Delete file', tone: 'critical', onSelect: () => remove(file) },
|
|
22
|
+
]}>
|
|
23
|
+
<div role="listitem" tabIndex={0}>{file.name} <RowActions file={file} /></div>
|
|
24
|
+
</ContextMenu>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- Do make the thing focusable, so the keyboard can open its menu.
|
|
28
|
+
- Do offer the same actions in a visible menu.
|
|
29
|
+
- Don't nest menus; open a `Dialog` for a long choice.
|
|
30
|
+
- Don't raise the thing while its menu is open; it's outlined.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Conversation
|
|
2
|
+
|
|
3
|
+
The thread between a person and an assistant: their messages, the assistant's answers (often streamed), copy, regenerate, feedback, versions, and "Jump to latest" once you've scrolled up. Pair it with `PromptInput`.
|
|
4
|
+
|
|
5
|
+
**Elevation: how settled is a streamed answer?** Settled messages are the record.
|
|
6
|
+
- **A person's message** is a flush `accent-soft` sheet on the right.
|
|
7
|
+
- **An assistant's complete message** is flush with no bubble and no shadow, with a quiet `line` rule down its left edge.
|
|
8
|
+
- **A streaming answer isn't the record yet**, and it isn't a pending write either: nothing you sent is in flight, so it mustn't hover. It rests on the ground with the treatment charts use for data still being counted: **a dashed `line-control` rule**, "Generating…" and a caret. It's `aria-busy`.
|
|
9
|
+
- **When it completes**, the rule goes solid and the message settles with one afterglow. The arrival signal is held until the answer is whole, because lifting it for the seconds it takes to stream would be a prolonged rise.
|
|
10
|
+
- **Stopped or failed** answers are kept, with "Stopped" or "Couldn't finish" (and a `critical` rule) and Retry.
|
|
11
|
+
- **Message actions** are ghost buttons that lift when pointed at. Feedback is a pair of toggles; regenerated answers are versions ("Version 2 of 2"), because a previous answer is a record and isn't overwritten.
|
|
12
|
+
- **"Jump to latest message"** floats at level 3 once you've scrolled up. The thread follows new text to the bottom until you scroll up, then lets go.
|
|
13
|
+
|
|
14
|
+
**Motion.** Text arrives without per-word animation. Completion: one afterglow. Under reduced motion the caret doesn't blink.
|
|
15
|
+
|
|
16
|
+
**Accessibility.** A `log` region of articles, each headed by who's speaking ("You" visually hidden). **Streamed text isn't announced token by token**: the log's own announcements are off, and each answer is announced once, when it completes ("Assistant: Last week the team shipped…", up to 280 characters). Stopping is announced politely, a failure assertively. The log is focusable so it can be scrolled by keyboard. Under forced colours the streaming rule is dotted and a complete one solid. Under `contrast="more"` the rule is `line-control`.
|
|
17
|
+
|
|
18
|
+
**Parts:** `Conversation` (`aria-label`); `ConversationContent`; `ConversationScrollButton`; `Message` (`from`, `status` (`'streaming' | 'complete' | 'stopped' | 'error'`), `name`, `onRetry`); `MessageAttachments` (`items`: `{ name, type?, size?, url?, alt?, href?, onOpen?, unavailable? }`), before the content: what was sent with the message, images as 64px thumbnails and other files as settled Tag plates with type and size, acting when they have `href` or `onOpen`; `MessageContent`; `MessageActions` (`onCopy`, `onRegenerate`, `onFeedback`, `feedback`, `versions`, `onEdit`); `Message edited`.
|
|
19
|
+
|
|
20
|
+
**Editing a sent message.** Give a person's message `MessageActions onEdit`: Edit opens it in place (the text stays where it is while a Textarea well opens around it), with Cancel and "Send edit"; Escape cancels, Ctrl/⌘+Enter sends. Sending calls `onEdit(text)`: start a new branch from that message, keep the earlier one, and show its `versions` ("Version 2 of 2") and `edited` ("Edited"). Switching versions is announced; render the turns of the branch shown. Nothing is deleted.
|
|
21
|
+
|
|
22
|
+
**The queue.** `ConversationQueue` (`items`, `onSendNow`, `onEdit`, `onRemove`) goes between the thread and the composer and lists messages queued while the agent works (`PromptInput onQueue`), in order, each with Send now (stop the turn and send it), Edit (in place) and Remove. A queued message is written but not dispatched, so it doesn't hover: it's the person's sheet, flush, with a dashed rim and "Queued · 1 of 2". When the turn ends, send the first; it becomes an ordinary message in the log.
|
|
23
|
+
|
|
24
|
+
**Checkpoints.** `ConversationCheckpoint` (`label`, `at`, `consequence`, `onRestore`, `onReturn`) marks a point between messages to go back to: a divider reading "Checkpoint · Before editing 6 files · 2:14 PM" with Restore, described by the consequence. It follows the consequential-action contract: with `onReturn` (real recovery), Restore runs at once and leaves "Return to where you were"; without it, Restore asks in a Dialog. Restoring hovers as pending, settles once when done, and sinks into a trench with Retry if it fails; the outcome is announced. Set the later turns aside as a branch rather than deleting them.
|
|
25
|
+
|
|
26
|
+
**You provide:** the messages and their status as your stream reports it. Code inside an answer goes in `CodeBlock`.
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
<Conversation aria-label="Assistant">
|
|
30
|
+
<ConversationContent>
|
|
31
|
+
{messages.map((m) => (
|
|
32
|
+
<Message key={m.id} from={m.role} status={m.status}>
|
|
33
|
+
<MessageContent>{m.content}</MessageContent>
|
|
34
|
+
<MessageActions onCopy onRegenerate={() => regen(m.id)} onFeedback={(v) => rate(m.id, v)} />
|
|
35
|
+
</Message>
|
|
36
|
+
))}
|
|
37
|
+
</ConversationContent>
|
|
38
|
+
<ConversationScrollButton />
|
|
39
|
+
</Conversation>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- Do pass the stream's status, so a finished answer settles and is announced.
|
|
43
|
+
- Do keep earlier versions when regenerating.
|
|
44
|
+
- Don't fade in text word by word or glow a streaming answer.
|
|
45
|
+
- Don't make a streaming answer hover; it isn't a pending write.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# CopyButton
|
|
2
|
+
|
|
3
|
+
Copies a value and says so: API keys, install commands, invite links, IDs, webhook URLs.
|
|
4
|
+
|
|
5
|
+
**Elevation.** It's a `Button` (ghost by default, or `secondary`), so it's live: it lifts one level when pointed at. **Copying is a press that confirms in words.** Once copied it stays pressed to the ground for `dur-linger` (2s), its icon cross-fades from copy to a check, and its words change to "Copied". Then it springs back. There's no rise and no afterglow, because nothing new arrived on the page. All its words share one cell, so saying "Copied" never changes its width.
|
|
6
|
+
|
|
7
|
+
**When it fails.** The Clipboard API needs a secure context and permission. Where it isn't available, CopyButton falls back to selecting a hidden copy of the text and running the copy command. If that fails too, the button says "Couldn't copy" with an alert icon, and "Couldn't copy. Select the text and press ⌘C." (Ctrl+C off a Mac) is announced. `onError` gets the error.
|
|
8
|
+
|
|
9
|
+
**Accessibility.** It's a plain [button](https://www.w3.org/WAI/ARIA/apg/patterns/button/): Enter or Space copies. The button keeps its name while it says "Copied": a polite status region announces the result instead, so focus never lands on a button whose name just changed. An icon-only CopyButton (no children) is square, named by `label`, and shows `label` in a `Tooltip`. Under forced colours a hovered button gets a 1px border.
|
|
10
|
+
|
|
11
|
+
**Props:** `value` (a string, or a function returning one or a promise of one); `label` (default "Copy"); `copiedLabel` (default "Copied"); `failedLabel` (default "Couldn't copy"); `onCopy(value)`; `onError(error)`; every `Button` prop except `square`, `loading` and `leading`.
|
|
12
|
+
|
|
13
|
+
**You provide:** the value, and a `label` that names what's copied ("Copy API key") when the button is icon-only.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<CopyButton value={apiKey} label="Copy API key" />
|
|
17
|
+
<CopyButton value={() => buildInviteUrl()} variant="secondary">Copy invite link</CopyButton>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- Do name what's copied ("Copy API key"), not just "Copy", when there are several on a page.
|
|
21
|
+
- Do put it next to the value it copies, so people can see what they got.
|
|
22
|
+
- Don't confirm a copy with a toast; the button says it where you're looking.
|
|
23
|
+
- Don't make a primary CopyButton: copying is rarely the main thing on a page.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# DataGrid
|
|
2
|
+
|
|
3
|
+
A table whose cells you edit one by one, with spreadsheet keys: arrows to move, type to replace, Enter or F2 to edit, Enter and Tab to save and move on, Escape to back out, a block pasted from a spreadsheet. For bulk entry and correction (prices, stock, schedules, mappings), where opening a form per row is the slowest path.
|
|
4
|
+
|
|
5
|
+
**Use `DataTable` instead** when cells aren't edited one by one: its rows are records you select, sort, filter and open, and it's a table, not a grid. DataGrid changes the semantics (`role="grid"`, one Tab stop, roving cell focus), so it's a sibling, not a mode.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<DataGrid<Product>
|
|
9
|
+
aria-label="Inventory"
|
|
10
|
+
rows={products}
|
|
11
|
+
getRowId={(p) => p.id}
|
|
12
|
+
getRowLabel={(p) => p.name}
|
|
13
|
+
columns={[
|
|
14
|
+
{ key: 'name', header: 'Name', editable: true, validate: (v) => v.trim() ? undefined : 'Enter a name.' },
|
|
15
|
+
{ key: 'sku', header: 'SKU' }, // read-only
|
|
16
|
+
{ key: 'stock', header: 'Stock', align: 'end', editable: true, parse: Number, validate: (n) => n >= 0 ? undefined : 'Use 0 or more.' },
|
|
17
|
+
{ key: 'status', header: 'Status', editable: true, cell: (p) => <Badge>{p.status}</Badge>,
|
|
18
|
+
editor: (control) => <Select {...control} items={statuses} defaultOpen /> },
|
|
19
|
+
]}
|
|
20
|
+
onCellSave={async ({ rowId, key, draft }) => { const r = await api.patch(rowId, { [key]: draft }); if (r.ok) setProducts(r.all); return r; }}
|
|
21
|
+
onCellsSave={async (changes) => { const r = await api.batchPatch(changes); if (r.ok) setProducts(r.all); return r; }}
|
|
22
|
+
onUndo={(last) => revert(last)}
|
|
23
|
+
/>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Writes follow the `InlineEdit` contract: resolve `{ ok: true }` after `rows` carries the canonical value, or `{ ok: false, error }`. `onCellsSave` takes a paste or a cleared range as one batch; without it, each cell goes through `onCellSave`. `onUndo` receives the last committed change (or batch); there's no client-side snapshot.
|
|
27
|
+
|
|
28
|
+
Data that arrived from elsewhere (an import, a sync) may already break a column's `validate`. `showInvalid` checks every cell at rest: an invalid value lies in a trench on critical-soft with an alert mark, and its problem is the cell's description, so the work left is visible at a glance and each cell says what's wrong. Fixing the value through `onCellSave` lifts it out.
|
|
29
|
+
|
|
30
|
+
## Elevation
|
|
31
|
+
|
|
32
|
+
- **Cells** are the record, flush in Table's rows. The **active cell** is a 2px `accent-ink` ring, not a height; a **range** is an `accent-soft` tint with `aria-selected`, because choosing a target isn't live content.
|
|
33
|
+
- **Editable columns** carry a pencil in the header and “editable” in its name. Cells don't lift on hover: a click selects; Enter, F2, typing or a double-click edits.
|
|
34
|
+
- **The cell being edited** opens into a well clipped to the cell, with the held rim. An **invalid** value deepens to a trench on `critical-soft`, with its message beside the cell (`role="alert"`, wired by `aria-describedby`).
|
|
35
|
+
- **Saving** shows a small raised plate in the cell after `dur-grace`, with `aria-busy`. A **failed** cell drops to a trench with a critical mark, keeps its old value, and reopening it restores the rejected draft. A **landed** value settles; a pasted range settles as one change.
|
|
36
|
+
- **A custom editor** (a Select) floats its list at level 3 from inside the cell, and commits on choosing.
|
|
37
|
+
|
|
38
|
+
Forced colours: the active cell Highlight outline, the range Highlight, the editing cell dashed (thicker when invalid), pending dotted, failed dashed. `contrast="more"`: the editing well gets the solid inner top edge.
|
|
39
|
+
|
|
40
|
+
## Keyboard
|
|
41
|
+
|
|
42
|
+
The APG grid, with spreadsheet editing:
|
|
43
|
+
|
|
44
|
+
| Mode | Key | |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| Navigation | arrows | one cell (mirrored in RTL); Shift extends a range |
|
|
47
|
+
| | Home / End, Ctrl+Home / Ctrl+End | row ends; grid corners |
|
|
48
|
+
| | PageUp / PageDown | ten rows |
|
|
49
|
+
| | Enter, F2, or a character | edit (a character replaces the value) |
|
|
50
|
+
| | Delete / Backspace | clear the cell or range, as one change that can be refused |
|
|
51
|
+
| | Ctrl/Cmd+C, Ctrl/Cmd+V | copy the range as tab-separated text; paste a block from the active cell |
|
|
52
|
+
| | Ctrl/Cmd+Z | `onUndo` with the last change |
|
|
53
|
+
| Editing | Enter / Shift+Enter | save and move down / up |
|
|
54
|
+
| | Tab / Shift+Tab | save and move right / left |
|
|
55
|
+
| | Escape | discard the draft |
|
|
56
|
+
|
|
57
|
+
Clicking another cell saves the open one and moves there, as in a spreadsheet; moving focus out of the grid keeps the draft open and marks it “Not saved yet”, rather than saving or discarding it. Nothing acts during IME composition. Announcements are batched: “Saved”, “4 cells saved”, “1 cell couldn't be saved: Price must be at least 0”, “3 by 4 cells selected”.
|
|
58
|
+
|
|
59
|
+
## Props
|
|
60
|
+
|
|
61
|
+
| Prop | |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `rows`, `getRowId`, `getRowLabel` | the data; the row label names each cell's editor (“Price, Desk lamp”) |
|
|
64
|
+
| `columns` | `{ key, header, label?, align?, editable?, parse?, format?, validate?, cell?, editor?, width? }` |
|
|
65
|
+
| `onCellSave`, `onCellsSave`, `onUndo` | the writes and their reversal |
|
|
66
|
+
| `maxHeight`, `aria-label`, `aria-labelledby` | |
|
|
67
|
+
|
|
68
|
+
## You provide
|
|
69
|
+
|
|
70
|
+
The rows, the writes and their concurrency guards, and undo. Virtualisation, a fill handle and row creation aren't covered yet.
|
|
71
|
+
|
|
72
|
+
## Do and don't
|
|
73
|
+
|
|
74
|
+
- **Do** validate in the column, so a bad value never leaves the cell.
|
|
75
|
+
- **Do** give every row a label people would say, for the cell names.
|
|
76
|
+
- **Don't** use it for records people mostly read, select or open: that's `DataTable`.
|
|
77
|
+
|
|
78
|
+
## Demo and verification
|
|
79
|
+
|
|
80
|
+
The `DataGrid` section of `build/previews.txt` is an inventory grid: the fictional service locks the Desk lamp's price, prices must be 0 or more, and Status uses a Select. `node checks/data-grid.mjs` (from `build/`; `ALT_FIXTURE_ROOT` selects React 19 fixtures) covers the grid keys, type-to-replace, Enter, Tab, F2, Escape, validation, IME Enter, click-to-commit, a refused save and its retry draft, read-only cells, ranges, copy, clearing a range, undo, a paste with one invalid cell, the Select column, and 320/960px layouts in both schemes under reduced motion, forced colours and `contrast="more"`.
|
|
81
|
+
**On a phone.** Under 480px of the frame's width, columns keep at least 6rem (or their `minWidth`) and the table scrolls inside its frame, fading the edge with more columns, instead of squeezing cells. `sticky: true` keeps a column in view over a flush 1px edge. Wide, nothing changes.
|
|
82
|
+
|