altitude-design 0.3.2 → 0.4.1

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.
Files changed (294) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/altitude.mjs +120 -20
  3. package/dist/guidance/README.md +4 -0
  4. package/dist/guidance/agent-setup.md +3 -2
  5. package/dist/guidance/components/AILabel/README.md +60 -0
  6. package/dist/guidance/components/Accordion/README.md +32 -0
  7. package/dist/guidance/components/AgentFleet/README.md +81 -0
  8. package/dist/guidance/components/AgentLanes/README.md +53 -0
  9. package/dist/guidance/components/AgentRequests/README.md +76 -0
  10. package/dist/guidance/components/Alert/README.md +71 -0
  11. package/dist/guidance/components/AltitudeProvider/README.md +111 -0
  12. package/dist/guidance/components/AppShell/README.md +121 -0
  13. package/dist/guidance/components/AreaChart/README.md +101 -0
  14. package/dist/guidance/components/Artifact/README.md +99 -0
  15. package/dist/guidance/components/Avatar/README.md +75 -0
  16. package/dist/guidance/components/Await/README.md +128 -0
  17. package/dist/guidance/components/Badge/README.md +66 -0
  18. package/dist/guidance/components/BarChart/README.md +109 -0
  19. package/dist/guidance/components/BarList/README.md +87 -0
  20. package/dist/guidance/components/Board/README.md +98 -0
  21. package/dist/guidance/components/Breadcrumb/README.md +66 -0
  22. package/dist/guidance/components/Button/README.md +58 -0
  23. package/dist/guidance/components/Card/README.md +52 -0
  24. package/dist/guidance/components/ChartBrush/README.md +98 -0
  25. package/dist/guidance/components/ChartContainer/README.md +229 -0
  26. package/dist/guidance/components/ChartLegend/README.md +78 -0
  27. package/dist/guidance/components/ChartReference/README.md +115 -0
  28. package/dist/guidance/components/ChartTooltip/README.md +101 -0
  29. package/dist/guidance/components/Checkbox/README.md +46 -0
  30. package/dist/guidance/components/Checklist/README.md +137 -0
  31. package/dist/guidance/components/CodeBlock/README.md +72 -0
  32. package/dist/guidance/components/ColorPicker/README.md +69 -0
  33. package/dist/guidance/components/Combobox/README.md +51 -0
  34. package/dist/guidance/components/CommandPalette/README.md +204 -0
  35. package/dist/guidance/components/CommentThread/README.md +81 -0
  36. package/dist/guidance/components/Confirmation/README.md +94 -0
  37. package/dist/guidance/components/ContextMenu/README.md +81 -0
  38. package/dist/guidance/components/Conversation/README.md +169 -0
  39. package/dist/guidance/components/CopyButton/README.md +54 -0
  40. package/dist/guidance/components/DataGrid/README.md +157 -0
  41. package/dist/guidance/components/DatePicker/README.md +116 -0
  42. package/dist/guidance/components/Dialog/README.md +204 -0
  43. package/dist/guidance/components/DiffView/README.md +107 -0
  44. package/dist/guidance/components/DotPlot/README.md +82 -0
  45. package/dist/guidance/components/Drilldown/README.md +43 -0
  46. package/dist/guidance/components/DropZone/README.md +71 -0
  47. package/dist/guidance/components/DropdownMenu/README.md +63 -0
  48. package/dist/guidance/components/EditRegion/README.md +263 -0
  49. package/dist/guidance/components/EmptyState/README.md +66 -0
  50. package/dist/guidance/components/FeatureGate/README.md +67 -0
  51. package/dist/guidance/components/Field/README.md +46 -0
  52. package/dist/guidance/components/FilterBar/README.md +132 -0
  53. package/dist/guidance/components/FormattedNumber/README.md +81 -0
  54. package/dist/guidance/components/Heatmap/README.md +116 -0
  55. package/dist/guidance/components/HoldButton/README.md +48 -0
  56. package/dist/guidance/components/HoverCard/README.md +71 -0
  57. package/dist/guidance/components/InlineCitation/README.md +56 -0
  58. package/dist/guidance/components/InlineEdit/README.md +379 -0
  59. package/dist/guidance/components/Input/README.md +63 -0
  60. package/dist/guidance/components/InputOTP/README.md +76 -0
  61. package/dist/guidance/components/ItemGroup/README.md +193 -0
  62. package/dist/guidance/components/Kbd/README.md +36 -0
  63. package/dist/guidance/components/LineChart/README.md +109 -0
  64. package/dist/guidance/components/Link/README.md +69 -0
  65. package/dist/guidance/components/Markdown/README.md +113 -0
  66. package/dist/guidance/components/Meter/README.md +81 -0
  67. package/dist/guidance/components/NotificationCenter/README.md +67 -0
  68. package/dist/guidance/components/NumberInput/README.md +66 -0
  69. package/dist/guidance/components/PageHeader/README.md +74 -0
  70. package/dist/guidance/components/Pagination/README.md +58 -0
  71. package/dist/guidance/components/PasswordInput/README.md +70 -0
  72. package/dist/guidance/components/PermissionMatrix/README.md +68 -0
  73. package/dist/guidance/components/Pile/README.md +54 -0
  74. package/dist/guidance/components/PlanPicker/README.md +62 -0
  75. package/dist/guidance/components/Popover/README.md +48 -0
  76. package/dist/guidance/components/Progress/README.md +41 -0
  77. package/dist/guidance/components/PromptInput/README.md +138 -0
  78. package/dist/guidance/components/PropertyList/README.md +137 -0
  79. package/dist/guidance/components/Question/README.md +81 -0
  80. package/dist/guidance/components/RadioGroup/README.md +47 -0
  81. package/dist/guidance/components/RelativeTime/README.md +73 -0
  82. package/dist/guidance/components/RemovalReceipt/README.md +111 -0
  83. package/dist/guidance/components/Resizable/README.md +65 -0
  84. package/dist/guidance/components/SaveBar/README.md +65 -0
  85. package/dist/guidance/components/SavedViews/README.md +82 -0
  86. package/dist/guidance/components/ScatterChart/README.md +104 -0
  87. package/dist/guidance/components/Schedule/README.md +142 -0
  88. package/dist/guidance/components/ScrollArea/README.md +35 -0
  89. package/dist/guidance/components/SecretField/README.md +74 -0
  90. package/dist/guidance/components/Select/README.md +97 -0
  91. package/dist/guidance/components/Separator/README.md +70 -0
  92. package/dist/guidance/components/Sheet/README.md +126 -0
  93. package/dist/guidance/components/Skeleton/README.md +50 -0
  94. package/dist/guidance/components/Slider/README.md +60 -0
  95. package/dist/guidance/components/SortableList/README.md +45 -0
  96. package/dist/guidance/components/Sparkline/README.md +87 -0
  97. package/dist/guidance/components/SplitButton/README.md +65 -0
  98. package/dist/guidance/components/Stat/README.md +92 -0
  99. package/dist/guidance/components/Stepper/README.md +90 -0
  100. package/dist/guidance/components/Suggestions/README.md +87 -0
  101. package/dist/guidance/components/Surface/README.md +48 -0
  102. package/dist/guidance/components/Switch/README.md +38 -0
  103. package/dist/guidance/components/SyncIndicator/README.md +78 -0
  104. package/dist/guidance/components/Table/README.md +138 -0
  105. package/dist/guidance/components/Tabs/README.md +73 -0
  106. package/dist/guidance/components/TagInput/README.md +116 -0
  107. package/dist/guidance/components/Terminal/README.md +69 -0
  108. package/dist/guidance/components/ThreadList/README.md +110 -0
  109. package/dist/guidance/components/TimeInput/README.md +63 -0
  110. package/dist/guidance/components/Timeline/README.md +87 -0
  111. package/dist/guidance/components/Toaster/README.md +54 -0
  112. package/dist/guidance/components/Toggle/README.md +85 -0
  113. package/dist/guidance/components/Toolbar/README.md +106 -0
  114. package/dist/guidance/components/Tooltip/README.md +41 -0
  115. package/dist/guidance/components/Tour/README.md +73 -0
  116. package/dist/guidance/components/Trace/README.md +129 -0
  117. package/dist/guidance/components/Tree/README.md +171 -0
  118. package/dist/guidance/evidence.json +18 -0
  119. package/dist/guidance/index.json +811 -7
  120. package/dist/guidance/llms.txt +153 -2
  121. package/dist/guidance/manifest.json +133 -12
  122. package/dist/guidance/patterns/screen-layout.md +1 -1
  123. package/dist/guidance/references/build/checks/agent-setup.mjs +7 -0
  124. package/dist/guidance/references/build/checks/guidance.mjs +8 -2
  125. package/dist/guidance/references/build/checks/source-check.mjs +101 -0
  126. package/dist/guidance/rules.json +155 -0
  127. package/dist/guidance/visual/ActionsInPlace.md +463 -0
  128. package/dist/guidance/visual/Charts.md +119 -0
  129. package/dist/guidance/visual/Distinctiveness.md +106 -0
  130. package/dist/guidance/visual/Elevation.md +1 -1
  131. package/dist/guidance/visual/Mobile.md +64 -0
  132. package/dist/guidance/visual/Motion.md +225 -0
  133. package/dist/guidance/visual/README.md +98 -0
  134. package/dist/guidance/visual/Research.md +1 -1
  135. package/dist/guidance/visual/Theming.md +349 -0
  136. package/dist/types/agent-fleet.d.ts +28 -0
  137. package/dist/types/ai-label.d.ts +7 -0
  138. package/dist/types/app-shell.d.ts +23 -0
  139. package/dist/types/area-chart.d.ts +8 -0
  140. package/dist/types/artifact.d.ts +14 -0
  141. package/dist/types/bar-chart.d.ts +6 -0
  142. package/dist/types/bar-list.d.ts +6 -0
  143. package/dist/types/breadcrumb.d.ts +6 -0
  144. package/dist/types/chart-brush.d.ts +6 -0
  145. package/dist/types/chart.d.ts +29 -1
  146. package/dist/types/checklist.d.ts +6 -0
  147. package/dist/types/code-block.d.ts +6 -0
  148. package/dist/types/collections.d.ts +15 -0
  149. package/dist/types/color-picker.d.ts +6 -0
  150. package/dist/types/command.d.ts +13 -0
  151. package/dist/types/comment-thread.d.ts +11 -0
  152. package/dist/types/confirmation.d.ts +8 -0
  153. package/dist/types/context-menu.d.ts +11 -0
  154. package/dist/types/controls.d.ts +174 -10
  155. package/dist/types/conversation.d.ts +21 -0
  156. package/dist/types/copy-button.d.ts +6 -0
  157. package/dist/types/core.d.ts +31 -0
  158. package/dist/types/data-grid.d.ts +25 -0
  159. package/dist/types/date-picker.d.ts +6 -0
  160. package/dist/types/diff-view.d.ts +8 -0
  161. package/dist/types/dot-plot.d.ts +6 -0
  162. package/dist/types/edit-region.d.ts +23 -0
  163. package/dist/types/empty.d.ts +9 -0
  164. package/dist/types/feature-gate.d.ts +8 -0
  165. package/dist/types/filter-bar.d.ts +8 -0
  166. package/dist/types/formatted-number.d.ts +6 -0
  167. package/dist/types/heatmap.d.ts +6 -0
  168. package/dist/types/inline-citation.d.ts +6 -0
  169. package/dist/types/inline-edit.d.ts +7 -0
  170. package/dist/types/input-otp.d.ts +11 -0
  171. package/dist/types/item-group.d.ts +29 -0
  172. package/dist/types/line-chart.d.ts +6 -0
  173. package/dist/types/link.d.ts +8 -1
  174. package/dist/types/markdown.d.ts +6 -0
  175. package/dist/types/meter.d.ts +6 -0
  176. package/dist/types/nav.d.ts +6 -0
  177. package/dist/types/notifications.d.ts +6 -0
  178. package/dist/types/number-input.d.ts +6 -0
  179. package/dist/types/overlays.d.ts +61 -0
  180. package/dist/types/page-header.d.ts +14 -0
  181. package/dist/types/pagination.d.ts +6 -0
  182. package/dist/types/password-input.d.ts +6 -0
  183. package/dist/types/permission-matrix.d.ts +7 -0
  184. package/dist/types/pile.d.ts +6 -0
  185. package/dist/types/plan-picker.d.ts +6 -0
  186. package/dist/types/prompt-input.d.ts +11 -0
  187. package/dist/types/property-list.d.ts +17 -0
  188. package/dist/types/question.d.ts +11 -0
  189. package/dist/types/relative-time.d.ts +8 -1
  190. package/dist/types/removal-receipt.d.ts +18 -1
  191. package/dist/types/save-bar.d.ts +6 -0
  192. package/dist/types/saved-views.d.ts +12 -0
  193. package/dist/types/scatter-chart.d.ts +8 -0
  194. package/dist/types/schedule.d.ts +21 -0
  195. package/dist/types/secret-field.d.ts +6 -0
  196. package/dist/types/select.d.ts +12 -0
  197. package/dist/types/separator.d.ts +8 -1
  198. package/dist/types/sheet.d.ts +19 -0
  199. package/dist/types/sparkline.d.ts +6 -0
  200. package/dist/types/split-button.d.ts +7 -0
  201. package/dist/types/stat.d.ts +7 -0
  202. package/dist/types/stepper.d.ts +12 -0
  203. package/dist/types/suggestions.d.ts +10 -0
  204. package/dist/types/sync-indicator.d.ts +8 -0
  205. package/dist/types/tag-input.d.ts +8 -0
  206. package/dist/types/terminal.d.ts +6 -0
  207. package/dist/types/thread-list.d.ts +16 -0
  208. package/dist/types/time-input.d.ts +6 -0
  209. package/dist/types/timeline.d.ts +9 -0
  210. package/dist/types/toggle.d.ts +6 -0
  211. package/dist/types/toolbar.d.ts +20 -0
  212. package/dist/types/tour.d.ts +10 -0
  213. package/dist/types/trace.d.ts +16 -0
  214. package/dist/types/tree.d.ts +21 -0
  215. package/dist/types-cjs/agent-fleet.d.cts +28 -0
  216. package/dist/types-cjs/ai-label.d.cts +7 -0
  217. package/dist/types-cjs/app-shell.d.cts +23 -0
  218. package/dist/types-cjs/area-chart.d.cts +8 -0
  219. package/dist/types-cjs/artifact.d.cts +14 -0
  220. package/dist/types-cjs/bar-chart.d.cts +6 -0
  221. package/dist/types-cjs/bar-list.d.cts +6 -0
  222. package/dist/types-cjs/breadcrumb.d.cts +6 -0
  223. package/dist/types-cjs/chart-brush.d.cts +6 -0
  224. package/dist/types-cjs/chart.d.cts +29 -1
  225. package/dist/types-cjs/checklist.d.cts +6 -0
  226. package/dist/types-cjs/code-block.d.cts +6 -0
  227. package/dist/types-cjs/collections.d.cts +15 -0
  228. package/dist/types-cjs/color-picker.d.cts +6 -0
  229. package/dist/types-cjs/command.d.cts +13 -0
  230. package/dist/types-cjs/comment-thread.d.cts +11 -0
  231. package/dist/types-cjs/confirmation.d.cts +8 -0
  232. package/dist/types-cjs/context-menu.d.cts +11 -0
  233. package/dist/types-cjs/controls.d.cts +174 -10
  234. package/dist/types-cjs/conversation.d.cts +21 -0
  235. package/dist/types-cjs/copy-button.d.cts +6 -0
  236. package/dist/types-cjs/core.d.cts +31 -0
  237. package/dist/types-cjs/data-grid.d.cts +25 -0
  238. package/dist/types-cjs/date-picker.d.cts +6 -0
  239. package/dist/types-cjs/diff-view.d.cts +8 -0
  240. package/dist/types-cjs/dot-plot.d.cts +6 -0
  241. package/dist/types-cjs/edit-region.d.cts +23 -0
  242. package/dist/types-cjs/empty.d.cts +9 -0
  243. package/dist/types-cjs/feature-gate.d.cts +8 -0
  244. package/dist/types-cjs/filter-bar.d.cts +8 -0
  245. package/dist/types-cjs/formatted-number.d.cts +6 -0
  246. package/dist/types-cjs/heatmap.d.cts +6 -0
  247. package/dist/types-cjs/inline-citation.d.cts +6 -0
  248. package/dist/types-cjs/inline-edit.d.cts +7 -0
  249. package/dist/types-cjs/input-otp.d.cts +11 -0
  250. package/dist/types-cjs/item-group.d.cts +29 -0
  251. package/dist/types-cjs/line-chart.d.cts +6 -0
  252. package/dist/types-cjs/link.d.cts +8 -1
  253. package/dist/types-cjs/markdown.d.cts +6 -0
  254. package/dist/types-cjs/meter.d.cts +6 -0
  255. package/dist/types-cjs/nav.d.cts +6 -0
  256. package/dist/types-cjs/notifications.d.cts +6 -0
  257. package/dist/types-cjs/number-input.d.cts +6 -0
  258. package/dist/types-cjs/overlays.d.cts +61 -0
  259. package/dist/types-cjs/page-header.d.cts +14 -0
  260. package/dist/types-cjs/pagination.d.cts +6 -0
  261. package/dist/types-cjs/password-input.d.cts +6 -0
  262. package/dist/types-cjs/permission-matrix.d.cts +7 -0
  263. package/dist/types-cjs/pile.d.cts +6 -0
  264. package/dist/types-cjs/plan-picker.d.cts +6 -0
  265. package/dist/types-cjs/prompt-input.d.cts +11 -0
  266. package/dist/types-cjs/property-list.d.cts +17 -0
  267. package/dist/types-cjs/question.d.cts +11 -0
  268. package/dist/types-cjs/relative-time.d.cts +8 -1
  269. package/dist/types-cjs/removal-receipt.d.cts +18 -1
  270. package/dist/types-cjs/save-bar.d.cts +6 -0
  271. package/dist/types-cjs/saved-views.d.cts +12 -0
  272. package/dist/types-cjs/scatter-chart.d.cts +8 -0
  273. package/dist/types-cjs/schedule.d.cts +21 -0
  274. package/dist/types-cjs/secret-field.d.cts +6 -0
  275. package/dist/types-cjs/select.d.cts +12 -0
  276. package/dist/types-cjs/separator.d.cts +8 -1
  277. package/dist/types-cjs/sheet.d.cts +19 -0
  278. package/dist/types-cjs/sparkline.d.cts +6 -0
  279. package/dist/types-cjs/split-button.d.cts +7 -0
  280. package/dist/types-cjs/stat.d.cts +7 -0
  281. package/dist/types-cjs/stepper.d.cts +12 -0
  282. package/dist/types-cjs/suggestions.d.cts +10 -0
  283. package/dist/types-cjs/sync-indicator.d.cts +8 -0
  284. package/dist/types-cjs/tag-input.d.cts +8 -0
  285. package/dist/types-cjs/terminal.d.cts +6 -0
  286. package/dist/types-cjs/thread-list.d.cts +16 -0
  287. package/dist/types-cjs/time-input.d.cts +6 -0
  288. package/dist/types-cjs/timeline.d.cts +9 -0
  289. package/dist/types-cjs/toggle.d.cts +6 -0
  290. package/dist/types-cjs/toolbar.d.cts +20 -0
  291. package/dist/types-cjs/tour.d.cts +10 -0
  292. package/dist/types-cjs/trace.d.cts +16 -0
  293. package/dist/types-cjs/tree.d.cts +21 -0
  294. package/package.json +1 -1
