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,30 @@
|
|
|
1
|
+
# Artifact
|
|
2
|
+
|
|
3
|
+
What an agent is making (a document, a component, a page, a chart, a config) in its own pane beside the conversation, with its versions. Claude's artifacts, ChatGPT's canvas and v0's preview work this way: the latest version as it's written, preview and source, earlier versions to go back to, what changed.
|
|
4
|
+
|
|
5
|
+
**Elevation: the current version is the record.**
|
|
6
|
+
- **The pane** lies on the ground beside the conversation (put it in a `ResizablePanel`), named by its title.
|
|
7
|
+
- **Being written** isn't the record yet: a dashed `line-control` rule at the pane's edge and "Writing… version 3" (`aria-busy`).
|
|
8
|
+
- **Complete**: the rule goes solid with one afterglow, and "Version 3" (announced once: "Pricing page, version 3, complete").
|
|
9
|
+
- **Failed**: "Couldn't finish version 3" with a critical mark and Retry; keep the last whole version on show. The edge stays solid and neutral: a coloured stripe is a tell of generated interfaces (Distinctiveness, CM3), and the words already say it.
|
|
10
|
+
- **Viewing an earlier version** is a mode, and a mode isn't a height: a flush strip across the top with a `line-control` border, "Viewing version 1 of 3", Return to latest and Restore this version.
|
|
11
|
+
- **Header tools** are ghost buttons named for the artifact. **Preview / Source / Changes** is segmented `Tabs`.
|
|
12
|
+
- **The reference** in a message is a plate that acts, lifting one level when pointed at.
|
|
13
|
+
|
|
14
|
+
**Accessibility.** A region named by its title. The reference is a button with `aria-controls` on the pane: it opens the pane and moves focus to the pane's heading, and Escape in the pane goes back to it. Give an iframe preview a `title`, and keep the source view as its alternative. Writing isn't announced again and again; completion is said once.
|
|
15
|
+
|
|
16
|
+
**Parts:** `Artifact` (`title`, `status`: `'writing' | 'complete' | 'failed'`, `version`, `onRetry`, `id`); `ArtifactHeader` (`onCopy`, `onDownload`, `onOpen`, `onClose`, extra tools as children); `ArtifactVersions` (`versions`, `current`, `onChange`, `onRestore`); `ArtifactContent` (`views`: `{ preview?, source?, changes? }`, `defaultView`); `ArtifactReference` (`title`, `version`, `status`, `controls`, `onOpen`).
|
|
17
|
+
|
|
18
|
+
**You provide:** the versions and their content (a sandboxed iframe for previews, `CodeBlock` for source, `DiffView` for changes), and where the pane sits. On a phone, a `ResizablePanelGroup` with `narrow="stack"` shows the conversation or the pane, one at a time; set `narrowPanel` to the pane when it opens.
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
<Artifact id="pricing" title="Pricing page" status={a.status} version={a.versions.length} onRetry={retry}>
|
|
22
|
+
<ArtifactHeader onCopy onDownload={download} onClose={close} />
|
|
23
|
+
<ArtifactVersions versions={a.versions.length} current={v} onChange={setV} onRestore={restore} />
|
|
24
|
+
<ArtifactContent views={{ preview: <iframe sandbox="allow-scripts" srcDoc={html} title="Pricing page preview" />, source: <CodeBlock language="tsx" code={code} /> }} />
|
|
25
|
+
</Artifact>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- Do sandbox preview iframes; the frame's permissions are yours.
|
|
29
|
+
- Do keep the last whole version visible when a new one fails.
|
|
30
|
+
- Don't lift the pane or tint it for an earlier version; say it in the strip.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Avatar
|
|
2
|
+
|
|
3
|
+
Represents a person. A face is a disc on the page, built from the same pieces as everything around it: while the person is here it rests on the page like a card, and while they're not it's part of the record, a plate set flush into the page. Only someone live leaves the ground.
|
|
4
|
+
|
|
5
|
+
| State | Height | Face | Lamp |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| `active` | resting: level 1, a contact shadow | `surface-raised` | filled `positive` |
|
|
8
|
+
| `idle` | resting: level 1, a contact shadow | `surface-raised` | an empty ring in `caution` |
|
|
9
|
+
| `offline` | flush in the page (ground), photo greyscale | a badge plate, `ink-muted` initials | none |
|
|
10
|
+
| `live` | lifted off the page, drifting over `dur-hover`; its shadow appears on the page beneath it | `surface-raised` at level 2 | as its status |
|
|
11
|
+
|
|
12
|
+
Faces are neutral. Status colours never stand for a person, so an initial on a pink disc can't read as an error; colour lives in the lamp, as it does on a `Badge`. Active and idle differ in the lamp's shape (filled or an empty ring), not only its colour, and not in height: they're one tier, because both people are here.
|
|
13
|
+
|
|
14
|
+
`live` (speaking, typing, presenting right now) lifts the face: the shadow beneath it appears on the page and gets smaller and fainter as the face rises, the cue people read as an object floating rather than resting. Under reduced motion it holds a little above the page instead of drifting. Height is animated through a registered `--hr` property, so face and shadow move together on `ease-settle`. `presence={1|2|3}` rings the face in that person's presence colour, a 2px ring drawn tight around it.
|
|
15
|
+
|
|
16
|
+
The lamp is cut out of the face by a 2px ring of ground. It shows at 24px and larger (`lamp={false}` hides it) and is never shown inside a group. With `onClick` the avatar becomes a button: hovering or focusing lifts it off the page, and pressing returns it to the ground, the same promise as everything else that lifts. Avatars without `onClick` never lift on hover.
|
|
17
|
+
|
|
18
|
+
**Teams, organisations, bots and agents** are `shape="square"`: a tile whose corners grow with its size (`radius-xs` up to 24px, `radius-sm` to 36, `radius-md` to 48, `radius-lg` above), with the same states, lamp and shadow as a person. Keep circles for people, so a glance tells you whether a person did something.
|
|
19
|
+
|
|
20
|
+
**Faces.** With `src` the photo fills the face; if it fails to load, the initials show instead. Initials are the first letter of the first and last words, skipping punctuation and emoji ("Mara de Quist" is MQ, "Jean-Luc Picard" JP, "李小龙" 李); below 28px, one letter, drawn larger. Names that no rule abbreviates well are better given a photo.
|
|
21
|
+
|
|
22
|
+
**Sizes.** 24px in dense rows and tables, 28–32px in lists and groups, 36px (the default) beside a name, 44px on cards, 64px in a profile header.
|
|
23
|
+
|
|
24
|
+
In an `AvatarGroup`, every face stands on one ground line, each cut out of the one before it by a ring of ground, overlapping by 12% so the initials beneath stay whole. Offline faces are flush plates in the row; a live face lifts in front of its neighbours. `max` caps the faces shown and the rest become a flush "+N" plate whose label names them ("2 more: Eli Ward, Kai Ito"); `size` sizes every face that doesn't set its own. The group is a `role="group"` labelled with how many people it holds ("7 people"); pass your own `aria-label` for something more specific ("Editing now").
|
|
25
|
+
|
|
26
|
+
**Loading.** While `name` is undefined (`user?.name` while your data loads), or a promise, the avatar is an imprint of its `size` and `shape`; it rises out of it when the name arrives (see `Await`).
|
|
27
|
+
|
|
28
|
+
**Forced colours and high contrast.** Faces get a `CanvasText` rim (`GrayText` when offline, 2px when live), and the active lamp is filled while the idle one stays a ring. Under `contrast="more"` a resting face sits on the same ink slab as a card.
|
|
29
|
+
|
|
30
|
+
**Right to left.** The lamp sits at the inline end and a group overlaps from the inline start, so both mirror with `dir="rtl"`.
|
|
31
|
+
|
|
32
|
+
**You provide:** `name` (always: it's the accessible label, with the status appended), optional `src`, `size` in pixels (default 36), `shape`, `status`, `live`, `presence`, `lamp`, `onClick`. On `AvatarGroup`: `max`, `size`.
|
|
33
|
+
|
|
34
|
+
- Leave about a fifth of the avatar's size free above a live or pressable avatar for it to rise into, and a quarter above a group.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Await
|
|
2
|
+
|
|
3
|
+
Components that bring their own loading state. Pass your data the way you already do: while it's `undefined` (`user?.name` while a query loads), the component shows its own imprint at its own size, and when the data arrives the content rises out of it. You don't write skeletons or loading flags. `Await` is the Suspense boundary inside each of them, exported so your components can do the same.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
const { data: user } = useQuery(userQuery(id)); // useSWR, a loader, your own state: anything that's undefined until it isn't
|
|
7
|
+
|
|
8
|
+
<Card>
|
|
9
|
+
<CardHeader>
|
|
10
|
+
<Avatar name={user?.name} size={44} />
|
|
11
|
+
<CardTitle>{user?.name}</CardTitle>
|
|
12
|
+
<CardDescription>{user?.role}</CardDescription>
|
|
13
|
+
</CardHeader>
|
|
14
|
+
<CardContent>{user?.bio}</CardContent>
|
|
15
|
+
</Card>
|
|
16
|
+
<DataTable columns={columns} rows={deploys} getRowId={(d) => d.id} />
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Elevation: loading is an imprint.** Content still to come is the shape of what's coming, pressed into the page (`surface-well`, `well`), as `Skeleton` draws it. The difference is who draws it. A component holds its own `<Suspense>` boundary, and its fallback is drawn from the same props and tokens as the component itself: an `Avatar` of `size={44}` leaves a 44px disc; a `CardTitle` leaves one line as tall as the title's line-height; a `Badge` leaves a plate; a `DataTable` keeps its real header over rows of cell imprints. So the imprint is the size of the content, and nothing moves when it lands.
|
|
20
|
+
|
|
21
|
+
**What counts as "not here yet".** `undefined` is on its way; `null` is nothing, and renders empty at once. Required content waits whenever it's undefined. Optional content (a card's description, text passed straight into `CardContent`) waits only while its card's required parts do, so a field that's simply missing from your data never leaves an imprint behind.
|
|
22
|
+
|
|
23
|
+
| Content | Waits while | Imprint |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `Avatar` `name` | undefined | a disc of its `size` |
|
|
26
|
+
| `CardTitle` children | undefined | one line of the title's type; `skeleton={{ width }}` |
|
|
27
|
+
| `Badge` children | undefined | a plate (`width`, default 64px; a `count` is round) |
|
|
28
|
+
| `DataTable` `rows` | undefined | the real header over `skeleton.lines` rows of imprinted cells (default 5) |
|
|
29
|
+
| `CardDescription` children | undefined and the card's required parts are waiting | two lines of the description's type; `skeleton={{ lines, width }}` |
|
|
30
|
+
| `CardContent` children | undefined and the card's required parts are waiting, or anything of yours inside suspends | `lines` lines (default 3) |
|
|
31
|
+
| `Card` | anything of yours inside suspends | a title line over `lines` lines (default 3) |
|
|
32
|
+
|
|
33
|
+
Every one of them also takes a promise in place of the value (from a server component, say) and waits for it the same way.
|
|
34
|
+
|
|
35
|
+
**If a request fails** your data stays undefined, so the imprint stays. Render your error state in place of the content, as you would anyway, or pass `null`. `skeleton={false}` on any component renders exactly what you pass, with no imprint.
|
|
36
|
+
|
|
37
|
+
**Motion.** An imprint waits `dur-grace` (300ms) before it fades in, so loads quicker than that show nothing and land without moving: Skeleton's rule, "don't show imprints for loads under about 300ms", is now kept for you. When content lands after its imprint showed, it rises 8px out of it (`dur-travel-3`, `ease-rise`) while the imprint fills in behind it. Rises share the attention budget (`riseDelay`): if many land at once, no more than four begin together. Containers (`Card`, `CardContent`, `DataTable`) leave no imprint behind; their content just rises. Under reduced motion it fades in place.
|
|
38
|
+
|
|
39
|
+
**Landing together.** A page full of boundaries fills in piece by piece as each one's data arrives. Wrap related parts in `RevealGroup` and each part that lands early stays an imprint until the slowest one in the group has landed; then they rise together (still four at a time). Group things that load from the same place; a group waits for its slowest member.
|
|
40
|
+
|
|
41
|
+
**Without height.** Every imprint carries a visually hidden "Loading" and `aria-busy`, and is otherwise hidden from assistive technology; the content that replaces it is announced as normal content. Under forced colours imprints have a dashed `CanvasText` outline, like every sunk surface.
|
|
42
|
+
|
|
43
|
+
**How it works.** An undefined value becomes a suspension: the component's outer part (which stays mounted and re-renders when your data arrives) holds a promise that resolves once the value turns up (`useArrival`), and its inner part reads it, so the component's own boundary shows the imprint, and the reveal is the same one a real promise gets. Reading is React 19's `use()` when it's there; on React 18 the same contract is kept by hand (the promise is thrown to the boundary and its result remembered), so both work. A component that has never waited takes the old path exactly: no boundary, no extra element. Once it has, the wrapper stays, as `display: contents`, so it adds nothing to layout.
|
|
44
|
+
|
|
45
|
+
**Limits.** Your own markup can't tell: `<p>{user?.bio}</p>` inside a card renders an empty paragraph while it loads; pass the text straight to `CardContent` or `CardDescription` instead. An optional part whose data comes from a different, slower request than its card's required parts is treated as missing once they land, and appears when its data does, without an imprint.
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
// Promises work too: from a server component, start the request and pass it along.
|
|
49
|
+
const user = getUser(id);
|
|
50
|
+
<Avatar name={user.then((u) => u.name)} size={44} />
|
|
51
|
+
|
|
52
|
+
// Your own component, the same way:
|
|
53
|
+
<Await inline shape="circle" imprint={<Skeleton shape="circle" width={24} />}><Flag code={country} /></Await>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Breath (experimental).** `<AltitudeProvider loading="breath">` changes how imprints wait and how content lands; nothing changes without it. Instead of light sweeping over them, imprints breathe in depth: the floor deepens to trench depth and releases (the back wall in Nightfall), once every `dur-breath` (4.8s), on one page clock, with a slow swell down the view. Ten seconds in, the breath calms. When content lands:
|
|
57
|
+
|
|
58
|
+
| Content | `settle` | Lands |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| On its own surface (a card from one request) | `"raised"` | rises 8px to level 2 and settles to level 1 over `dur-arrive` |
|
|
61
|
+
| On the ground (text, a badge, a row) | `"flush"` (default) | its floor comes up just past the ground and fills in flush |
|
|
62
|
+
| Into a view already showing (Load more, a boundary that suspends again) | `late` | an untinted plate under it, gone in `dur-plate` |
|
|
63
|
+
|
|
64
|
+
A `Card`'s parts land together, and a `RevealGroup` takes one place in the attention budget (its parts 16ms apart). A card's optional parts still don't wait: if its description is `undefined` once its title has landed, it renders empty, so pass a promise for a detail that's still coming. Chart, stat and meter imprints hold still, and a chart's marks still fade in place. Under reduced motion the breath stops and nothing travels.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<AltitudeProvider scheme="daylight" loading="breath">
|
|
68
|
+
<Await settle="raised" imprint={<Skeleton height={103} radius="var(--radius-lg)" />}><DeployCard deploy={deploy} /></Await>
|
|
69
|
+
{older.map((e) => <Await key={e.id} late imprint={<Skeleton height={26} />}><Event event={e.promise} /></Await>)}
|
|
70
|
+
</AltitudeProvider>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**You provide:** your data as it is, `undefined` until it arrives. Hints where the default shape won't match (`skeleton={{ lines: 3 }}`). If you pass promises, make them stable across renders: started in a server component, a loader, a cache or state, never created inline in a client component's render. `skeleton={false}` on any of them to opt out.
|
|
74
|
+
|
|
75
|
+
- Do pass `user?.name` and friends straight in, and let each part land when its data does.
|
|
76
|
+
- Do wrap parts that read better arriving together (a profile header) in `RevealGroup`.
|
|
77
|
+
- Don't wrap Suspense-native components in your own `Skeleton`s; they already imprint themselves.
|
|
78
|
+
- Don't use it for errors: a rejected promise throws to your nearest error boundary, where an `Alert` belongs.
|
|
79
|
+
- Don't use `null` for "still loading"; it means there's nothing, and renders empty.
|
|
80
|
+
- `Skeleton` with `loaded` is still the way for layouts of your own.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Badge
|
|
2
|
+
|
|
3
|
+
Labels status or metadata. A badge is an **indicator plate**: part of the record, so it's set flush into the page (a shallow stamp, darker at its top edge with a light lip below) and never lifts.
|
|
4
|
+
|
|
5
|
+
- **Lamp.** Status carries an indicator lamp: a flat 7px dot in its tone's own colour (`positive`, `caution`, `critical` or `accent`, the same colour the tone uses for text), with no gloss, socket or glow. It's printed on the plate rather than set into it, so it never competes with the plate's height. It's on by default for `positive`, `caution` and `critical`; `lamp` on a neutral badge shows an unlit lamp, an empty `ink-muted` ring ("Draft"); `lamp={false}` hides it. With a lamp, the label stays in `ink` and the colour lives in the lamp. Without one, a toned plate says its tone in its lettering.
|
|
6
|
+
- **Relight and re-stamp.** When `tone` changes the lamp relights: it starts as an empty ring and fills in over `dur-travel-3`. When the label changes, the plate is re-stamped (it comes down 3px onto the page and settles). Neither happens on first render.
|
|
7
|
+
- **Pulse.** `lamp="pulse"` fades the dot to a third and back over `dur-hover`, for something in progress ("Deploying"). It holds still under reduced motion.
|
|
8
|
+
|
|
9
|
+
| `variant` | Looks like | Use |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `plate` (default) | a neutral plate stamped into the ground | Most status and metadata. |
|
|
12
|
+
| `tinted` | the plate takes its tone's `-soft` colour, lettering in the tone | When status needs to be seen from across the page. (`soft` is the old name.) |
|
|
13
|
+
| `outline` | a `line-control` hairline only | Versions, IDs, counts of things that don't need attention. |
|
|
14
|
+
| `count` | a number on a floating pill (`elev-2`) | Unread or waiting items: the one badge that rises, because it wants you. It pops up when the number changes. Solid `accent` or `critical`; neutral is `surface-floating`. Position it yourself (for example absolutely, over a Button's corner). |
|
|
15
|
+
|
|
16
|
+
**Loading.** While the label is undefined (your data hasn't arrived) or a promise, the badge is a plate-shaped imprint (see `Await`).
|
|
17
|
+
|
|
18
|
+
**You provide:** a short label (one or two words, sentence case) or a number for `count`, `tone`, optionally `variant` and `lamp`.
|
|
19
|
+
|
|
20
|
+
- Do pair status colour with the word: "Outage", never a lamp alone.
|
|
21
|
+
- Don't make badges clickable; if it filters or navigates, it's a ghost Button.
|
|
22
|
+
- Don't use `count` for totals that aren't asking for attention; use a plate or plain text.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# BarChart
|
|
2
|
+
|
|
3
|
+
Amounts compared across categories or periods: deploys per team, signups per month, cost per region. One series, several side by side (grouped), or stacked into a total. Length on a common baseline is the most accurately read encoding there is (Cleveland & McGill, 1984), so this is the workhorse chart. It renders inside a [`ChartContainer`](../ChartContainer/README.md).
|
|
4
|
+
|
|
5
|
+
**Why it's flat.** Bars are the record drawn as lengths, so they stand on the ground line: flat `data-*` fills, square where they meet the ground and rounded (`radius-xs`) only at the data end. Neighbours and stacked segments are parted by a real 2px gap, never a shadow or an outline. A bar never lifts, on hover or anywhere else: a lifted bar leaves the ground line or casts a shadow below its top, and both misstate its value. A negative bar hangs below the ground line. That's position, not depth: it has no inner wall and doesn't read as an opening. One series wears `data-1` throughout; bars are never coloured by their value.
|
|
6
|
+
|
|
7
|
+
**Without colour.** Under forced colours and in print, and with `patterns` on the container, each series gets its hatch (the first stays solid) and filled bars a `CanvasText` edge. With `contrast="more"` bars get a 1px `ink` edge and 3px gaps. Value labels are `ink` text, never a series colour.
|
|
8
|
+
|
|
9
|
+
**Motion.** Values glide at one level, re-sorts are carried, a rescale comes first, and nothing grows from the ground line on first render; a bar grows only when its value really goes from zero to something ([Motion](../../visual/Motion.md), "Charts").
|
|
10
|
+
|
|
11
|
+
**Accessibility.** The container names the chart and summarises it; without a written `summary`, the generated one names the series and each one's lowest and highest category. The keyboard path through the bars and the readout are #63 and #52, and the table view is #64.
|
|
12
|
+
|
|
13
|
+
**Props:**
|
|
14
|
+
|
|
15
|
+
- `data` and `x`: the records and the key of each one's category;
|
|
16
|
+
- `sort`: `'desc'` or `'asc'` by total;
|
|
17
|
+
- `stack`: `true` for parts of a total, `'expand'` for shares of 100%;
|
|
18
|
+
- `layout="horizontal"`: categories down the side, for long names or many of them. Names longer than 40% of the width are cut with an ellipsis; the full name stays in the summary;
|
|
19
|
+
- `labels`: `'max'` labels the largest bar (or stack), `'last'` the last category's. Never every bar;
|
|
20
|
+
- `status`: which categories aren't yet the record (`'partial'` or `'projected'`), and `boundary`, the rule's word. See below;
|
|
21
|
+
- `window` and `onWindowChange`: for bars over time (a bar per week), a stretch of the categories by date, `{ start, end }` or the last `{ points }`. A `ChartBrush` inside the chart picks it on an overview.
|
|
22
|
+
|
|
23
|
+
**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".
|
|
24
|
+
|
|
25
|
+
**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".
|
|
26
|
+
|
|
27
|
+
**Not yet the record.** `status={(d) => d.month === thisMonth ? 'partial' : undefined}` keeps this month so far from reading as a drop. A partial bar is its series colour hatched at 45° on the plot's own colour, with no solid fill; a projected one also keeps a 1.5px outline in its colour. Neither is lifted: a pending write hovers, but a bar that hovers misstates its value, so it says "not yet" with pattern and a word instead. A 1px `line-control` rule marks where the record ends, named "Today" (`boundary="…"` renames it, `boundary={false}` drops it). The bar's value label, readout, keyboard name and table row say "So far" or "Projected". When the period completes and its status goes, the hatching cross-fades to solid over `dur-travel-3`: it's been pressed into the record. Under forced colours the pending bar takes a dotted `CanvasText` outline, the same cue as a pending write.
|
|
28
|
+
|
|
29
|
+
The y axis always includes zero, whatever the container's `zero` says: a truncated bar axis isn't offered.
|
|
30
|
+
|
|
31
|
+
**Focus.** `focus={['web']}` keeps those series' colours and draws the rest in `data-context`; on a one-series chart, `focus={(d) => d.team === 'Cedar' || d.team === 'Delta'}` picks out records instead, the categories the takeaway names. See [ChartContainer](../ChartContainer/README.md), "Focus".
|
|
32
|
+
|
|
33
|
+
**Against the record before.** `compare={{ key: 'lastQuarter', label: 'Q2' }}` (a field on each record) or `compare={{ data: lastWeek, label: 'Last week' }}` (records matched by position, before any sort) draws the earlier value as a 2px `ink` tick across each bar, 4px past it on each side: Few's comparative measure. A value that fell leaves its tick past the bar's end, the drop visible as empty space. Grouped bars compare one `series` (default the first); a stack compares its total; shares (`stack="expand"`) don't compare. The readout, point names ("Up 11% from Q2"), the table ("Q2" and "vs Q2" columns) and the summary ("Against Q2: 3 rose, 1 fell.") say the difference, and earlier values join the axis.
|
|
34
|
+
|
|
35
|
+
**You provide:** a `ChartContainer` with a `config` naming each series, the `data` and `x`, and usually a `summary`.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
<ChartContainer config={{ deploys: { label: 'Deploys' } }} summary="Cedar deployed most, 51 times.">
|
|
39
|
+
<BarChart data={byTeam} x="team" sort="desc" labels="max" />
|
|
40
|
+
</ChartContainer>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- Do sort categories that have no order of their own, so the comparison reads at a glance.
|
|
44
|
+
- Do keep grouped bars to four series or fewer; past that, stack them or use small multiples.
|
|
45
|
+
- Do use `stack="expand"` only when the shares matter more than the totals, and say so in the title.
|
|
46
|
+
- Don't label every bar: label the one the chart is about, and leave the rest to the readout and the table.
|
|
47
|
+
- Don't colour bars by their value or give them a gradient; for polarity, a diverging ramp on a separate chart says it better.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# BarList
|
|
2
|
+
|
|
3
|
+
Top pages, top referrers, top customers by spend: a ranked list where each row is its own bar, the label is readable at any length, and the exact value sits at the end. It sits between a `Table` (exact, no shape) and a horizontal `BarChart` (shape, but long labels squeeze the plot).
|
|
4
|
+
|
|
5
|
+
**Why it's flat.** Rows are the record, so they lie flush on the ground with `line` dividers, like a table's rows. Each row's bar is a flat tint of `data-1` behind its label, as long as the row's value against the largest (the top row's bar fills its row), and faint enough that `ink` holds 4.5:1 over it in every scheme. It has a `radius-xs` corner and no shadow, wall or gradient. A long label wraps; it's never cut. The value sits at the row's end in tabular figures, and with `share` its percentage of the total sits beyond the bar in `ink-muted`.
|
|
6
|
+
|
|
7
|
+
**Hover is a promise.** A row that does something (`href` makes it a link, `onSelect` a button) lifts one level when pointed at, exactly as `Table`'s interactive rows do, and drops back when pressed; its bar stays flat on it. A row that does nothing never moves.
|
|
8
|
+
|
|
9
|
+
**Other and Show all.** With `limit`, the rows past it fold into one "Other (14 pages)" row in `data-other`, and a ghost "Show all 21 pages" button lists them all (and "Show fewer" folds them back). `noun` names what the rows are.
|
|
10
|
+
|
|
11
|
+
**Filter in place.** `relevance` raises matches one level and sinks misses into a well, as `TableRow relevance` does; nothing is removed, and misses say "not matching" to screen readers. Under the list it says how many match: "2 of 5 pages match", using `noun`.
|
|
12
|
+
|
|
13
|
+
**Motion.** When values change, rows are carried to their new ranks (FLIP, `dur-travel-3`, `ease-glide`), all at once, because a re-rank is one change; bars glide to their new lengths. Under reduced motion both change in place. While `data` is undefined the list is imprinted rows, which land like `Await` content.
|
|
14
|
+
|
|
15
|
+
**Accessibility.** Plain HTML: an `ol` (rank is meaningful) of links, buttons or plain items, each reading "1. /pricing, 4,210, 23%". The bar is hidden from assistive technology; the value is text. Under forced colours bars are a light `Highlight` tint with a 1px `Highlight` border, matches are outlined and misses dashed. Under `contrast="more"` bars get a 1px `ink` edge.
|
|
16
|
+
|
|
17
|
+
**Props:** `data` (`{ name, value, key?, href? }[]`); `format`; `limit`; `share`; `sort` (`'desc'`, the default, or `false` to keep your order); `noun`; `onSelect`; `relevance`; `locale`; and any `ol` attribute (name it with `aria-labelledby` or `aria-label`).
|
|
18
|
+
|
|
19
|
+
**You provide:** the rows, a name for the list, and a `format` with the unit when it isn't a plain count.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<BarList aria-labelledby="pages-title" data={pages} format={(n) => n.toLocaleString()} limit={8} share noun="pages" />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Do use it for a ranked top-N with long labels; use a `BarChart` to compare a few categories across series.
|
|
26
|
+
- Do give rows an `href` when they open a report, so they're real links.
|
|
27
|
+
- Do fold a long tail into "Other" with `limit` rather than scrolling a hundred rows.
|
|
28
|
+
- Don't colour bars by status or give each row its own colour: colour is identity, and these rows are one series.
|
|
29
|
+
- Don't set the bars' lengths to share of total yourself; `share` says it in words, and the bars compare rows with each other.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Board
|
|
2
|
+
|
|
3
|
+
Columns of cards you move between, a kanban: columns are receptacles that open when you carry a card, and the card floats in your hand until it lands.
|
|
4
|
+
|
|
5
|
+
**Elevation: receptacles and a hand.** At rest a board is just its cards: the columns lie flush on the ground, with only their titles and counts to mark them, and the cards are raised (level 1), lifting to level 2 under the pointer. The moment a card is carried, every column opens into a well (things you put something into are openings), and the column under the card deepens to a trench with an accent rim, as a DropZone does when it's ready to receive. The carried card floats at level 3, tilted 1.2° and 1.5% larger, and follows the pointer; the cards around it glide aside (`ease-settle`), and the slot it will land in opens as a trench. Let go and it's carried into the slot with a slight overshoot (`ease-settle`) while its shadow sinks back to `elev-1`, and the columns close up. A move to the column it came from, or nowhere, travels back the same way.
|
|
6
|
+
|
|
7
|
+
- **A collapsed column is a Pile.** Its thickness (one, two or three sheets) and its count badge say how many cards it holds. It still takes drops: carried over it, it deepens and fans its sheets toward the card, and the card is added to the end.
|
|
8
|
+
- **Someone else's hand.** A card a collaborator is holding (`heldBy`) is lifted to level 3, outlined in their presence colour and wears their name; you can't pick it up meanwhile.
|
|
9
|
+
- **Afterglow.** `settle` makes a card arrive tinted and lifted, then settle: for cards moved by someone else.
|
|
10
|
+
|
|
11
|
+
**Editing in place.** On a board the column you add to is the context, so cards are added and edited where they are, not in a Dialog.
|
|
12
|
+
|
|
13
|
+
- **Add.** With `onCreate(title)` on a `BoardColumn`, the column ends in an add slot: a dashed well (an opening) named “Add a card to To do”, deepening to a trench under the pointer. Activating it raises it into a new card (static, level 1) holding a “Card title” field. Enter creates the card and leaves a fresh, empty field, so several can be added in a row; Escape, or Enter on an empty field, closes it and returns focus to the slot. A pending card hovers after `dur-grace`; a refused one drops to a trench with its title kept and Retry. Resolve `{ ok: true, value: { id } }` after the card is among the children, or `{ ok: false, error }`. A collapsed column shows no add slot. Announced: “Added Fix login to To do, position 4 of 4.”
|
|
14
|
+
- **Rename.** With `onRename(draft)` (InlineEdit's contract) and a `label`, F2 or the card's visible Rename button turns the title into a field, selected. Enter saves, Escape cancels, a refusal keeps the draft with its error, and focus returns to the card. While the field is open, Space and Enter type into it and don't pick the card up; Space and Enter on the card itself still pick it up, so renaming has F2 and a button rather than Enter.
|
|
15
|
+
- **Edit fields.** With `value`, `renderEditor(field)` and `onSave` (EditRegion's contract), the card's Edit button grows it into an `EditRegion` in place, and the cards below are pushed down. `onValueChange` publishes the saved record. A card being edited can't be picked up.
|
|
16
|
+
|
|
17
|
+
**Keyboard and screen readers.** Each column is a `section` named by its title (an `h3`), with its card count. Cards are focusable buttons with `aria-roledescription="draggable card"` and instructions attached. Space or Enter picks a card up (`aria-pressed`); ↑ and ↓ move it within its column, ← and → carry it to the next open column; Space drops it; Escape puts it back where it started. Every step is announced in an assertive live region ("Review, position 2 of 3"). A card held by a collaborator says so instead of lifting. Moving focus away puts a held card down where it is. Under reduced motion cards don't travel or tilt; their levels still change. Forced colours dash the open columns and outline the target in Highlight.
|
|
18
|
+
|
|
19
|
+
| Part | |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `Board` | `onMove({ cardId, from, to })`, where `from` and `to` are `{ column, index }`; you update your data and the board carries the cards |
|
|
22
|
+
| `BoardColumn` | `id`, `title`, `collapsible`, `collapsed`/`defaultCollapsed`/`onCollapsedChange`, `onCreate`, `addLabel` |
|
|
23
|
+
| `BoardCard` | `id`, `label` (for announcements), `heldBy`, `settle`, `onRename`, `value`/`renderEditor`/`onSave`/`onValueChange`; anything inside it |
|
|
24
|
+
|
|
25
|
+
**On touch.** A swipe scrolls the board; a press-and-hold picks a card up, then a drag moves it. The card tools are always shown where there's no hover.
|
|
26
|
+
|
|
27
|
+
**You provide:** the data and an `onMove` that moves the card in it (splice it out of `from.column` at `from.index`, into `to.column` at `to.index`); card content (a title, then a `small` line of detail); short column titles.
|
|
28
|
+
|
|
29
|
+
- Do keep buttons and links inside cards: pressing them doesn't pick the card up.
|
|
30
|
+
- Do collapse finished columns rather than hiding them; the pile keeps their weight visible.
|
|
31
|
+
- Don't use a board for a single list you reorder; that's SortableList.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Breadcrumb
|
|
2
|
+
|
|
3
|
+
Where you are in an app with routes: Settings › Members › Jane Doe, or Projects › Atlas › Issues.
|
|
4
|
+
|
|
5
|
+
**Elevation: a staircase.** It's `Drilldown`'s breadcrumb, now available on its own, and the two share one implementation. Every step stands on the same ground line, outlined face and wall together, and **each deeper place is a taller step** (its wall 3px longer per level). Steps you can go back to are live: they lift one level when pointed at and press to the ground. The current page is the last and tallest step, in weight 600, and it doesn't press. A route-based app has no stack of views, so there's no pile under it, and **a route change re-renders it without motion**: a new route isn't an arrival.
|
|
6
|
+
|
|
7
|
+
**Deep paths.** With `maxItems`, a longer path keeps its first step and its last `maxItems − 1`. The middle waits behind a "…" step that opens a `DropdownMenu`, which floats at level 3.
|
|
8
|
+
|
|
9
|
+
**No separators.** Each step's height already says it's deeper than the one before, so there are no slashes or chevrons. `BreadcrumbSeparator` exists for code written for shadcn/ui and renders nothing.
|
|
10
|
+
|
|
11
|
+
**Accessibility.** The [breadcrumb pattern](https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/): a `nav` labelled "Breadcrumb", an ordered list, and `aria-current="page"` on the last step. The "…" step is a menu button named "Show 3 more levels". Under forced colours steps keep a 1px outline and the current step a 2px one. Under `contrast="more"` each step's wall is `ink`.
|
|
12
|
+
|
|
13
|
+
**Props.**
|
|
14
|
+
- `items` (`{ label, href?, onClick? }[]`; the last entry is the current page).
|
|
15
|
+
- `maxItems`.
|
|
16
|
+
- `renderLink(item, props)` for a router's link: spread `props` onto it.
|
|
17
|
+
- `onNavigate(item)` for steps chosen from the "…" menu (default: follow `href`).
|
|
18
|
+
- **Composable parts:** `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbPage` and `BreadcrumbEllipsis` mirror shadcn/ui's for full control.
|
|
19
|
+
|
|
20
|
+
**Fitting the width.** `fit="collapse"` (the default) keeps the trail on one line: when it doesn't fit, steps from the second onward move behind "…" until it does, keeping the first step and the current page, and they come back when there's room. `fit="wrap"` lets it wrap instead. `maxItems` still caps it by count.
|
|
21
|
+
|
|
22
|
+
**You provide:** the path, from the top of the app to here.
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
<Breadcrumb
|
|
26
|
+
items={[{ label: 'Settings', href: '/settings' }, { label: 'Members', href: '/settings/members' }, { label: 'Jane Doe' }]}
|
|
27
|
+
maxItems={4}
|
|
28
|
+
renderLink={(item, props) => <NextLink href={item.href!} {...props}>{item.label}</NextLink>}
|
|
29
|
+
/>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- Do end with the current page, as plain text.
|
|
33
|
+
- Do use it for location in a hierarchy, not for history ("back").
|
|
34
|
+
- Don't add separators between steps.
|
|
35
|
+
- Don't use it inside a `Drilldown`, which has its own.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Button
|
|
2
|
+
|
|
3
|
+
Triggers an action. A button is a slab standing on the page: its height is drawn as a visible wall under the face, and that height is its importance.
|
|
4
|
+
|
|
5
|
+
| Variant | Height | Use |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `primary` | level 2, `wall-2` (4px), `accent` | The one thing this view is for. At most one per view. |
|
|
8
|
+
| `secondary` | level 1, `wall-1` (2px), `surface-raised` | Everything else. The default. |
|
|
9
|
+
| `ghost` | level 0, flush, no wall | Low-emphasis actions in toolbars, a dialog's cancel, table rows. On hover it rises out of the page with a 2px wall. |
|
|
10
|
+
| `danger` | level 2, `wall-2`, `critical` | Irreversible actions, usually confirmed in a Dialog or a HoldButton. |
|
|
11
|
+
|
|
12
|
+
The wall is the face colour darkened, so it reads in every scheme and with any custom accent. A button stays exactly where it is until the pointer is on it: it doesn't react to an approaching pointer or follow it across its face, because a control that moves on its own reads as something going wrong. Hover raises the face 1px while the wall grows to keep its foot on the ground. Pressing pushes the face all the way down: the wall disappears, the sheen flattens, and on release it settles back up on `ease-settle`. `loading` holds it down with a spinner and ignores clicks. `disabled` has no height at all: flush, on `surface-well`, in `ink-faint`. In high-contrast mode the wall becomes solid ink. The keyboard focus ring wraps the whole slab, face and wall, 4px out, in `accent-ink`.
|
|
13
|
+
|
|
14
|
+
**You provide:** a label (verb first, sentence case: "Publish changes"), `variant`, `size` (`sm` 30px, `md` 36px, `lg` 44px), optional `leading`/`trailing` icons at 16px. Icon-only buttons take `square` and an `aria-label`.
|
|
15
|
+
|
|
16
|
+
- Don't put two primary buttons in one view. Promote one, demote the other to secondary.
|
|
17
|
+
- Leave room below a row of buttons: the wall extends up to 5px under the face.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Card
|
|
2
|
+
|
|
3
|
+
A raised surface that groups related content; it moves only if the whole card is one action.
|
|
4
|
+
|
|
5
|
+
A static Card sits at level 1 and never reacts to the pointer. An `interactive` Card stays put until the pointer is on it, lifts to level 2 and 2px on hover or focus and presses to ground on click; with `onClick` it becomes keyboard-reachable (`role="button"`, Enter and Space).
|
|
6
|
+
|
|
7
|
+
**Loading.** Pass your data as it is. A `CardTitle` whose text is undefined imprints one line of its own type; a `CardDescription`, or text passed straight to `CardContent`, imprints too, but only while the card's required parts (title, avatar, badge) are still waiting, so a missing field never leaves an imprint behind. A Card and its `CardContent` also hold a Suspense boundary for anything of yours that suspends inside them (`skeleton={{ lines }}`, default 3; `false` passes it to your own boundary). See `Await`.
|
|
8
|
+
|
|
9
|
+
**You provide:** content through `CardHeader` (with `CardTitle` and `CardDescription`), `CardContent` and `CardFooter` (right-aligned actions), or your own children. Padding is `space-5`.
|
|
10
|
+
|
|
11
|
+
Cards take the same depth signals as `Surface`: `settle`, `status`, `heldBy` and `relevance`.
|
|
12
|
+
|
|
13
|
+
- Do make the entire card the target when it navigates somewhere (a project tile).
|
|
14
|
+
- Don't make a card interactive *and* put buttons inside it: nested targets break the press metaphor. Keep the card static and let its buttons lift.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# ChartBrush
|
|
2
|
+
|
|
3
|
+
Picks a time window on an overview of the whole series, under the plot: look at a year and read a fortnight without losing where you are (overview + detail; Cockburn, Karlson & Bederson, 2009). Put it inside a `LineChart`, `AreaChart` or `BarChart` over time.
|
|
4
|
+
|
|
5
|
+
**Why it's sunk, and why the window lifts.** Most chart libraries draw a brush as a grey rectangle with handles, which nobody recognises as something to drag. Altitude already has a build for "a window you slide along a range": the range `Slider`. The overview is an opening, a 40px trench (`surface-trench`, `trench`) with the series lying in it as a flat 1.5px line, because it's a track you slide along. Like a `Sparkline`'s line it spans its own low to high: it's there for the shape, and an area that didn't stand on zero would misstate every value. The window is a raised lens over it: level 1, a 2px `accent` rim and a clear face, level 2 on hover and 3 while held. Its two grips are the slider's thumbs. Outside the window the overview recedes to 35%.
|
|
6
|
+
|
|
7
|
+
**What it does.**
|
|
8
|
+
|
|
9
|
+
- **Drag the lens** to pan. The window keeps its length and stops at the ends.
|
|
10
|
+
- **Drag a grip** to resize. A grip pushed against the other stops a record short; the window always holds at least two records.
|
|
11
|
+
- **Press outside the lens** to centre the window there, then keep dragging to pan from it.
|
|
12
|
+
- A held grip shows its date in a bubble below the overview, clear of the plot's axis; panning shows both. Near an end, the bubble aligns to that end so it never leaves the frame.
|
|
13
|
+
- The frame says the window in words under the overview: "1 Mar – 30 Apr 2026".
|
|
14
|
+
- The chart shows only the window, and its axis spans exactly the window. The plot cross-fades to it, because the x values under the marks changed ([Motion](../../visual/Motion.md), "Charts", rule 5).
|
|
15
|
+
|
|
16
|
+
**Accessibility.** The brush is a group named "Overview, 1 Jan – 31 Dec 2026" (or your `aria-label`). It follows the APG multi-thumb slider: "Window start" and "Window end" are each a Tab stop and a `role="slider"` with the date as `aria-valuetext`, bounded by the other grip; arrows step a record, Page Up/Down a tenth, and Home/End go to the grip's own bounds. The lens is a focusable group named "Window, 1 Mar – 30 Apr 2026": arrows pan both ends by a record, Page Up/Down by a tenth, Home and End to the start and the end.
|
|
17
|
+
|
|
18
|
+
**Without height.** Under forced colours the trench is dashed, the lens is a 1px `Highlight` border (2px while held), the grips are outlined in `CanvasText` and the line is `CanvasText`. With `contrast="more"` the lens rim is `ink`.
|
|
19
|
+
|
|
20
|
+
**Props.** `series`: the series the overview draws (default the chart's first shown series); `aria-label`; `className`. The window itself is the chart type's:
|
|
21
|
+
|
|
22
|
+
- `window={{ start, end }}` with `onWindowChange`, controlled. `start` and `end` are the records' own x values, so a `DateRange` from a `DateRangePicker` fits as it is, and the picker and the brush share one state. `null` leaves an end open.
|
|
23
|
+
- Neither, and the brush keeps its own window, starting over every record.
|
|
24
|
+
|
|
25
|
+
**You provide:** a `ChartBrush` inside the chart type, and the window state if anything else needs it.
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
const [range, setRange] = React.useState({ start: '2026-03-01', end: '2026-04-30' });
|
|
29
|
+
|
|
30
|
+
<DateRangePicker value={range} onValueChange={setRange} />
|
|
31
|
+
<ChartContainer config={config}>
|
|
32
|
+
<AreaChart data={year} x="day" window={range} onWindowChange={setRange}>
|
|
33
|
+
<ChartBrush />
|
|
34
|
+
</AreaChart>
|
|
35
|
+
</ChartContainer>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- Do use a brush when the whole range matters for context and a part of it for reading: a year and a fortnight.
|
|
39
|
+
- Do give the window a typed twin (a `DateRangePicker`) when exact dates matter; dragging is for finding, typing for stating.
|
|
40
|
+
- Don't use a brush for a few dozen records: the chart can show them all.
|
|
41
|
+
- Don't put a brush on a chart of categories; it windows records in their order, which only means something over time.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# ChartContainer
|
|
2
|
+
|
|
3
|
+
The frame every chart shares. It sizes the plot, turns data into positions (the scales), draws the axes, the gridlines and the ground line, gives each series a label, a format and a colour slot, and names and summarises the chart. Every chart type renders inside it, and so can marks you draw yourself.
|
|
4
|
+
|
|
5
|
+
**Why it's flush.** A chart is the record drawn as a shape (see [Charts](../../visual/Charts.md)), so everything in the frame lies on whatever it sits on, the page or a `Card`'s `surface-raised`, at no level and with no shadow. Amounts are length and position on a flat plot standing on the ground line, never height: the zero baseline is a 1px `ink-muted` rule, the same ground line `Stepper` and the `Drilldown` staircase stand on. Gridlines are `line` hairlines, horizontal only. When zero isn't on the axis (a line chart may leave it off), the plot's foot is an ordinary gridline, not the ground line. Tick labels are caption text in `ink-muted` with tabular figures. Marks wear `data-*` colours and nothing else. On resize the frame lays out again without animating.
|
|
6
|
+
|
|
7
|
+
**Without height, and without colour.** Nothing here relies on height to begin with. Colour is never alone either: each slot carries a redundant encoding, fixed like its colour.
|
|
8
|
+
|
|
9
|
+
| Slot | Line dash | Marker | Fill hatch |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| `data-1` | solid | circle | none (solid) |
|
|
12
|
+
| `data-2` | 6 3 | square | 45° lines |
|
|
13
|
+
| `data-3` | 2 2 | triangle | 135° lines |
|
|
14
|
+
| `data-4` | 8 3 2 3 | diamond | dots |
|
|
15
|
+
| `data-5` | 12 4 | ring | horizontal lines |
|
|
16
|
+
| `data-6` | 4 2 1 2 | cross | crosshatch |
|
|
17
|
+
| `data-other` | 1 3 | circle | dots |
|
|
18
|
+
|
|
19
|
+
Patterns are on under forced colours and in print, and anywhere with `patterns` (for readers who want them, or a chart whose slots fall in the colour-blind warning band). Under forced colours:
|
|
20
|
+
|
|
21
|
+
- marks are `CanvasText`, hatches are `CanvasText` on `Canvas`, and fills get a `CanvasText` edge;
|
|
22
|
+
- tick labels and the ground line are `CanvasText`, and gridlines are `GrayText`;
|
|
23
|
+
- `directLabels` turns on, as it does in print, so chart types label series at their marks.
|
|
24
|
+
|
|
25
|
+
With `contrast="more"`, lines are 3px, adjacent marks are 3px apart, fills get a 1px `ink` edge, gridlines take `line-control` and the ground line `ink`.
|
|
26
|
+
|
|
27
|
+
**Accessibility.** It's a `figure`, named by its `title` or by `aria-labelledby` (point it at the `CardTitle`), and described by its description and a summary. Pass `summary` with what the chart shows ("Revenue rose 44% while costs held flat."): it's shown under the plot. Without it a summary is generated for screen readers: the chart's kind, its series, the x range, and each series' lowest and highest values with where they fall. The plot is a `group` with `aria-roledescription="chart"`, and its SVG is hidden from assistive technology.
|
|
28
|
+
|
|
29
|
+
**Say one thing first.** `takeaway` is the one thing the chart says, set as its heading in `text-heading` ink ("Cedar deployed nearly three times as often as Delta"). The `title` then sits above it as a caption-sized `ink-muted` label, so the chart is still named: the figure's accessible name stays the title, and its description is the takeaway, then the summary. When the takeaway's words change with the data they cross-fade over `dur-travel-2`, without the afterglow. Write it yourself, from the data shown; nothing generates one, because a slanted or trivial headline is worse than none. In a `Card`, name the chart with the figure's `title` rather than a `CardTitle`, so the takeaway isn't a second heading under the card's. See [Charts](../../visual/Charts.md), rule 9.
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
<ChartContainer config={config} title="Deploys by team" takeaway="Cedar deployed nearly three times as often as Delta"
|
|
33
|
+
summary="Cedar deployed most, 51 times; Delta least, 19.">
|
|
34
|
+
<BarChart data={deploys} x="team" sort="desc" />
|
|
35
|
+
</ChartContainer>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Axis text.** Tick labels are captions: 13px, weight 400, `ink-muted`, tabular figures. Weight 500 is for labels on controls, so axis text is lighter, and the frame's words rank below the marks, the ground line and the references. (This changed from 500 12.5px in #193, with no API change.) `yLabels="inline"` sets each y label just above its gridline at the plot's left, on a halo of the plot's colour, so the plot runs the card's full width: 56px more in a 440px card, about a fifth of the plot at phone width. The top label carries the unit ("250 ms"), the rest only the number. A label that would sit on a line, an area or a bar is left out, and its gridline stays. The ground line goes unlabelled, because it's zero. It's for vertical cartesian charts; the default stays `'gutter'`.
|
|
39
|
+
|
|
40
|
+
**The keyboard path.** The plot is one Tab stop: a layer of focusable nodes over the SVG, one level at a time, with a roving tabindex. After Olli and Data Navigator, it goes from the summary to the series to the data:
|
|
41
|
+
|
|
42
|
+
| Key | On the summary | On a series | On a point |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
| ←/→ | — | previous/next series | previous/next x |
|
|
45
|
+
| ↑/↓ | — | previous/next series | same x, previous/next series |
|
|
46
|
+
| Home/End | — | first/last series | first/last x |
|
|
47
|
+
| Page Up/Down | — | — | ±10% of the x range |
|
|
48
|
+
| Enter | into the series | into its points | — |
|
|
49
|
+
| Escape | — | back to the summary | back to its series |
|
|
50
|
+
|
|
51
|
+
A chart with one series skips the series level. In a stacked `AreaChart`, whose bands are drawn from the bottom up, ↑ moves to the band above and ↓ to the one below. The nodes are named:
|
|
52
|
+
|
|
53
|
+
- **the summary:** the written `summary`, or the generated one, then "Press Enter to explore the series";
|
|
54
|
+
- **a series:** "Revenue, 12 points, $61K to $104K, rising. 1 of 2.";
|
|
55
|
+
- **a point:** "Mar 1, 2026, Revenue, $83K. Down 2% from Feb 1, 2026. 9 of 12." Stacks add "Total …"; a stacked `AreaChart` says the band's share instead ("34% of 3.2K").
|
|
56
|
+
|
|
57
|
+
A focused point gets a 2px `accent-ink` ring on its mark (`Highlight` under forced colours) and opens the readout, so keyboard and pointer get the same feedback. A focused series draws the others receded. Chart types say where each of their marks sits (`chartPoint`); hand-drawn marks get a ring at `(xOf, y(value))`. The table view is #64.
|
|
58
|
+
|
|
59
|
+
**Scales.** All in-house, no dependency:
|
|
60
|
+
|
|
61
|
+
- `band` for categories, in data order, with `bandPadding` (default 0.2) between them;
|
|
62
|
+
- `time` for ISO `'YYYY-MM-DD'` strings (read as UTC, as `DatePicker` writes them), timestamps or `Date`s, with ticks at whole days, weeks, months, quarters or years, as many as fit;
|
|
63
|
+
- `linear` for other numbers;
|
|
64
|
+
- y is always linear, with about five 1–2–5 ticks, made nice, and includes zero unless you pass `zero={false}`. Bars always need zero.
|
|
65
|
+
|
|
66
|
+
x is inferred from the first value of `x`; set `xScale` to override it. Numbers and dates are formatted with `Intl`, in the reader's locale unless you pass `locale`.
|
|
67
|
+
|
|
68
|
+
**Colour.** Series take slots in config order: the first is `data-1`, the seventh and later are `data-other`. Set `color: 'data-3'` to pin one. The order is fixed for the chart's life, so hiding or filtering a series never repaints the others. The figure also sets `--color-<key>` for each series, for your own CSS.
|
|
69
|
+
|
|
70
|
+
**Focus.** A chart type's `focus` says what the chart is about: `focus={['eu']}` keeps those series' colours and draws the rest in `data-context`, one flat neutral, with their end labels and legend keys in `ink-muted`; on bars and dots, `focus={(d) => d.team === 'Cedar'}` does the same for records. It's said in words: "Highlighted: EU." leads the generated summary (and follows a written one), focused legend keys say "highlighted", and the readout and the keyboard path list focused series first. Changing it cross-fades the colours over `dur-travel-1`, at once under reduced motion. It's the author's statement of importance, not a reader's control; the reader's is `relevance`, which recedes misses to 35% without repainting, so the two never read alike. Your own marks get it from `encode(key, d).context` and its colour. Pair it with a `takeaway`: when the words and the colour pick out the same thing, that's what people remember (E102, E105).
|
|
71
|
+
|
|
72
|
+
**Motion.** When the data changes, the frame moves its marks by the one engine every chart shares ([Motion](../../visual/Motion.md), "Charts"). Values glide at one level on `ease-glide`, after the axis has rescaled if its domain changed, never at the same time. Re-sorted bands are carried, and records and series entering or leaving cross-fade. First render, a change of x under a line, more than 500 marks and reduced motion all cross-fade instead. Resizing never animates. The plot's `data-stage` says where a change is (`rescale`, `values` or `fade`).
|
|
73
|
+
|
|
74
|
+
**Loading, refetch and nothing.** Pass what you have (`data={query.data}`) and the frame does the rest, Suspense-native like `DataTable`:
|
|
75
|
+
|
|
76
|
+
| `data` | What shows |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `undefined`, or a pending promise | an imprint after `dur-grace`: the title, description and legend in place, the plot pressed into the page in the chart's shape (`surface-well` bars, a band, or an area), no labels, `aria-busy` and "Loading" |
|
|
79
|
+
| arrives | the marks fade in place. Unlike `Await`'s content they never rise out of the imprint, because a rising bar would leave the ground line and misstate its value |
|
|
80
|
+
| `null` or `[]` | an `EmptyState` at widget size in the plot's own box, with no axes; set its words and action with `empty={{ title, description, action }}` (on the container or the chart type). All-zero data is real data, and is drawn |
|
|
81
|
+
| a new promise while data shows, or `refreshing` | the old chart stays; after `dur-grace` its marks drop to half strength, with `aria-busy` and "Updating". Nothing is imprinted over what someone is reading |
|
|
82
|
+
| rejected | your error boundary (put an `Alert` there), as with `Await` |
|
|
83
|
+
|
|
84
|
+
A `RevealGroup` around a row of charts and stats lands them together.
|
|
85
|
+
|
|
86
|
+
**The table view.** `views` adds a segmented Chart | Table control beside the title: the same record as a sortable `DataTable`, x first (headed by `xLabel`), a column per series in its own format, and a Total for stacks. It sits in the chart's own box and scrolls there, so switching doesn't move the page; the two views cross-fade over `dur-travel-2`, and the hidden one is inert. `views="table"` starts on the table; `view` and `onViewChange` control it. It's opt-in so that small widget charts don't each grow a control; give it to any chart people may want to read, copy or check.
|
|
87
|
+
|
|
88
|
+
**Not yet the record.** `BarChart`, `LineChart` and `AreaChart` take `status={(d) => 'partial' | 'projected' | undefined}`. The ground is the record, and a projection or an unfinished period isn't yet, so it's drawn hatched or dashed, never raised, and said in words: "So far" or "Projected" in point names ("Oct 1, 2026, Revenue, $61K, so far."), after the readout's heading, in a Status column in the table, and at the end of the generated summary, whose trend and extremes are read from the settled records. A 1px `line-control` rule named "Today" marks where the record ends. For your own marks, `useChart()` gives `status(d)`, `statusRanges` (the stretches of x that are pending, for `ChartStatusClip`) and `encode(key).pending`, `pendingOpacity` and `pendingDash`.
|
|
89
|
+
|
|
90
|
+
**Marks you can select or drill into.** Give a chart type `onSelect` (with `selected`), or `drill` inside a `Drilldown`, and its marks act. Hover is a promise, so only then do they respond to the pointer:
|
|
91
|
+
|
|
92
|
+
- **Hover:** a pointer cursor, a 2px `ink` outline on the record's marks (appearing over `dur-travel-1`), and the readout ends with the action: "Select to filter", "Select to clear", "Select to open". A mark can't lift without misstating its value, so the outline, cursor and readout carry the cue, and height stays with the readout. Without `onSelect` or `drill`, marks don't respond at all; only the crosshair and readout do.
|
|
93
|
+
- **Selected:** the selected record keeps its full colour and the others recede to 35%, in the same language as a filter. The frame says "Filtered to EU" with a "Clear" ghost button, and so does the summary. Choosing it again, Clear or Escape clears it.
|
|
94
|
+
- **Drilling:** `drill={(d) => d.q}` returns the id of the current `Drilldown` node's child to open. The child is laid on top as a sheet with a step added to the breadcrumb staircase, `Drilldown`'s own choreography, and focus moves to the new chart's summary. A node with `content` and `children` shows its content and lets the chart open the children (`useDrilldown().open(id)`).
|
|
95
|
+
- **Keyboard and touch:** on a point, Enter or Space selects or drills, and points carry `aria-pressed` (or `aria-haspopup` for a drill) and say what Enter does. A tap selects; press and hold still shows the readout. Under forced colours the outline is `Highlight`.
|
|
96
|
+
|
|
97
|
+
**Filtering in place.** `BarChart`, `LineChart` and `AreaChart` take `relevance={(d) => 'match' | 'miss' | undefined}`, to pair with a `FilterBar`, a legend or a selection in another chart. Filtered-out data stays where it was, so nothing you'd just read is reshuffled:
|
|
98
|
+
|
|
99
|
+
- **Misses recede** to 35% opacity, keeping their place and their colour slot; their value labels turn `ink-muted`. A line or an area recedes around each missing record (each owns its stretch of x, to the midpoints with its neighbours), so its shape never changes. The change cross-fades over `dur-travel-1`.
|
|
100
|
+
- **The axis holds** by default. `rescale` fits it to the matches, and the rescale is staged like any other.
|
|
101
|
+
- **It's said in words.** The frame shows "4 of 12 match" (`countLabel` rewrites it: "4 of 12 services match"), and so does the summary. A miss says "Not matching." in its point name and readout.
|
|
102
|
+
- **Contrast only.** Elsewhere relevance raises matches, but marks can't rise, so in a chart it's spent as contrast alone. Under forced colours misses are `GrayText`.
|
|
103
|
+
|
|
104
|
+
`useChart()` gives `relevance(d)`, `hasRelevance` and `relevanceRegions`; wrap your own marks in `ChartRelevance`. `BarList` takes the same `relevance`, and there rows rise and sink as `TableRow`s do.
|
|
105
|
+
|
|
106
|
+
**Since your last visit.** `since={{ data: snapshot, label: 'since Tuesday 09:00' }}` answers "what moved while I was away?", which the afterglow can't when the change came before the page did. Records are matched by the chart's id or by x; a new one counts as changed. A changed bar carries a 2px `ink` tick at its old value (the comparison's mark, as `compare` draws it), a changed line point a small hollow `ink-muted` ring. The frame says "3 changed since Tuesday 09:00" in a `Switch` that turns on a filter for the changed records, so the rest recede to 35% where they stand. On arrival, up to four changed marks take `live`'s afterglow; more, and the caption takes it instead. Point names ("Up 40% since Tuesday 09:00"), the readout, the table and the summary ("3 of 8 changed since Tuesday 09:00, most of all Webhooks, up 75%.") say what changed. Keeping what each person last saw, and when, is the application's job.
|
|
107
|
+
|
|
108
|
+
**Live data.** `live` is for charts that update on their own (deploys, error rates, queue depth). People miss changes they weren't attending to, so, as elsewhere in Altitude, a change shows at once and leaves a fading trace:
|
|
109
|
+
|
|
110
|
+
- **A changed mark glows.** A point or bar whose value is new or changed carries a 2px `ink` outline (an outline, not height), full for the first second and decaying over `dur-settle` on `ease-decay`, with its value beside it for that first second. Its point name says "Changed." until it fades. Afterglows share the page's attention budget (`riseDelay`), so no more than four begin in any 250ms.
|
|
111
|
+
- **A whole refresh is one change.** If more than four marks change at once, none glows; the "Updated 12:04" caption under the plot gets a ground-level tint instead, as a table re-sort does.
|
|
112
|
+
- **A window glides.** A chart type's `window` (`{ minutes: 15 }`) keeps the latest stretch of x. When it moves on, x glides one step on `ease-glide` over `dur-travel-2` and the marks ride with it, clipped to the plot. The motion is the passage of time. Under reduced motion it updates in place.
|
|
113
|
+
- **It holds still while you read.** While the pointer or keyboard focus is in the plot, updates wait and the caption says "new data when you move away"; they land when you leave, so the readout never changes under you. `pausable` adds a "Pause updates" `Switch` (WCAG 2.2.2); `paused` and `onPausedChange` control it.
|
|
114
|
+
- **It doesn't chatter.** Nothing is announced per tick. `announce={(data, previous) => …}` returns a sentence for what matters ("Error rate crossed 2%"), said politely.
|
|
115
|
+
|
|
116
|
+
**References and annotations.** [`ChartReference`](../ChartReference/README.md) draws a target, a limit, a normal range or the mean as a flush 1px line or a flat band, and `ChartAnnotation` marks an event on x with a `Badge` plate and a leader. Place them inside the chart type or the container. The frame draws them, so their labels are laid out together and kept clear of each other, and they're read after the summary.
|
|
117
|
+
|
|
118
|
+
**Charts that draw their own plot.** A [`Heatmap`](../Heatmap/README.md) has no value axis: its cells lie on two categories it names itself. The frame leaves out the gridlines, the ground line, the axes and the crosshair, makes the plot as tall as the cells need, puts the readout beside the cell, and goes from the summary straight to the cells, which the chart type moves between by row and column. A [`ScatterChart`](../ScatterChart/README.md) keeps the axes but both are measures: the frame finds the nearest dot in two dimensions, leaves out the crosshair, puts the readout beside the dot, and lets the chart type name each axis in room above and below the plot. Its series are its groups rather than the config's keys; the config names the two measures. Records are known by their own id rather than by x, for motion, the table, selection and live changes. Records without one cross-fade rather than glide.
|
|
119
|
+
|
|
120
|
+
**Readout and legend.** Every chart has a readout ([`ChartTooltip`](../ChartTooltip/README.md)); add a [`ChartLegend`](../ChartLegend/README.md) to name series, or to let people hide and show them. Hidden series keep their slots and leave the scales. A [`ChartBrush`](../ChartBrush/README.md) inside a chart over time adds an overview under the plot to pick a window on.
|
|
121
|
+
|
|
122
|
+
**Your own marks.** `useChart()` gives you the following, and children are drawn inside the plot, in its coordinates, under the ground line's rule:
|
|
123
|
+
|
|
124
|
+
- `width` and `height`: the plot's size;
|
|
125
|
+
- the `x` and `y` scales, and `xOf(datum)` (a band's centre, or a time or value);
|
|
126
|
+
- `ground`: the y of the ground line;
|
|
127
|
+
- `data`: the records as they're shown right now (gliding values, lingering leavers);
|
|
128
|
+
- `alpha(datum, seriesKey)`: how present a mark is while it enters or leaves, to multiply into its opacity;
|
|
129
|
+
- `series`, with their colours and formats, and `format`;
|
|
130
|
+
- `encode(key)`: the series' `fill`, `stroke`, `dash`, `marker` and `edge` for the current mode. `ChartMarker` draws the marker shape;
|
|
131
|
+
- `patterns`, `directLabels`, `strokeWidth` and `gap`;
|
|
132
|
+
- `status(datum)`, `hasStatus` and `statusRanges`, for data that isn't yet the record.
|
|
133
|
+
|
|
134
|
+
Draw with `encode()` rather than `series.color`, and forced colours, print and `contrast="more"` are handled for you.
|
|
135
|
+
|
|
136
|
+
Draw from these and your marks move like every other chart's.
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
function Target({ value }) {
|
|
140
|
+
const { y, width } = useChart();
|
|
141
|
+
return <line x1={0} x2={width} y1={y(value)} y2={y(value)} stroke="var(--ink-muted)" strokeDasharray="4 3" />;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**You provide:** `config` (each data key: `label`, optionally `format` and `color`), the `data` and its `x` key (or a chart type inside that takes them), and a size: `aspect` (width ÷ height, default 2) or `height` in px. Usually a `summary`, and `aria-labelledby` or `title`; a `takeaway` when the chart makes a point.
|
|
146
|
+
|
|
147
|
+
- Do give every chart a summary that says what it shows, not what it is.
|
|
148
|
+
- Do put a chart on a dashboard in a `Card`: the card is raised because it's something you work with, not because its data is.
|
|
149
|
+
- Do keep one y axis. Two measures of different scale are two charts.
|
|
150
|
+
- Don't give marks a shadow, a wall, a sheen, a gradient or a lift.
|
|
151
|
+
- Don't colour text with a series colour; labels and ticks wear `ink` and `ink-muted`.
|
|
152
|
+
- Don't draw more than about 2,000 marks: SVG gets slow. Aggregate, or use a canvas library with Altitude's theme (#77).
|