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,18 @@
|
|
|
1
|
+
# HoverCard
|
|
2
|
+
|
|
3
|
+
A richer preview of what a link or a face points to: who someone is, what a project holds. It rises from its trigger after the pointer rests on it.
|
|
4
|
+
|
|
5
|
+
**Elevation.** Level 3, like Popover: `surface-floating`, `elev-3`, rising 6px from 96% scale out of its trigger. It's something you glance at, above the page and gone as soon as you move on, which is what level 3 means. Unlike a Tooltip it can hold more than a label (an avatar, a sentence, a few numbers), so it's a card with `radius-lg` and `space-4` padding.
|
|
6
|
+
|
|
7
|
+
**Timing is the design.** It opens only after the pointer has rested on the trigger for 400ms (hover intent), so passing over a paragraph of names doesn't set off a string of cards. It waits 250ms after the pointer leaves, and an invisible bridge spans the gap between trigger and card, so you can move into it. It shares Tooltip's warm-up: once a card or tooltip has shown, a neighbour opens at once instead of waiting again. Only one card is open at a time; moving to a neighbour hands over. Under reduced motion it fades without travelling.
|
|
8
|
+
|
|
9
|
+
**Keyboard and touch.** Keyboard focus on the trigger opens it after the same pause; Tab continues into any links inside it; Escape closes it and leaves focus on the trigger. On touch there's no hover, so it never opens: a tap just follows the link.
|
|
10
|
+
|
|
11
|
+
**Supplementary, always.** Nothing may live only in a HoverCard. The trigger must work on its own (the link goes to the profile; the profile has everything the card showed), because touch users never see the card and screen readers aren't told about it. In forced colours it has the floating tier's 2px border.
|
|
12
|
+
|
|
13
|
+
**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.
|
|
14
|
+
|
|
15
|
+
**You provide:** a `trigger` (a link or an avatar with its own job) and the card's content: a heading line, one or two sentences, perhaps a few numbers. Optionally `side`, `align`, `openDelay` and `closeDelay`.
|
|
16
|
+
|
|
17
|
+
- Do keep it to a glance: if people need to act on it, it's a Popover (click to open) or a page.
|
|
18
|
+
- Don't put the only copy of anything in it, and don't put a form in it.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# InlineCitation
|
|
2
|
+
|
|
3
|
+
Numbered source markers in a grounded answer ("…renews monthly [2]"), with a preview of each source on hover or focus, and `Sources`, the list at the end.
|
|
4
|
+
|
|
5
|
+
**Elevation: a citation is part of the record.**
|
|
6
|
+
- **The marker** is a small flush plate with a mono number, raised a little off the baseline like a superscript. It's `ChartAnnotation`'s build: a flush `Badge` plate, and only one that acts lifts. It acts, so it **lifts one level when pointed at**.
|
|
7
|
+
- **The preview** is a `HoverCard` at level 3: "Source 2 · example.com", the title (linked), and the excerpt the answer relied on.
|
|
8
|
+
- **Sources** is a flush ordered list under a heading. Each entry is a marker's destination, highlighted when you arrive.
|
|
9
|
+
- **An unverified source** keeps its plate, adding a `caution` mark and "Unverified source" in words.
|
|
10
|
+
|
|
11
|
+
**Accessibility.** The marker is a link named "Source 2: Billing FAQ", never "[2]". Tab reaches it, the preview opens on focus as well as hover, and Enter follows it (to its entry in Sources by default, or your `href`). Under forced colours the plate has a `LinkText` border. Under `contrast="more"` its border is `line-control`.
|
|
12
|
+
|
|
13
|
+
**Props:** `InlineCitation`: `index`, `source` (`{ title, url?, excerpt?, unverified? }`), `href`, `idPrefix`. `Sources`: `items`, `heading`, `idPrefix`.
|
|
14
|
+
|
|
15
|
+
**You provide:** the sources, numbered in the order the answer cites them. When there are several answers on a page, give each its own `idPrefix`.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<p>Plans renew monthly <InlineCitation index={2} source={{ title: 'Billing FAQ', url, excerpt }} />.</p>
|
|
19
|
+
<Sources items={sources} />
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- Do include the excerpt the answer relied on.
|
|
23
|
+
- Do mark sources you couldn't verify.
|
|
24
|
+
- Don't render citations as bare numbers or superscripts with no name.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# InlineEdit
|
|
2
|
+
|
|
3
|
+
Edit one value where its committed value already appears: by default a single-line string, a paragraph with `multiline`, or any value with a custom `editor`. Read mode shows the saved text (or “Not set”) and a visible Edit button. Editing happens in place: the value's text sits in a slot with the control's own inset and font, so when the control opens there the text doesn't move, and Save and Cancel take Edit's place on the same line. It is a group, with normal page Tab order and no focus trap.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
const [name, setName] = React.useState('Brand guidelines.pdf');
|
|
7
|
+
|
|
8
|
+
<InlineEdit
|
|
9
|
+
label="File name"
|
|
10
|
+
value={name}
|
|
11
|
+
onValueChange={setName}
|
|
12
|
+
onSave={async (draft, { previousValue }) => {
|
|
13
|
+
// Application code: enforce the record's server revision and reconcile
|
|
14
|
+
// an interrupted response before reporting a retryable failure.
|
|
15
|
+
const result = await renameFileOnServer(draft, previousValue);
|
|
16
|
+
return result.ok
|
|
17
|
+
? { ok: true, value: result.canonicalName }
|
|
18
|
+
: { ok: false, error: result.userMessage };
|
|
19
|
+
}}
|
|
20
|
+
required
|
|
21
|
+
maxLength={100}
|
|
22
|
+
/>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`value`, `onValueChange`, `onSave` and `label` are required. The application owns the committed string, authorization, validation policy and persistence. `onSave(draft, { previousValue })` is the sole write. It returns `{ ok: true, value: string }` with the canonical value, or `{ ok: false, error: string }` for a known failed write. A throw, rejection or malformed result displays “We couldn’t save this change. Try again.” without exception details.
|
|
26
|
+
|
|
27
|
+
Use an ordinary synchronous state setter for `onValueChange`: it publishes the accepted canonical string exactly once. It must not make another network write, veto success or defer publication. Do not update `value` optimistically from `onSave`; an observed external change means conflict. The captured `previousValue` helps an adapter identify its baseline, but is not a server lock. Enforce a record revision or equivalent server-side concurrency guard.
|
|
28
|
+
|
|
29
|
+
A transport timeout does not prove a write failed. Before throwing or returning a retryable failure, the adapter must reconcile the server record or retry the same logical operation ID idempotently. If the write committed, return canonical success. Only a known uncommitted write may become a new user retry. The preview includes an interrupted-after-save fixture demonstrating one committed mutation and reconciliation of the lost response.
|
|
30
|
+
|
|
31
|
+
## Editing and validation
|
|
32
|
+
|
|
33
|
+
Typing changes local text without trimming. Cancel and Escape discard that text; blur, outside clicks and Tab do nothing to the session. Enter saves (Ctrl/Cmd+Enter with `multiline`), except while IME composition is active (including legacy key code 229). Composition Escape also remains with the IME. Input Enter prevents a surrounding form from submitting.
|
|
34
|
+
|
|
35
|
+
Saving an exact unchanged value closes without validation, persistence, publication or afterglow. `required` defaults to false; when true it rejects the empty string using HTML required semantics. Spaces stay legal unless application validation rejects them. `maxLength` uses JavaScript/HTML UTF-16 string length and the native input limit. Optional `validate(draft)` runs only on Save and returns a user-facing error string or `undefined`, synchronously or as a promise. Use validation to reject whitespace-only or duplicate names when your application requires it.
|
|
36
|
+
|
|
37
|
+
While validation runs, the input is read-only and Save is unavailable; Cancel and Escape remain usable. Late validation results cannot affect a cancelled or replaced session. During persistence the input stays read-only, Save shows loading and Cancel is unavailable. Escape announces “Saving changes. Wait for the result.” It does not cancel a dispatched write. Duplicate and reentrant submissions dispatch at most once. A failed save preserves editable input and the original baseline for retry; typing clears ordinary errors.
|
|
38
|
+
|
|
39
|
+
## External updates and lifecycle
|
|
40
|
+
|
|
41
|
+
An observed `value` change during editing, validation or error retains the draft and blocks Save. The region shows the latest saved value and “Use current value”. That explicit action replaces the draft and baseline and refocuses the input; Cancel displays the latest record.
|
|
42
|
+
|
|
43
|
+
An update during a write latches a conflict even if a later observed update restores the earlier string. The request may finish, but its result is never published through `onValueChange`. The recovery message says the save may have completed and asks the person to check the current record. The application must reconcile its server state; the component cannot roll back a dispatched write or assert authoritative freshness.
|
|
44
|
+
|
|
45
|
+
Unmount invalidates pending work and suppresses subsequent publication, announcements, state and focus work. It does not abort persistence. Applications must still reconcile writes after navigation. Becoming `disabled` prevents new edits and Save while keeping an existing session cancellable and allowing a dispatched write to settle.
|
|
46
|
+
|
|
47
|
+
## Paragraphs: `multiline`
|
|
48
|
+
|
|
49
|
+
`multiline` swaps the Input for a Textarea, for a description, note or comment edited where it's shown.
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<InlineEdit
|
|
53
|
+
label="Description"
|
|
54
|
+
multiline
|
|
55
|
+
rows={4} // initial height in lines (default 3)
|
|
56
|
+
maxRows={16} // grows with its content up to here, then scrolls (default 12)
|
|
57
|
+
maxLength={2000} // the Textarea's fill from the floor and live count near the limit
|
|
58
|
+
value={task.description}
|
|
59
|
+
onValueChange={setDescription}
|
|
60
|
+
onSave={(draft) => api.setDescription(task.id, draft, task.revision)}
|
|
61
|
+
/>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Enter inserts a newline; Ctrl+Enter (⌘+Enter on Apple platforms) saves; Escape cancels. During IME composition none of these act. The hint under the field names the keys with `Kbd` and describes the textarea. Opening puts the caret at the end instead of selecting everything, because selecting a paragraph invites replacing it by accident. Read mode keeps line breaks (`white-space: pre-wrap`) in a slot with the textarea's inset, with Edit (then Save and Cancel) below it, and shows “Not set” when empty. Opening glides the region's height from the read text to the editor's; after that the textarea grows line by line without animating, and under reduced motion the height changes at once. A chat-style “Enter sends” composer is a different component.
|
|
65
|
+
|
|
66
|
+
`multiline` applies to the default editor; with a custom `editor` it's ignored. `renderValue` still draws read mode if you pass it.
|
|
67
|
+
|
|
68
|
+
## Other values: `editor`, `renderValue` and `commit`
|
|
69
|
+
|
|
70
|
+
`InlineEdit<V>` takes any value type. `renderValue(value)` draws read mode (default: the value as text, or “Not set” when it's `''`, `null`, `undefined` or an empty array). `editor(control)` draws edit mode; spread `control` onto an Altitude control. It carries `value`, `onValueChange`, the `id` the label points at, `aria-describedby` for the error or hint, `invalid`, `readOnly` (checking, saving or blocked by a conflict; changes are ignored then), `disabled` and `autoFocus`. The session is unchanged: baseline, draft, `validate`, the `onSave` contract, conflict latching, unmount safety and focus return all work the same way. Without `editor`, the existing Input is used and nothing about the string case changes.
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<InlineEdit<Status>
|
|
74
|
+
label="Status"
|
|
75
|
+
value={issue.status}
|
|
76
|
+
onValueChange={setStatus}
|
|
77
|
+
renderValue={(s) => <Badge tone={toneFor(s)}>{labelFor(s)}</Badge>}
|
|
78
|
+
editor={(control) => <Select {...control} items={statuses} />}
|
|
79
|
+
commit="on-choose"
|
|
80
|
+
onSave={(draft) => api.setStatus(issue.id, draft, issue.revision)}
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
<InlineEdit<number | null>
|
|
84
|
+
label="Estimate"
|
|
85
|
+
value={task.estimate}
|
|
86
|
+
onValueChange={setEstimate}
|
|
87
|
+
required
|
|
88
|
+
validate={(n) => Number.isInteger(n) && n >= 0 ? undefined : 'Use a whole number, 0 or more.'}
|
|
89
|
+
renderValue={(n) => `${n} points`}
|
|
90
|
+
editor={({ value, onValueChange, ...control }) => (
|
|
91
|
+
<Input {...control} type="number" value={value ?? ''}
|
|
92
|
+
onChange={(e) => onValueChange(e.target.value === '' ? null : Number(e.target.value))} />
|
|
93
|
+
)}
|
|
94
|
+
onSave={(draft) => api.setEstimate(task.id, draft, task.revision)}
|
|
95
|
+
/>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`commit="on-choose"` is for single-choice editors (Select, Combobox, a date picker): choosing a value is the deliberate act, so it dispatches the save. Save is hidden and the hint under the control reads “Choose to save”; Cancel stays, so a pointer user can leave without choosing. Choosing the current value closes without a write. A rejected save keeps the editor open on the chosen value with the error, and adds “Try again”; choosing another value saves that instead. On success the list closes first, then the editor settles, and the status region names the value (“Status set to In review”, from the rendered read value). With the default `commit="explicit"`, Enter in a custom editor's single-line text input saves, as it does in the default Input; a choice control's own Enter (opening or choosing) is left to it.
|
|
99
|
+
|
|
100
|
+
Escape inside an open popup closes the popup only; the next Escape cancels the session. `isEqual(a, b)` (default `Object.is`) decides both whether a draft is unchanged and whether an observed `value` is an external change, so pass one for object values (a date range) whose identity changes on every render. `required` rejects an empty value of any type; `maxLength` applies to strings only. A canonical `onSave` result for a custom editor may be any value except `undefined`; for the default Input it must still be a string.
|
|
101
|
+
|
|
102
|
+
## In place
|
|
103
|
+
|
|
104
|
+
The value lives in a slot that has the control's inset (12px and a transparent 1px border, as `Input` and `Select` have) and, by default, its height (40px, or the value's line height plus 18px if that's taller). The line reaches 13px past its column on each side, so the value's text lines up with the text around it and the opened well reaches into the gutter. The control takes the value's own font: a 19px title is edited as a 19px title. Opening fades in only the well's floor, walls and border around text that stays put; no visible label is added above it (the label names the control for assistive technology). Whatever still changes size glides on the real box, so what's below is pushed rather than jumped: Save and Cancel wrapping onto a line of their own at narrow widths, an error, the key hint. Leave 13px of room either side of an InlineEdit for its well.
|
|
105
|
+
|
|
106
|
+
## Ways to open: `trigger` and `hint`
|
|
107
|
+
|
|
108
|
+
`trigger` sets how editing starts:
|
|
109
|
+
|
|
110
|
+
| `trigger` | read mode | opening |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `'button'` (default) | the value, then a visible Edit button (`editLabel`) | Edit, or Enter/Space on it; the value is selected |
|
|
113
|
+
| `'value'` | one button holding the value, a plate that lifts on hover | click, Enter or Space; the value is selected |
|
|
114
|
+
| `'pencil'` | one button holding the value; a pencil appears at the end of the text on hover or keyboard focus, in space that's always reserved, so showing it moves nothing. Touch screens can't hover, so they always show it | click, Enter or Space; the value is selected |
|
|
115
|
+
| `'field'` | one button holding the value; pointing at it draws the filled input's border and floor around the text, so the click only focuses it | a click puts the caret where it landed; Enter or Space selects the value |
|
|
116
|
+
|
|
117
|
+
With every trigger except `'button'`, the button is named “<label>: <value>. <editLabel>”, `emptyLabel` names an empty value (default “Not set”), and `shortcut` puts a key in the button's `aria-keyshortcuts` for a surrounding list to handle. `PropertyList` uses `'value'` with all three.
|
|
118
|
+
|
|
119
|
+
A trigger that only shows on hover needs a sign at rest, or people won't know to point at the value. `hint="seam"` scores a faint groove under the value: a hairline cut in `line-control` with the well's light lip beneath it, where the well will open. It's an opening's edge drawn on the ground, not a link's underline, so it reads as something that can be opened rather than followed. With `'field'` the field's own border replaces it on hover. It's flat in `contrast="more"` and dashed in forced colours, and not drawn for an empty value, which already shows its own words. Default `hint="none"`.
|
|
120
|
+
|
|
121
|
+
## Focus and accessible equivalents
|
|
122
|
+
|
|
123
|
+
Opening focuses and selects the input (with a custom `editor`, it focuses the editor's first focusable control). Save or Cancel restores focus to Edit only when focus was still in the editor at completion. Completion never steals focus from another part of the page. If Edit is unavailable while the component remains mounted, `focusFallback` may return a connected, enabled, focusable element; otherwise it leaves focus where it is. When a parent removes a row, the parent owns next-row or heading focus.
|
|
124
|
+
|
|
125
|
+
A visually hidden label and a single alert wire the input's name, description and invalid state. A polite status announces checking, saving and accepted success outside the saving region's `aria-busy` subtree; it's never shown, and clears itself after `dur-linger`. The editor stays on the ground with its well open, persistence uses Surface's pending signal, failed writes use its failed treatment, and accepted canonical text settles with an afterglow that fills the value's slot, so it never sits against the text. Text, controls, error icons and borders preserve every state without motion or shadows. Actions wrap below the field at narrow widths.
|
|
126
|
+
|
|
127
|
+
Optional labels are `editLabel` (default `Edit ${label}`), `saveLabel` (“Save changes”) and `cancelLabel` (“Cancel”). `settleDuration` sets how long a saved value's afterglow takes to fade, in ms (default 5000, shorter than `dur-settle` because the person is watching the change). `className` applies to the local group. There is no autosave, history, portal or form abstraction.
|
|
128
|
+
|
|
129
|
+
## Demo and verification
|
|
130
|
+
|
|
131
|
+
The canonical `InlineEdit` section in `build/previews.txt` renders a task title (in its heading font), a multiline description, the task's status (Select, on choose, with a rejected “Blocked”), assignee (Combobox, on choose) and estimate (a numeric Input), an independent filename table cell with this component, and “Ways to open”: a pencil and two fields on hover, with seams. A collapsed documentation harness selects deterministic fixtures before mounting, resets records and counters, delays validation, applies external updates, removes the filename consumer during work and opens a dialog comparison using the same validation and persistence adapter. Fixture controls and operation logs stay outside the product scene.
|
|
132
|
+
|
|
133
|
+
Run `node checks/inline-edit.mjs` from `build/` after generating preview fixtures. `ALT_FIXTURE_ROOT=test/react19` selects the React 19 source fixtures. The checks cover editing in place (the value's text doesn't move, the control takes its font, the pencil and the field move nothing on hover, the caret lands where clicked, the seam, the announcement that clears itself), multiline (newline on Enter, Ctrl/Cmd+Enter, IME, Escape, caret at the end, growth to `maxRows`, line breaks in read mode), custom editors (focus, Escape layering, on-choose saves and rejections, unchanged choices, numeric validation), lifecycle races, retained input, uncertainty reconciliation, conflict latching, keyboard/IME, focus, disabled behavior and the four-scheme responsive/accessibility matrix. Automated checks do not replace the human screen-reader, touch, magnification or formative comparisons recorded in [Actions in place validation](https://github.com/richlaconte/altitude/blob/v0.4.0/project/ActionsInPlaceValidation.md).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Input
|
|
2
|
+
|
|
3
|
+
A single-line text field. A field is an **opening cut into the page**, and you can see into it.
|
|
4
|
+
|
|
5
|
+
Altitude has one camera, slightly in front of and above the page: under a raised face you see its front wall (a button's wall), and inside an opening you see its **back wall**, just below the top edge. That inside wall (`--alt-wall-in`, 4px) is what makes an empty field read as a hole you can put something into.
|
|
6
|
+
|
|
7
|
+
- **Empty:** a well (`surface-well`) with its inside wall showing and a soft occlusion shadow on the floor below it. The placeholder sits on the floor, half a wall lower than it would on a flat field.
|
|
8
|
+
- **Approached:** as the pointer comes within 56px the wall deepens up to 2px more, the way a receptacle opens for something about to be put in. Content sinks with it. Only the opening changes: the field itself never moves. Fields are the one place Altitude responds to a pointer before it arrives; buttons and cards wait until it's on them.
|
|
9
|
+
- **Filled:** as soon as it holds a value, the floor rises flush with the page: the wall shrinks to nothing on `ease-settle`, the text rises with it, and the field turns `surface-raised`. A form's unfilled fields read as holes at a glance.
|
|
10
|
+
- **Focused:** the floor lights up (`surface-raised`), and an empty focused field holds the extra depth approach previewed, even after the pointer moves away, until something is put in it. A field you click into shows nothing more: you know where you clicked, and the caret is there.
|
|
11
|
+
- **Focused from the keyboard:** the rim also darkens to a 2px neutral (`--alt-rim-held`: `ink` mixed 80% into `line-control`, a 1px border doubled by a 1px outline), so you can find the field you tabbed to. No accent and no halo: a form you're working through shouldn't glow at every field. The rim keeps at least a 3:1 change from the resting rim in every scheme, so it meets WCAG's focus-appearance contrast. Whether focus came from the keyboard is decided as it arrives (`useKeyFocus`), so typing into a field you clicked doesn't add the rim, and clicking a field removes it. An invalid field's keyboard rim stays `critical`.
|
|
12
|
+
- **Invalid:** dropped to trench depth (a 6px wall in a `critical` tint, on `critical-soft`), whether or not it's filled. It drops on `ease-sink` rather than shaking.
|
|
13
|
+
- **Disabled:** flat on the ground with a `line` rim, no wall.
|
|
14
|
+
- **Suggested:** a value proposed for the field (an AI completion, autofill, a template) is drawn as a **ghost of the value** in `ink-faint`, the feedforward vocabulary ("the ghost of the check a box would get"). With `suggestion` alone it's the whole value while the field is empty, or the rest of it while what's typed matches its start. With `suggestionMode="append"` it continues the text. **Tab accepts** (the floor rises flush with one afterglow, and `onChange` and `onAcceptSuggestion` hear the new value), **Escape dismisses**, and typing carries on past it. **A suggested value isn't a value**: it's never put in the field, so an empty field with a ghost stays an opening. It's read out as the field's description ("Suggestion: Billing. Press Tab to accept."), Tab moves focus as usual when no ghost is showing, and acceptance is announced. Under forced colours the ghost is `GrayText`.
|
|
15
|
+
|
|
16
|
+
The shadow list is always four layers (wall, occlusion, lip, halo; the halo is unused by Altitude's own states and kept so custom states animate), so every state animates into every other. Empty and filled are detected with CSS (`:placeholder-shown`), so it works for controlled and uncontrolled inputs; when you don't pass a placeholder, a blank one is set for you. In forced-colours mode empty fields have a dashed rim, filled ones a solid rim and invalid ones a thick dashed rim.
|
|
17
|
+
|
|
18
|
+
`Textarea` is the same opening, multi-line and resizable. Give it a `maxLength` and it shows **how full** it is: a faint fill rises from the floor with a waterline at its top, turning `caution` at 90% and `critical` at the limit, with a live count below.
|
|
19
|
+
|
|
20
|
+
**On touch.** Under a coarse pointer the field steps up to `size-field-md`'s 48px, and the text you type is at least 16px, so iOS Safari never zooms the page when the field is focused. Labels stay 14px. See [Theming](../../visual/Theming.md), "Density and target size".
|
|
21
|
+
|
|
22
|
+
**You provide:** `placeholder` in `ink-muted` (a hint, never the label), `size` (`sm` 34px, `md` 40px), optional `leading` and `trailing` nodes (16px icons, a `Kbd`, a unit). For Textarea, optional `maxLength` and `showFill={false}` to hide the fill. For either, an optional `suggestion`, `suggestionMode` and `onAcceptSuggestion`. Wrap either in `Field` for a label and messages.
|
|
23
|
+
|
|
24
|
+
- Do use `Field` rather than a placeholder as the only label.
|
|
25
|
+
- Don't add an accent focus ring or glow to fields, or a rim on click. Keyboard focus on buttons and choice controls is the `accent-ink` ring; on a field the lit floor is the focus, plus the dark rim when you arrived by keyboard.
|
|
26
|
+
- Don't put a suggestion in the value: pass it as `suggestion`, so people can tell what they wrote from what was proposed.
|
|
27
|
+
- Don't place inputs on `surface-well` backgrounds: a well inside a well disappears. Put forms on ground or raised surfaces.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# InputOTP
|
|
2
|
+
|
|
3
|
+
A one-time code: two-factor sign-in, email verification, device approval. Four to eight characters, shown as a row of openings.
|
|
4
|
+
|
|
5
|
+
**Elevation.** Completeness, applied per character. Each slot is built like an `Input`: an empty slot is a hole with its back wall showing, and **its floor rises flush as its character lands**, so the code shows how far along it is. The slot the next character goes into is held: its floor is lit, it opens to the depth approach would preview, it shows a caret, and it gets the neutral rim when reached by keyboard. A pasted or autofilled code fills left to right, 35ms a slot. A rejected code drops the whole row to a trench on `critical-soft`, with the message from `Field`. While the code is being checked (`verifying`), nothing more can be typed.
|
|
6
|
+
|
|
7
|
+
**One field, not six.** A single real `<input inputmode="numeric" autocomplete="one-time-code">` lies invisible over the row and drives the slots. So SMS and email autofill, paste (into any slot) and screen readers all see one field. Characters that don't match `pattern` are dropped, so pasting "123-456" gives 123456.
|
|
8
|
+
|
|
9
|
+
**Accessibility.** One label (from `Field`), described as "6 digits", with errors through `Field`. There are no announcements per slot: the slots are `aria-hidden`, and the input is what's read. Typing, Backspace, Left and Right, Home and End work as they do in any text field. `mask` shows • and makes the input a password field. Under forced colours empty slots are dashed and filled ones solid. Under reduced motion the caret doesn't blink and floors change without travel.
|
|
10
|
+
|
|
11
|
+
**Parts.**
|
|
12
|
+
- `InputOTP`: `maxLength`, `value`, `defaultValue`, `onChange`, `onComplete`, `pattern` (`'digits' | 'alphanumeric' |` a RegExp for one character), `mask`, `invalid`, `verifying`, `disabled`.
|
|
13
|
+
- `InputOTPGroup`, `InputOTPSlot` (`index`) and `InputOTPSeparator` lay the slots out. Without children, it's one group of `maxLength` slots.
|
|
14
|
+
|
|
15
|
+
**You provide:** the length, what happens on `onComplete` (verify the code), and where the code was sent, in `Field`'s hint.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<Field label="Verification code" hint="We sent it to j•••@acme.com." error={error}>
|
|
19
|
+
<InputOTP maxLength={6} value={code} onChange={setCode} onComplete={verify}>
|
|
20
|
+
<InputOTPGroup>{[0, 1, 2].map((i) => <InputOTPSlot key={i} index={i} />)}</InputOTPGroup>
|
|
21
|
+
<InputOTPSeparator />
|
|
22
|
+
<InputOTPGroup>{[3, 4, 5].map((i) => <InputOTPSlot key={i} index={i} />)}</InputOTPGroup>
|
|
23
|
+
</InputOTP>
|
|
24
|
+
</Field>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- Do verify on `onComplete`, without a separate Submit button.
|
|
28
|
+
- Do say where the code went and how to get a new one.
|
|
29
|
+
- Don't split the code into several inputs; autofill and paste break.
|
|
30
|
+
- Don't clear the code on a rejection without saying why; leave it so it can be corrected.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# ItemGroup
|
|
2
|
+
|
|
3
|
+
A list of records shown as slats (media, title, description, actions), changed in the list itself: edit a slat in place, add one at the end, reorder by picking one up, remove one with real recovery. Settings lists (API keys, webhooks, team members), option lists (a form builder's choices) and “add another” sections. It's the Files recipe from [Actions in place](../../visual/ActionsInPlace.md) as a component; the editor is [EditRegion](../EditRegion/README.md).
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
<ItemGroup<Webhook>
|
|
7
|
+
aria-label="Webhooks"
|
|
8
|
+
items={webhooks}
|
|
9
|
+
getKey={(w) => w.id}
|
|
10
|
+
getLabel={(w) => w.name}
|
|
11
|
+
renderItem={(w, { editProps, removeProps }) => (
|
|
12
|
+
<Item>
|
|
13
|
+
<ItemMedia><WebhookIcon /></ItemMedia>
|
|
14
|
+
<ItemContent><ItemTitle>{w.name}</ItemTitle><ItemDescription>{w.url}</ItemDescription></ItemContent>
|
|
15
|
+
<ItemActions>
|
|
16
|
+
<Button size="sm" variant="ghost" {...editProps}>Edit</Button>
|
|
17
|
+
<Button size="sm" variant="ghost" {...removeProps}>Remove</Button>
|
|
18
|
+
</ItemActions>
|
|
19
|
+
</Item>
|
|
20
|
+
)}
|
|
21
|
+
renderEditor={(field) => <>
|
|
22
|
+
<Field label="Name" error={field.error('name')}><Input {...field('name')} /></Field>
|
|
23
|
+
<Field label="Endpoint URL" error={field.error('url')}><Input type="url" {...field('url')} /></Field>
|
|
24
|
+
</>}
|
|
25
|
+
onSave={(draft, { previousValue }) => api.updateWebhook(draft, previousValue.revision)} // EditRegion's contract
|
|
26
|
+
onItemChange={(w) => setWebhooks((list) => list.map((x) => (x.id === w.id ? w : x)))}
|
|
27
|
+
newItem={() => ({ id: '', name: '', url: 'https://' })}
|
|
28
|
+
onCreate={(draft) => api.createWebhook(draft)} // { ok: true, value } after items includes it
|
|
29
|
+
addLabel="Add webhook"
|
|
30
|
+
onReorder={(next) => api.reorder(next.map((w) => w.id))} // shows grips
|
|
31
|
+
onRemove={(w) => api.archiveWebhook(w.id)}
|
|
32
|
+
removal={{ kind: 'recoverable', undo: (w) => api.restoreWebhook(w.id) }}
|
|
33
|
+
empty={<EmptyState size="sm" title="No webhooks yet" />}
|
|
34
|
+
/>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`removal` has no default, on purpose: say whether removal is recoverable (it happens at once, and Undo calls your real inverse operation) or needs confirmation (`{ kind: 'confirm', title, description?, confirmLabel? }`: a stationary `Dialog` naming the record, Cancel focused). A danger button doesn't imply irreversibility.
|
|
38
|
+
|
|
39
|
+
A recoverable removal leaves its result where the slat was, as a [RemovalReceipt](../RemovalReceipt/README.md): “Webhook is removed.” with Undo, at the slat's height, so nothing moves while it's read. After the hold the slot closes, and Undo moves to a toast that the group raises itself. Render a `Toaster` once in your app for it to appear. The toast is a supplement to Undo, not the only route back, so keep a durable recovery route of your own (Archived → Restore).
|
|
40
|
+
|
|
41
|
+
## Elevation
|
|
42
|
+
|
|
43
|
+
- **Slats** are the record: flush on the ground with `line` dividers, as Table rows, in a plain `ul`. Actions are visible ghost buttons, not hover-only.
|
|
44
|
+
- **Editing**: one slat at a time lifts into its EditRegion, a static raised card holding the fields; its neighbours stay flush and readable. Opening another while one is open keeps the first and moves focus back to it.
|
|
45
|
+
- **The add slat** is an opening: a dashed well (an `EmptyState`-style button) that deepens to a trench under the pointer or focus. Activating it rises into an empty editor in the same place; once added, the new slat settles with an afterglow and a fresh add slat is ready below.
|
|
46
|
+
- **Reordering**: a slat picked up by its grip floats at level 3, tilted, shown where it will land; the slot it leaves opens as a well.
|
|
47
|
+
- **Pending** removals and reorders hover at level 2 after `dur-grace`; known failures are a critical Alert, and the list is unchanged.
|
|
48
|
+
- **Removed** (recoverable): once `onRemove` resolves, the slat's contents cross-fade (`dur-travel-2`) to its receipt in the same slot: a check, “<label> is removed.” in `ink-muted` on the ground, and Undo.
|
|
49
|
+
- **The hold.** The receipt holds for `dur-linger`, pausing while it's pointed at or focused and waiting while a mouse is still over the list. Then it lifts off and the gap glides shut (`dur-travel-3`, `ease-glide`). Removals due within 250ms of each other close together.
|
|
50
|
+
- **Undo during the hold.** The slat cross-fades back and settles with an afterglow; nothing moves.
|
|
51
|
+
- **Undo after the close** is in a toast (“<label> is removed.”, Undo). That Undo glides the slot open where it was, then the slat settles. The toast is withdrawn if the item comes back another way.
|
|
52
|
+
- **Removed** (confirmed): the slat glides shut where it was instead of vanishing. There's no receipt, because there's nothing to undo.
|
|
53
|
+
|
|
54
|
+
Forced colours: the add slat and slot dashed, the carried slat 2px, pending dotted, a receipt outlined in `CanvasText`. `contrast="more"`: the carried slat gets a 3px slab.
|
|
55
|
+
|
|
56
|
+
## Keyboard and focus
|
|
57
|
+
|
|
58
|
+
Tab reaches each slat's controls in order. Edit opens the editor and focuses its first field; Enter in a single-line field saves and Escape cancels (EditRegion's rules), and focus returns to that slat's Edit button. Add focuses the new editor; after it's added, focus goes to the fresh add slat, so several can be added in a row. Remove moves focus to the next slat's Remove, else the previous one's, else the add slat or the list. A receipt's Undo stays in the tab order while it's held (Shift+Tab reaches it), and the hold doesn't expire while focus is in it. After Undo, focus goes to the restored slat's Remove. Grips follow SortableList: Space picks up, ↑/↓ move, Space drops, Escape cancels, with positions announced assertively (“Position 2 of 5”). One polite status announces saved, added, removed (confirmed), restored and errors. A recoverable removal is announced by its receipt (“<label> is removed. Undo is available.”), not by the status as well, and the toast after the close isn't announced again. Under reduced motion the receipt still fades and holds, and the gap closes at once.
|
|
59
|
+
|
|
60
|
+
## Props
|
|
61
|
+
|
|
62
|
+
| Prop | |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `items`, `getKey`, `getLabel`, `renderItem` | the slats; `renderItem` gets `edit`, `remove`, and `editProps`/`removeProps` to spread onto visible buttons (name, handler, focus repair) |
|
|
65
|
+
| `renderEditor`, `onSave`, `onItemChange` | the editor's fields; EditRegion's save contract; publish the canonical record |
|
|
66
|
+
| `newItem`, `onCreate`, `addLabel` | the add slat |
|
|
67
|
+
| `onReorder` | grips; resolve after `items` is in the new order |
|
|
68
|
+
| `onRemove`, `removal` | removal and its stated recovery policy |
|
|
69
|
+
| `empty`, `aria-label`, `aria-labelledby` | |
|
|
70
|
+
|
|
71
|
+
Parts: `Item`, `ItemMedia` (`variant` icon, image or avatar), `ItemContent`, `ItemTitle`, `ItemDescription`, `ItemActions`, `ItemSeparator`.
|
|
72
|
+
|
|
73
|
+
## You provide
|
|
74
|
+
|
|
75
|
+
The records, the writes and their concurrency guards, and the recovery policy and its inverse operation. Pointer reordering, multi-select and bulk actions aren't covered yet.
|
|
76
|
+
|
|
77
|
+
## Do and don't
|
|
78
|
+
|
|
79
|
+
- **Do** state the removal policy honestly; offer Undo only when you can really reverse it.
|
|
80
|
+
- **Do** keep the editor short; past about five fields, open the record in a `Sheet`.
|
|
81
|
+
- **Don't** hide Edit and Remove behind hover or a “…” menu when there are only one or two.
|
|
82
|
+
|
|
83
|
+
## Demo and verification
|
|
84
|
+
|
|
85
|
+
The `ItemGroup` section of `build/previews.txt` shows webhooks (edit, add, reorder, recoverable removal) and a form builder's answer options (empty state, confirmed removal). `node checks/item-group.mjs` (from `build/`; `ALT_FIXTURE_ROOT` selects React 19 fixtures) covers editing and focus return, one editor at a time, adding with field errors and several in a row, keyboard reordering with cancel, confirmed removal with Cancel focused, the empty state, and removal. For removal it checks:
|
|
86
|
+
- the receipt in place with focus repair and a single announcement, and Undo during the hold, with every frame sampled to show nothing below moves;
|
|
87
|
+
- the hold of `dur-linger`, then the gap gliding shut;
|
|
88
|
+
- the toast's unannounced Undo gliding the slot back open;
|
|
89
|
+
- the hold pausing on focus, several removals closing together, reduced motion, and a confirmed slat gliding shut.
|
|
90
|
+
|
|
91
|
+
It also renders and 320/960px layouts in both schemes under reduced motion, forced colours and `contrast="more"`.
|
|
92
|
+
**Narrow.** When the group is under 480px wide, `ItemActions` moves to its own row under the content, so the title keeps its width; `narrow="inline"` keeps the actions beside it. `ItemDescription truncate` (or a number of lines) clamps a long description.
|
|
93
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Kbd
|
|
2
|
+
|
|
3
|
+
Shows a keyboard key as a keycap: a tiny slab with a 2px wall that presses down when the person presses that key.
|
|
4
|
+
|
|
5
|
+
Keys sit at level 1 in the mono family with `wall-1` under them. With `listen`, the cap drops to the ground (wall gone) while the matching key (a `KeyboardEvent.key`, case-insensitive) is held: a small confirmation that a shortcut registered.
|
|
6
|
+
|
|
7
|
+
**On touch.** `hideOnTouch` hides a key cap on a touch-only device (no hover, a coarse pointer) until a key is pressed in the session, for a hint that only makes sense with a keyboard. It's off by default, because an app may be documenting its shortcuts on purpose; Altitude's own hints (menu shortcuts, CommandPalette's keys, InlineEdit's save hint) already do this. `useKeyboardLikely()` gives the same answer to your own components.
|
|
8
|
+
|
|
9
|
+
**You provide:** the key's glyph as children (one `Kbd` per key: `<Kbd>⌘</Kbd><Kbd>K</Kbd>`), and `listen` when the shortcut is live on this page.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# LineChart
|
|
2
|
+
|
|
3
|
+
Change over time: latency over a day, revenue over a year, one series or a few compared. It renders inside a [`ChartContainer`](../ChartContainer/README.md), which places x by time (ISO dates, timestamps) or by number.
|
|
4
|
+
|
|
5
|
+
**Why it's flat.** Lines are the record, so they lie on the plot: 2px strokes in their series' colours, round joins and caps, no fill, no glow, no shadow. A missing value (`null`) is a gap in the line, never bridged: nothing was recorded there, as `Await` tells `null` from not-yet. `curve="monotone"` curves through the points without overshooting them (Fritsch–Carlson), so a curve never invents a peak or a dip the data doesn't have. The last point wears an 8px marker, and each line is named at its end in `ink` (up to four series), so the lines are their own legend; end labels are nudged at least 16px apart where lines converge.
|
|
6
|
+
|
|
7
|
+
**The axis.** Unlike bars, a line's y axis doesn't have to include zero, but its range is always labelled. When zero isn't on it, the plot's foot is drawn as an ordinary gridline, not the ground line, so nothing reads as a baseline that isn't one.
|
|
8
|
+
|
|
9
|
+
**Without colour.** Under forced colours and in print, and with `patterns` on the container, each line takes its slot's dash and every point its slot's marker (circle, square, triangle, diamond, ring, cross), in `CanvasText` when forced. With `contrast="more"` lines are 3px.
|
|
10
|
+
|
|
11
|
+
**Motion.** When the data changes and the x values are the same, the line morphs on `ease-glide`; when they differ (a new range), the old chart and the new one cross-fade. A rescale comes first. There's no draw-on animation on first render ([Motion](../../visual/Motion.md), "Charts").
|
|
12
|
+
|
|
13
|
+
**Accessibility.** Without a written `summary`, the generated one says which way each series went between its first and last values ("p95 rises from 168 ms at 14 Mar, 12:00 AM to 207 ms…"), and its lowest and highest points. The keyboard path along x and between series is #63, the readout #52, and the table view #64.
|
|
14
|
+
|
|
15
|
+
**Props:** `data` and `x`; `curve` (`'linear'` or `'monotone'`); `y={{ format }}` to format the axis; `endLabels` to turn end labels on or off (default on for up to four series, and always under forced colours and in print). `status` and `boundary` (below); `band={{ low, high, series?, label? }}` for an uncertainty band. `linePath(points, curve)` is exported for your own marks: it draws the same path, gaps and all.
|
|
16
|
+
|
|
17
|
+
**Selecting and drilling.** `selected` and `onSelect` make the marks a selection that cross-filters the rest of a dashboard, and `drill` opens a level down inside a `Drilldown`. Hover only promises what a click does. See [ChartContainer](../ChartContainer/README.md), "Marks you can select or drill into".
|
|
18
|
+
|
|
19
|
+
**Filtering in place.** `relevance={(d) => 'match' | 'miss' | undefined}` recedes misses to 35% and keeps everything in place; `rescale` fits the axis to the matches. See [ChartContainer](../ChartContainer/README.md), "Filtering in place".
|
|
20
|
+
|
|
21
|
+
**Not yet the record.** `status={(d) => d.day > today ? 'projected' : undefined}` dashes the line (5 4) from the last settled point on, and draws those points hollow: ringed in the series colour on the plot's own. It's one path clipped two ways, so a curve doesn't kink where the record ends. A 1px `line-control` rule named "Today" marks the boundary. Point names, the readout's heading and the table say "Projected" or "So far", and the generated summary reads its trend from the settled points only, then says what's projected. While patterns are on, a line that has its own dash keeps it, and the hollow points carry the difference.
|
|
22
|
+
|
|
23
|
+
**A sliding window.** `window={{ minutes: 15 }}` (or `hours`, `days`, or `points` for the last N records) shows only the latest stretch, and the axis spans exactly that window, however sparse the data. In a `live` container, when the window moves on, x glides one step and the line rides along with it.
|
|
24
|
+
|
|
25
|
+
**A chosen window.** `window={{ start, end }}` shows a fixed stretch of x, both ends included; a `DateRange` fits as it is. Put a `ChartBrush` inside the chart to pick it on an overview, with `onWindowChange` to hear where it moved.
|
|
26
|
+
|
|
27
|
+
**Uncertainty is a band.** `band={{ low: 'p10', high: 'p90', label: 'Range 10th–90th percentile' }}` draws a flat fill of the series colour at 16% between the two keys, under the lines. It's never error bars, which readers misjudge (Correll & Gleicher 2014), and never a gradient. Its edges count toward the y axis. The readout adds a row for it ("499–787 Range 10th–90th percentile"), and so do point names and the table.
|
|
28
|
+
|
|
29
|
+
**Banked to 45°.** `aspect="banked"` lets the data choose the chart's height: slopes read best when the segments average about 45° (Cleveland's banking), so the plot takes the width ÷ height that sets their mean absolute orientation there, held between 1 and 4 for the data area so a card is never a sliver or a tower. Steep, jagged data banks wide; a gentle trend banks tall. The width stays the card's. It's read from every shown series, and kept while values glide; only new x values, which cross-fade as a new chart, bank it afresh, and the height change itself never animates. Put it on the `LineChart` or on the container. In a grid of equal cards, keep a fixed `aspect`: banked neighbours would differ in height.
|
|
30
|
+
|
|
31
|
+
**Focus.** `focus={['team']}` keeps those lines' colours and draws the rest in `data-context`, their end labels in `ink-muted`; the readout and the keyboard path list the focused lines first. See [ChartContainer](../ChartContainer/README.md), "Focus".
|
|
32
|
+
|
|
33
|
+
**Against the record before.** `compare={{ data: lastWeek, label: 'Last week' }}` matches records by position (or `key` reads a field on each record) and draws the earlier period as a 1.5px solid `ink-muted` trace under the compared series (`series`, default the first), without points, named at its end in `ink-muted`. It's solid because dashes mean projected, and it follows the data, which a 1px straight reference line never does. The readout, point names, the table and the summary say the difference.
|
|
34
|
+
|
|
35
|
+
**You provide:** a `ChartContainer` with a `config` naming each series, the `data` in x order, and usually a `summary`.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
<ChartContainer config={{ p50: { label: 'p50' }, p95: { label: 'p95' } }} aspect={2.5}>
|
|
39
|
+
<LineChart data={latency} x="minute" curve="monotone" y={{ format: (ms) => `${ms} ms` }} />
|
|
40
|
+
</ChartContainer>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- Do keep it to four series or fewer; past that, use small multiples, or a legend and the readout.
|
|
44
|
+
- Do leave missing values as `null`; don't fill them with zero or the last value.
|
|
45
|
+
- Do use `monotone` for smooth measures, and `linear` when each point is a discrete count.
|
|
46
|
+
- Don't fill the area under a line to make it look richer; that's `AreaChart`, for a total and its parts.
|
|
47
|
+
- Don't put two measures of different scale on one plot; that's two charts.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Link
|
|
2
|
+
|
|
3
|
+
Navigation: a link in prose, "Learn more" after a hint, "View all invoices →" at the end of a card, an external link to docs.
|
|
4
|
+
|
|
5
|
+
**Elevation.** A link is text in the record, so it lies on the ground and **never lifts**. Lifting a word would break its line, and height means "press it", which is what a `Button` is for. It's `accent-ink` with an underline, so it's never told apart by colour alone. Hover thickens the underline from 1px to 2px over `dur-travel-1`. Focus is the 2px `accent-ink` ring at a 2px offset. A standalone link ("View all invoices →") is still ground.
|
|
6
|
+
|
|
7
|
+
**Link or button?** A link goes somewhere (it has an `href`). A button does something. Don't use a ghost `Button` for navigation: it has the wrong semantics, and it lifts on hover like a slab.
|
|
8
|
+
|
|
9
|
+
**External links.** `external` opens a new tab with `rel="noopener noreferrer"`, ends with an external-link icon, and adds "(opens in a new tab)" for screen readers.
|
|
10
|
+
|
|
11
|
+
**Router links.** `asChild` puts the styles and behaviour on your own element (a router's `Link`) instead of rendering an `<a>`.
|
|
12
|
+
|
|
13
|
+
**Accessibility.** It's a plain `<a href>`, so Enter follows it. Under forced colours it's `LinkText`. Under `contrast="more"` the underline is always 2px. `visited` turns a followed link's underline `ink-muted` (off by default; it helps in long lists of documents).
|
|
14
|
+
|
|
15
|
+
**Props:** `href` and every anchor attribute; `external`; `asChild`; `visited`.
|
|
16
|
+
|
|
17
|
+
**You provide:** where it goes, and link text that says where (not "click here").
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<Link href="/docs/webhooks">Read about webhooks</Link>
|
|
21
|
+
<Link href="https://status.example.com" external>Status page</Link>
|
|
22
|
+
<Link asChild><NextLink href="/billing">Manage billing</NextLink></Link>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Do write link text that makes sense on its own ("Read about webhooks", not "here").
|
|
26
|
+
- Do use `external` for anything that leaves your app.
|
|
27
|
+
- Don't use a link for an action (Save, Delete); that's a `Button`.
|
|
28
|
+
- Don't colour a link yourself, or take its underline away inside prose.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Markdown
|
|
2
|
+
|
|
3
|
+
An answer's Markdown, set as Altitude prose, as it streams. Models answer in Markdown; `Markdown` renders it in the system's type and hands the parts Altitude already has to their components: fenced code to `CodeBlock`, links to `Link`, `[n]` markers to `InlineCitation`, tables to `Table`'s flush rows, `---` to `Separator`.
|
|
4
|
+
|
|
5
|
+
**Elevation: the record's typography, adding no height.**
|
|
6
|
+
- **Paragraphs, lists and headings** are on the ground. A `#` heading is the heading style (never the title, which belongs to the page), weight 600; `##` and below step down.
|
|
7
|
+
- **Inline code** has a 1px `line` rim and no fill. **Fenced code** is a `CodeBlock` titled with its language.
|
|
8
|
+
- **Tables** are `Table`'s flush rows with `line` dividers, in a frame that scrolls sideways when the table is wider than the message.
|
|
9
|
+
- **Blockquotes** have a 2px `line` rule at their start. **Task items** have a read-only stamped check.
|
|
10
|
+
- **A citation** is the one part that lifts, by one, when pointed at.
|
|
11
|
+
- **An image** waits in a sunk imprint at a 16:9 size and rises out of it when it loads; if it fails, its alt text shows.
|
|
12
|
+
- **While streaming** an open fence is a streaming `CodeBlock` (dashed rule, "Writing…", `aria-busy`), a table shows nothing until its delimiter row arrives and never a half-received row, and an unclosed `**`, backtick or link shows its words without the markers. The answer as a whole is still carried by `Message status="streaming"`.
|
|
13
|
+
|
|
14
|
+
**Accessibility.** Plain HTML: headings offset by `headingLevel` (default 3, so `#` is `h3` under the conversation's), lists, `table` with a header row, `blockquote`, `pre`/`code`, `a`. A table is in a focusable region named after its first columns, so it can be scrolled by keyboard. Markdown announces nothing; `Conversation` announces an answer once it completes. Off-site links say "(opens in a new tab)".
|
|
15
|
+
|
|
16
|
+
**Safety.** Raw HTML in the source is shown as text unless `allowHtml`. URLs pass through `transformUrl`, which by default refuses `javascript:`, `vbscript:`, `file:` and `data:` (except images); a refused link keeps its words.
|
|
17
|
+
|
|
18
|
+
**Props:** `children` (the source), `streaming`, `citations`, `idPrefix`, `syntax`, `headingLevel`, `components` (`a`, `img`, `code`, `pre`, `table`, `blockquote`), `allowHtml`, `transformUrl`. Also exported: `parseMarkdown`, `safeUrl`.
|
|
19
|
+
|
|
20
|
+
**You provide:** the text, `streaming` while it arrives, and the sources its `[n]` markers refer to. It covers CommonMark plus GFM tables, task lists, strikethrough and bare links, with no dependencies; footnotes, math and reference-style links aren't parsed. For those, render with your own parser and pass its parts through `components`.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<Message from="assistant" status={m.status}>
|
|
24
|
+
<MessageContent>
|
|
25
|
+
<Markdown streaming={m.status === 'streaming'} citations={m.sources} syntax={highlight}>{m.text}</Markdown>
|
|
26
|
+
</MessageContent>
|
|
27
|
+
</Message>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- Do pass `streaming` while the answer arrives, so half-received constructs don't flash as raw text.
|
|
31
|
+
- Do give each answer its own `idPrefix` when several cite sources on one page.
|
|
32
|
+
- Don't turn on `allowHtml` for model output or anything else you don't control.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Meter
|
|
2
|
+
|
|
3
|
+
Where a reading sits in a known range: storage used of a quota, seats used of a plan, CPU, a score. It isn't a task's progress: a meter doesn't move toward done, and being near the top can be bad. With `segments` it shows parts of a whole (storage by type), the honest replacement for a donut chart.
|
|
4
|
+
|
|
5
|
+
**Why it's flush, not a trench.** `Progress` and `Meter` look alike and mean different things (see [Charts](../../visual/Charts.md)). `Progress` is a task under way, which is live, so its fill rises out of a trench. A meter's reading is the record, so it lies on the ground: a flush track of `line` with the reading drawn across it as flat ink (`data-1`), with no shadow, wall or sheen.
|
|
6
|
+
|
|
7
|
+
**Thresholds.** `high` is where the reading is near its limit and `critical` where it's over (by default, anything above `max`). Each threshold inside the range is a 1px `ink-muted` notch standing at the track's rim. Past `high` the fill turns `caution`, and past the limit `critical`, and the meter says so in words ("Near limit", "Over limit") on a `Badge` whose lamp relights when the state changes. Thresholds assume that higher is worse, as for quotas and load. For a score where lower is worse, leave them off and say the state in your own words.
|
|
8
|
+
|
|
9
|
+
**Segments.** Each part is a flat `data-*` fill in config order (`data-1`, `data-2`, …), parted from the one before by a 2px cut in the surface's own colour, with a legend of each part's value beneath. The segments add up to the reading, and thresholds still apply to the total.
|
|
10
|
+
|
|
11
|
+
**Motion.** The fill glides to a new reading on `ease-glide` over `dur-travel-3`, as Progress does, and its colour cross-fades when it crosses a threshold. Under reduced motion it changes in place. While the reading loads (`value` undefined) the track is an imprint and lands like `Await` content.
|
|
12
|
+
|
|
13
|
+
**Accessibility.** The [APG meter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/meter/): `role="meter"` named by its label, with `aria-valuemin`, `aria-valuemax` and `aria-valuenow` (kept within the range when the reading is over it) and an `aria-valuetext` in words: "Storage, 62 GB of 100 GB", "11,800 of 10,000, over limit", "62 GB of 100 GB, Media 38 GB, Documents 14 GB…". Under forced colours the track is outlined, the fill is `Highlight`, the notches are `CanvasText`, and the state is in the words. Under `contrast="more"` the track gets a 1px `ink` edge and the notches are `ink`.
|
|
14
|
+
|
|
15
|
+
**Props:** `label`; `value`; `min` (default 0); `max` (default 100); `high`; `critical`; `segments` (`{ key, label, value, color? }[]`); `format`; `states` (`{ high, critical }` words); `hideValue`; `locale`.
|
|
16
|
+
|
|
17
|
+
**You provide:** the reading and its range, the thresholds that matter, and a `format` that names the unit.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<Meter label="Storage" value={62} max={100} high={80} format={(n) => `${n} GB`} />
|
|
21
|
+
<Meter label="Storage by type" max={100} format={(n) => `${n} GB`}
|
|
22
|
+
segments={[{ key: 'media', label: 'Media', value: 38 }, { key: 'docs', label: 'Documents', value: 14 }, { key: 'other', label: 'Other', value: 10 }]} />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Do use `Progress` for a task moving toward done, and `Meter` for a reading.
|
|
26
|
+
- Do name the unit in `format` ("62 GB of 100 GB"), not only a percentage.
|
|
27
|
+
- Do use segments for parts of a whole instead of a pie or donut.
|
|
28
|
+
- Don't give it more than six segments; fold the rest into "Other".
|
|
29
|
+
- Don't colour a reading caution or critical yourself; set `high` and `critical`, so the words come with the colour.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# NotificationCenter and NotificationList
|
|
2
|
+
|
|
3
|
+
An inbox of what happened: mentions, assignments, approvals waiting, failed jobs, billing notices. A bell with a count opens the list, grouped by day, with Mark all as read, actions on each item, and a link to preferences.
|
|
4
|
+
|
|
5
|
+
**Elevation.**
|
|
6
|
+
- **The bell** is live: a ghost button. **Only its count floats** (`Badge variant="count"`), because a count wants you.
|
|
7
|
+
- **The panel** floats at level 3 (a `Popover`), or at level 4 as a bottom `Sheet` on narrow screens.
|
|
8
|
+
- **The list in it** lies on the ground: it's a record of what happened.
|
|
9
|
+
- **Unread is a mark, not a height**: an `accent` dot, weight 600, and "Unread" in the item's name. Keeping unread items raised or tinted would make height mean "unread", which breaks "tiers carry meaning" (see Elevation, "States that don't rise").
|
|
10
|
+
- **A new notification** arrives lifted and settles, as anything new does. An item that links lifts when pointed at. Mark all as read removes the dots with a cross-fade, and nothing travels.
|
|
11
|
+
|
|
12
|
+
**Toasts are momentary; this is the record.** Use `toast` for "Saved". Use the notification center for things that wait for someone.
|
|
13
|
+
|
|
14
|
+
**Accessibility.** The bell's name includes the unread count ("Notifications, 2 unread"). New notifications are announced politely and batched ("2 new notifications"), whether or not the panel is open. Mark all as read comes first in the panel. Items that link are links, and opening one marks it read and closes the panel. Item actions are buttons, and an unread item without a link gets "Mark as read". Under forced colours the dot keeps its colour (`forced-color-adjust: none`), and the word carries it too.
|
|
15
|
+
|
|
16
|
+
**Props.** `NotificationCenter`: `items` (`{ id, title, body?, createdAt, read, href?, actions? }[]`), `onMarkRead(ids)`, `onMarkAllRead`, `preferencesHref`, `groupBy` (`'day' | 'none'`), `onOpen`, `mobileBreakpoint` (default 640). `NotificationList` is the same list without the bell, for a page of its own.
|
|
17
|
+
|
|
18
|
+
**You provide:** the notifications and whether each is read, and what reading one does.
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
<NotificationCenter items={notifications} onMarkRead={(ids) => markRead(ids)} onMarkAllRead={markAllRead}
|
|
22
|
+
preferencesHref="/settings/notifications" />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Do let each notification lead to its subject (`href`).
|
|
26
|
+
- Do keep an action on the item when it's waiting for someone ("Approve").
|
|
27
|
+
- Don't raise or tint unread items.
|
|
28
|
+
- Don't put momentary confirmations here; that's a toast.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# NumberInput
|
|
2
|
+
|
|
3
|
+
A number field: seats on a plan, rate limits, retention days, prices, quantities. `parseNumber()` reads text the same way.
|
|
4
|
+
|
|
5
|
+
**Elevation.** It's an opening, like `Input`: a well that shows its back wall while it's empty and whose floor rises flush once it holds a value. Invalid drops it to a trench on `critical-soft`. **The steppers are small raised slabs inside the well's end cap**, because you press them (push up, fill down). They're level 1 with a 1px wall, lift to 2 when pointed at, press flat, and lie flush in `ink-faint` at a limit. The unit ("days", "€", "%") sits beside the number in `ink-muted`. Editing isn't a change to the record, so nothing glows while you type or step.
|
|
6
|
+
|
|
7
|
+
**Parsing.** What you type is read in the locale's own way: in `de-DE`, "1.250,50" is 1250.5 and "1,5" is 1.5. Arabic-Indic, Persian, Devanagari, Bengali and full-width digits are read too. The value is committed on blur, on Enter and on every step, and it's clamped to `min` and `max` when committed, so it can't leave its range. Nonsense ("abc") keeps the last good value. Steps land on the grid that starts at `min` (or 0), without binary-fraction drift (0.1 + 0.2 is 0.3).
|
|
8
|
+
|
|
9
|
+
**Formats.** `format` takes `Intl.NumberFormat` options. `{ style: 'unit', unit: 'day' }` puts "days" beside the number. `{ style: 'currency', currency: 'EUR' }` puts the currency where the locale puts it. With `{ style: 'percent' }` the value is a fraction (0.1 is shown as "10 %").
|
|
10
|
+
|
|
11
|
+
**Accessibility.** The [spinbutton pattern](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/): `role="spinbutton"` with `aria-valuenow`, `aria-valuemin`, `aria-valuemax` and an `aria-valuetext` that includes the unit ("30 days"). Up and Down step, Shift+Up/Down and PageUp/PageDown move ten steps, and Home and End go to `min` and `max`. The steppers aren't tab stops (`tabIndex={-1}`, as the APG says), so name them for what they change (`incrementLabel="Increase seats"`). The scroll wheel never changes the value unless you set `wheel`, and then only while the field is focused. Under forced colours the steppers have a 1px border. Under `contrast="more"` their wall is `ink`.
|
|
12
|
+
|
|
13
|
+
**Props:** `value`, `defaultValue` (`number | null`), `onValueChange`; `min`, `max`, `step` (default 1); `format`; `locale`; `invalid`; `size`; `wheel`; `incrementLabel`, `decrementLabel`; and the input's own attributes.
|
|
14
|
+
|
|
15
|
+
**Target size.** The two steppers sit side by side, down then up, each 24×24 (WCAG 2.5.8): stacked, two 15px steppers could never each reach 24px. Under a coarse pointer each reaches 44px tall through a transparent hit area. The arrow keys still step, and Page Up and Page Down step by ten.
|
|
16
|
+
|
|
17
|
+
**You provide:** the range, the step, the unit (in `format`) and a label (with `Field`).
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<Field label="Seats">
|
|
21
|
+
<NumberInput value={seats} onValueChange={setSeats} min={1} max={500} incrementLabel="Increase seats" decrementLabel="Decrease seats" />
|
|
22
|
+
</Field>
|
|
23
|
+
<NumberInput value={days} onValueChange={setDays} format={{ style: 'unit', unit: 'day', unitDisplay: 'long' }} />
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- Do give it `min` and `max` whenever the value has limits, and say them in the hint.
|
|
27
|
+
- Do put the unit in `format`, not in the label, so it's spoken with the value.
|
|
28
|
+
- Don't use it for identifiers that happen to be digits (a ZIP code, a card number); that's an `Input`.
|
|
29
|
+
- Don't use it to choose roughly within a range; that's a `Slider`.
|