@@ -0,0 +1,58 @@
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.
18
+
19
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
20
+ ## Example
21
+
22
+ `Button` as first used in its preview:
23
+
24
+ ```tsx
25
+ import { Button } from 'altitude-design';
26
+
27
+ <Button variant="primary">Publish changes</Button>
28
+ ```
29
+
30
+ The full preview, as rendered in Storybook and checked in CI (`col`, `row`, `cap` are the preview page's own layout classes, not Altitude's):
31
+
32
+ ```tsx
33
+ import * as React from 'react';
34
+ import { Button, Icons } from 'altitude-design';
35
+
36
+ export default function App() {
37
+ const [busy, setBusy] = React.useState(false);
38
+ return (
39
+ <div className="col">
40
+ <div className="row">
41
+ <Button variant="primary">Publish changes</Button>
42
+ <Button>Preview</Button>
43
+ <Button variant="ghost">Cancel</Button>
44
+ <Button variant="danger">Delete project</Button>
45
+ </div>
46
+ <div className="row">
47
+ <Button variant="primary" size="sm">Small</Button>
48
+ <Button variant="primary" size="lg">Large</Button>
49
+ <Button square aria-label="Close"><Icons.X /></Button>
50
+ <Button variant="primary" loading={busy} onClick={() => { setBusy(true); setTimeout(() => setBusy(false), 1600); }}>Save</Button>
51
+ <Button disabled>Disabled</Button>
52
+ </div>
53
+ <p className="cap">Primary rests at level 2, secondary at 1, ghost at 0. Hover climbs one level; pressing returns to the ground. Click Save: a loading button stays pressed.</p>
54
+ </div>
55
+ );
56
+ }
57
+ ```
58
+ <!-- /example -->
@@ -0,0 +1,52 @@
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.
15
+
16
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
17
+ ## Example
18
+
19
+ `Card` as first used in its preview:
20
+
21
+ ```tsx
22
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'altitude-design';
23
+
24
+ <Card>
25
+ <CardHeader><CardTitle>Usage this month</CardTitle><CardDescription>Static: a grouping surface, it never lifts.</CardDescription></CardHeader>
26
+ <CardContent>18,240 requests · 62% of plan</CardContent>
27
+ </Card>
28
+ ```
29
+
30
+ The full preview, as rendered in Storybook and checked in CI (`grid` is the preview page's own layout class, not Altitude's):
31
+
32
+ ```tsx
33
+ import * as React from 'react';
34
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'altitude-design';
35
+
36
+ export default function App() {
37
+ const [n, setN] = React.useState(0);
38
+ return (
39
+ <div className="grid">
40
+ <Card>
41
+ <CardHeader><CardTitle>Usage this month</CardTitle><CardDescription>Static: a grouping surface, it never lifts.</CardDescription></CardHeader>
42
+ <CardContent>18,240 requests · 62% of plan</CardContent>
43
+ </Card>
44
+ <Card interactive onClick={() => setN(n + 1)}>
45
+ <CardHeader><CardTitle>Open project</CardTitle><CardDescription>Interactive: the whole card is the target, so it lifts and presses.</CardDescription></CardHeader>
46
+ <CardContent>{n ? `Opened ${n}×` : 'Click or press Enter'}</CardContent>
47
+ </Card>
48
+ </div>
49
+ );
50
+ }
51
+ ```
52
+ <!-- /example -->
@@ -0,0 +1,98 @@
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.
42
+
43
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
44
+ ## Example
45
+
46
+ `ChartBrush` as first used in its preview:
47
+
48
+ ```tsx
49
+ import { ChartBrush } from 'altitude-design';
50
+
51
+ <ChartBrush />
52
+ ```
53
+
54
+ The full preview, as rendered in Storybook and checked in CI (`cb`, `col`, `pick` are the preview page's own layout classes, not Altitude's):
55
+
56
+ ```tsx
57
+ import * as React from 'react';
58
+ import { AreaChart, BarChart, Card, CardContent, CardDescription, CardHeader, CardTitle, ChartBrush, ChartContainer, DateRangePicker, Field } from 'altitude-design';
59
+
60
+ // A year of daily active users, with a dip in August and a launch in late September.
61
+ const cbYear = Array.from({ length: 365 }, (_, i) => {
62
+ const day = new Date(Date.UTC(2026, 0, 1 + i)).toISOString().slice(0, 10);
63
+ const trend = 4200 + i * 9, week = [0.82, 1, 1.04, 1.05, 1.03, 0.97, 0.78][i % 7];
64
+ const summer = i > 212 && i < 243 ? 0.8 : 1, launch = i > 265 ? 1.18 : 1;
65
+ return { day, active: Math.round((trend + 260 * Math.sin(i / 9)) * week * summer * launch) };
66
+ });
67
+ const cbWeeks = Array.from({ length: 52 }, (_, i) => ({ week: new Date(Date.UTC(2026, 0, 5 + i * 7)).toISOString().slice(0, 10), signups: Math.round(310 + i * 6 + 90 * Math.sin(i / 3) + (i > 37 ? 160 : 0)) }));
68
+ const cbK = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 });
69
+ export default function App() {
70
+ const [range, setRange] = React.useState({ start: '2026-03-01', end: '2026-04-30' });
71
+ return (
72
+ <div className="cb">
73
+ <Card>
74
+ <CardHeader><CardTitle id="cb1">Daily active users</CardTitle><CardDescription>Drag the lens to pan, a grip to resize, or press the overview to centre it there. The picker is the same state, typed.</CardDescription></CardHeader>
75
+ <CardContent className="col">
76
+ <Field label="Window" className="pick"><DateRangePicker value={range} onValueChange={setRange} min="2026-01-01" max="2026-12-31" locale="en-GB" /></Field>
77
+ <ChartContainer aria-labelledby="cb1" config={{ active: { label: 'Active users', format: cbK.format } }} aspect={3.4} locale="en-GB">
78
+ <AreaChart data={cbYear} x="day" window={range} onWindowChange={setRange}>
79
+ <ChartBrush />
80
+ </AreaChart>
81
+ </ChartContainer>
82
+ </CardContent>
83
+ </Card>
84
+ <Card>
85
+ <CardHeader><CardTitle id="cb2">Weekly sign-ups</CardTitle><CardDescription>Bars over time with a brush that keeps its own window</CardDescription></CardHeader>
86
+ <CardContent>
87
+ <ChartContainer aria-labelledby="cb2" config={{ signups: { label: 'Sign-ups' } }} aspect={4} locale="en-GB">
88
+ <BarChart data={cbWeeks} x="week">
89
+ <ChartBrush />
90
+ </BarChart>
91
+ </ChartContainer>
92
+ </CardContent>
93
+ </Card>
94
+ </div>
95
+ );
96
+ }
97
+ ```
98
+ <!-- /example -->
@@ -0,0 +1,229 @@
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).
153
+
154
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
155
+ ## Example
156
+
157
+ `ChartContainer` as first used in its preview:
158
+
159
+ ```tsx
160
+ import { ChartContainer } from 'altitude-design';
161
+
162
+ <ChartContainer aria-labelledby="rc" data={months} x="month" aspect={1.7} summary="Revenue rose 44% while costs held flat."
163
+ config={{ revenue: { label: 'Revenue', format: usd.format }, costs: { label: 'Costs', format: usd.format } }}>
164
+ <Lines />
165
+ </ChartContainer>
166
+ ```
167
+
168
+ The full preview, as rendered in Storybook and checked in CI (`charts` is the preview page's own layout class, not Altitude's):
169
+
170
+ ```tsx
171
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle, ChartContainer, ChartMarker, useChart } from 'altitude-design';
172
+
173
+ // Until the chart types land, these marks are drawn by hand with useChart(): the same scales the types will use.
174
+ const usd = new Intl.NumberFormat('en', { style: 'currency', currency: 'USD', notation: 'compact' });
175
+ const months = [
176
+ { month: '2026-01-01', revenue: 8200, costs: 5100 }, { month: '2026-02-01', revenue: 8900, costs: 5000 },
177
+ { month: '2026-03-01', revenue: 9400, costs: 5300 }, { month: '2026-04-01', revenue: 9100, costs: 5200 },
178
+ { month: '2026-05-01', revenue: 10600, costs: 5400 }, { month: '2026-06-01', revenue: 11800, costs: 5300 },
179
+ ];
180
+ const regions = [
181
+ { region: 'North', q2: 42, q3: 51 }, { region: 'South', q2: 35, q3: 31 }, { region: 'East', q2: 28, q3: 37 }, { region: 'West', q2: 19, q3: 24 },
182
+ ];
183
+ // encode() draws a series by its slot: colour, and a dash, marker or hatch whenever patterns are on (forced colours, print).
184
+ function Lines() {
185
+ const { series, data, xOf, y, encode, patterns, strokeWidth } = useChart();
186
+ return series.map((s) => {
187
+ const e = encode(s.key);
188
+ return <g key={s.key}>
189
+ <polyline fill="none" stroke={e.stroke} strokeDasharray={e.dash} strokeWidth={strokeWidth} strokeLinejoin="round" strokeLinecap="round"
190
+ points={data.map((d) => `${xOf(d)},${y(d[s.key])}`).join(' ')} />
191
+ {patterns && data.map((d, i) => <ChartMarker key={i} shape={e.marker} x={xOf(d)} y={y(d[s.key])} color={e.stroke} />)}
192
+ </g>;
193
+ });
194
+ }
195
+ function Bars() {
196
+ const { series, data, x, y, xKey, ground, alpha, encode, gap } = useChart();
197
+ const w = (x.bandwidth - gap * (series.length - 1)) / series.length;
198
+ return data.map((d) => series.map((s, i) => {
199
+ const v = d[s.key], top = Math.min(y(v), ground), e = encode(s.key);
200
+ return <rect key={d[xKey] + s.key} x={x(d[xKey]) + i * (w + gap)} y={top} width={w} height={Math.abs(y(v) - ground)}
201
+ fill={e.fill} stroke={e.edge} strokeWidth={e.edge ? 1 : undefined} opacity={alpha(d, s.key)} />;
202
+ }));
203
+ }
204
+ export default function App() {
205
+ return (
206
+ <div className="charts">
207
+ <Card>
208
+ <CardHeader><CardTitle id="rc">Revenue and costs</CardTitle><CardDescription>Jan to Jun 2026</CardDescription></CardHeader>
209
+ <CardContent>
210
+ <ChartContainer aria-labelledby="rc" data={months} x="month" aspect={1.7} summary="Revenue rose 44% while costs held flat."
211
+ config={{ revenue: { label: 'Revenue', format: usd.format }, costs: { label: 'Costs', format: usd.format } }}>
212
+ <Lines />
213
+ </ChartContainer>
214
+ </CardContent>
215
+ </Card>
216
+ <Card>
217
+ <CardHeader><CardTitle id="dr">Deals closed by region</CardTitle><CardDescription>Q2 and Q3</CardDescription></CardHeader>
218
+ <CardContent>
219
+ <ChartContainer aria-labelledby="dr" data={regions} x="region" aspect={1.7}
220
+ config={{ q2: { label: 'Q2' }, q3: { label: 'Q3' } }}>
221
+ <Bars />
222
+ </ChartContainer>
223
+ </CardContent>
224
+ </Card>
225
+ </div>
226
+ );
227
+ }
228
+ ```
229
+ <!-- /example -->
@@ -0,0 +1,78 @@
1
+ # ChartLegend
2
+
3
+ Names each series, and optionally lets people hide and show series. Every chart with two or more series needs one, unless its marks are labelled directly (a `LineChart` with up to four series names each line at its end). A single series doesn't need one: the title names it.
4
+
5
+ **Why it's flat, or why it lifts.** A legend that only labels is settled: keys flush on the ground, each a swatch in its series colour (a block for bars, a short line for lines, the group's marker for a `ScatterChart`) and its name in `ink`. A legend you can toggle is a row of choices, and in most chart libraries those are invisible affordances: the key looks like a label and behaves like a button, which is the flat-UI failure Burmistrov et al. (2015) measured. So with `toggle` each key is built like a `Checkbox`. It's a ghost button that lifts one level when pointed at, because clicking it does something. It holds a trench (sunk, `surface-trench`) that the series colour pours into while the series is shown and drains out of when it's hidden. The fill is the series colour rather than `accent`, because the swatch *is* the key.
6
+
7
+ **What toggling does.** The series fades out of (or into) the plot over `dur-travel-1`. If y has to rescale, that's staged first, as every change to a chart is ([Motion](../../visual/Motion.md), "Charts"). Hidden series keep their colour slots, so nothing else repaints. The last shown series can't be hidden; its key says "At least one series stays shown."
8
+
9
+ **Without height.** Under forced colours, swatches take each series' pattern, hidden swatches are dashed, and a hovered key gets a 1px border. With `contrast="more"` the trench gets a solid inner top edge.
10
+
11
+ **Accessibility.** A static legend is a list named "Series". A toggle legend is an APG toolbar named "Series" holding `aria-pressed` buttons, one per series, so a long legend is one Tab stop: ←/→ (or ↑/↓) move between keys, Home and End go to the ends, and Space or Enter toggles. Toggling is announced politely ("Costs hidden. 2 of 3 series shown.").
12
+
13
+ **Props.** `ChartLegend` takes `content`: a `ChartLegendContent` with these options:
14
+
15
+ - `toggle`: make the keys switches;
16
+ - `hidden` and `onHiddenChange`, controlled; or `defaultHidden`, uncontrolled;
17
+ - `label`: the toolbar's name, default "Series".
18
+
19
+ **You provide:** a `ChartLegend` inside the chart type or the container.
20
+
21
+ ```tsx
22
+ <ChartContainer config={config}>
23
+ <AreaChart data={usage} x="day" stack>
24
+ <ChartLegend content={<ChartLegendContent toggle />} />
25
+ </AreaChart>
26
+ </ChartContainer>
27
+
28
+ // controlled
29
+ <ChartLegendContent toggle hidden={hidden} onHiddenChange={setHidden} />
30
+ ```
31
+
32
+ - Do label lines directly when there are four or fewer, and keep the legend for toggling.
33
+ - Do use `toggle` when people need to compare a few series out of many; otherwise a static legend is quieter.
34
+ - Don't let colour be the only link between a key and its series: under forced colours the keys show each series' pattern.
35
+ - Don't put the legend somewhere the chart's readout or summary can't be read alongside it.
36
+
37
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
38
+ ## Example
39
+
40
+ `ChartLegend` as first used in its preview:
41
+
42
+ ```tsx
43
+ import { ChartLegend, ChartLegendContent } from 'altitude-design';
44
+
45
+ <ChartLegend content={<ChartLegendContent />} />
46
+ ```
47
+
48
+ The full preview, as rendered in Storybook and checked in CI (`cl` is the preview page's own layout class, not Altitude's):
49
+
50
+ ```tsx
51
+ import { BarChart, Card, CardContent, CardDescription, CardHeader, CardTitle, ChartContainer, ChartLegend, ChartLegendContent, LineChart } from 'altitude-design';
52
+
53
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'].map((month, i) => ({ month, api: 20 + ((i * 7) % 11), web: 14 + ((i * 5) % 9), jobs: 6 + ((i * 3) % 7) }));
54
+ const plans = ['2026-01-01', '2026-02-01', '2026-03-01', '2026-04-01', '2026-05-01', '2026-06-01'].map((month, i) => ({ month, free: 40 + i * 2, team: 34 + i * 3, business: 30 + i * 2.6, enterprise: 12 + i }));
55
+ export default function App() {
56
+ return (
57
+ <div className="cl">
58
+ <Card>
59
+ <CardHeader><CardTitle id="g1">Requests, all services</CardTitle><CardDescription>A legend that only labels</CardDescription></CardHeader>
60
+ <CardContent>
61
+ <ChartContainer aria-labelledby="g1" aspect={1.8} config={{ api: { label: 'API' }, web: { label: 'Web' }, jobs: { label: 'Jobs' } }}>
62
+ <BarChart data={months} x="month" stack><ChartLegend content={<ChartLegendContent />} /></BarChart>
63
+ </ChartContainer>
64
+ </CardContent>
65
+ </Card>
66
+ <Card>
67
+ <CardHeader><CardTitle id="g2">Seats by plan</CardTitle><CardDescription>toggle: each key is a switch; Enterprise starts hidden</CardDescription></CardHeader>
68
+ <CardContent>
69
+ <ChartContainer aria-labelledby="g2" aspect={1.8} config={{ free: { label: 'Free' }, team: { label: 'Team' }, business: { label: 'Business' }, enterprise: { label: 'Enterprise' } }}>
70
+ <LineChart data={plans} x="month"><ChartLegend content={<ChartLegendContent toggle defaultHidden={['enterprise']} />} /></LineChart>
71
+ </ChartContainer>
72
+ </CardContent>
73
+ </Card>
74
+ </div>
75
+ );
76
+ }
77
+ ```
78
+ <!-- /example -->