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,193 @@
1
+ # ItemGroup
2
+
3
+ A list of records shown as slats (media, title, description, actions), changed in the list itself: edit a slat in place, add one at the end, reorder by picking one up, remove one with real recovery. Settings lists (API keys, webhooks, team members), option lists (a form builder's choices) and “add another” sections. It's the Files recipe from [Actions in place](../../visual/ActionsInPlace.md) as a component; the editor is [EditRegion](../EditRegion/README.md).
4
+
5
+ ```tsx
6
+ <ItemGroup<Webhook>
7
+ aria-label="Webhooks"
8
+ items={webhooks}
9
+ getKey={(w) => w.id}
10
+ getLabel={(w) => w.name}
11
+ renderItem={(w, { editProps, removeProps }) => (
12
+ <Item>
13
+ <ItemMedia><WebhookIcon /></ItemMedia>
14
+ <ItemContent><ItemTitle>{w.name}</ItemTitle><ItemDescription>{w.url}</ItemDescription></ItemContent>
15
+ <ItemActions>
16
+ <Button size="sm" variant="ghost" {...editProps}>Edit</Button>
17
+ <Button size="sm" variant="ghost" {...removeProps}>Remove</Button>
18
+ </ItemActions>
19
+ </Item>
20
+ )}
21
+ renderEditor={(field) => <>
22
+ <Field label="Name" error={field.error('name')}><Input {...field('name')} /></Field>
23
+ <Field label="Endpoint URL" error={field.error('url')}><Input type="url" {...field('url')} /></Field>
24
+ </>}
25
+ onSave={(draft, { previousValue }) => api.updateWebhook(draft, previousValue.revision)} // EditRegion's contract
26
+ onItemChange={(w) => setWebhooks((list) => list.map((x) => (x.id === w.id ? w : x)))}
27
+ newItem={() => ({ id: '', name: '', url: 'https://' })}
28
+ onCreate={(draft) => api.createWebhook(draft)} // { ok: true, value } after items includes it
29
+ addLabel="Add webhook"
30
+ onReorder={(next) => api.reorder(next.map((w) => w.id))} // shows grips
31
+ onRemove={(w) => api.archiveWebhook(w.id)}
32
+ removal={{ kind: 'recoverable', undo: (w) => api.restoreWebhook(w.id) }}
33
+ empty={<EmptyState size="sm" title="No webhooks yet" />}
34
+ />
35
+ ```
36
+
37
+ `removal` has no default, on purpose: say whether removal is recoverable (it happens at once, and Undo calls your real inverse operation) or needs confirmation (`{ kind: 'confirm', title, description?, confirmLabel? }`: a stationary `Dialog` naming the record, Cancel focused). A danger button doesn't imply irreversibility.
38
+
39
+ A recoverable removal leaves its result where the slat was, as a [RemovalReceipt](../RemovalReceipt/README.md): “Webhook is removed.” with Undo, at the slat's height, so nothing moves while it's read. After the hold the slot closes, and Undo moves to a toast that the group raises itself. Render a `Toaster` once in your app for it to appear. The toast is a supplement to Undo, not the only route back, so keep a durable recovery route of your own (Archived → Restore).
40
+
41
+ ## Elevation
42
+
43
+ - **Slats** are the record: flush on the ground with `line` dividers, as Table rows, in a plain `ul`. Actions are visible ghost buttons, not hover-only.
44
+ - **Editing**: one slat at a time lifts into its EditRegion, a static raised card holding the fields; its neighbours stay flush and readable. Opening another while one is open keeps the first and moves focus back to it.
45
+ - **The add slat** is an opening: a dashed well (an `EmptyState`-style button) that deepens to a trench under the pointer or focus. Activating it rises into an empty editor in the same place; once added, the new slat settles with an afterglow and a fresh add slat is ready below.
46
+ - **Reordering**: a slat picked up by its grip floats at level 3, tilted, shown where it will land; the slot it leaves opens as a well.
47
+ - **Pending** removals and reorders hover at level 2 after `dur-grace`; known failures are a critical Alert, and the list is unchanged.
48
+ - **Removed** (recoverable): once `onRemove` resolves, the slat's contents cross-fade (`dur-travel-2`) to its receipt in the same slot: a check, “<label> is removed.” in `ink-muted` on the ground, and Undo.
49
+ - **The hold.** The receipt holds for `dur-linger`, pausing while it's pointed at or focused and waiting while a mouse is still over the list. Then it lifts off and the gap glides shut (`dur-travel-3`, `ease-glide`). Removals due within 250ms of each other close together.
50
+ - **Undo during the hold.** The slat cross-fades back and settles with an afterglow; nothing moves.
51
+ - **Undo after the close** is in a toast (“<label> is removed.”, Undo). That Undo glides the slot open where it was, then the slat settles. The toast is withdrawn if the item comes back another way.
52
+ - **Removed** (confirmed): the slat glides shut where it was instead of vanishing. There's no receipt, because there's nothing to undo.
53
+
54
+ Forced colours: the add slat and slot dashed, the carried slat 2px, pending dotted, a receipt outlined in `CanvasText`. `contrast="more"`: the carried slat gets a 3px slab.
55
+
56
+ ## Keyboard and focus
57
+
58
+ Tab reaches each slat's controls in order. Edit opens the editor and focuses its first field; Enter in a single-line field saves and Escape cancels (EditRegion's rules), and focus returns to that slat's Edit button. Add focuses the new editor; after it's added, focus goes to the fresh add slat, so several can be added in a row. Remove moves focus to the next slat's Remove, else the previous one's, else the add slat or the list. A receipt's Undo stays in the tab order while it's held (Shift+Tab reaches it), and the hold doesn't expire while focus is in it. After Undo, focus goes to the restored slat's Remove. Grips follow SortableList: Space picks up, ↑/↓ move, Space drops, Escape cancels, with positions announced assertively (“Position 2 of 5”). One polite status announces saved, added, removed (confirmed), restored and errors. A recoverable removal is announced by its receipt (“<label> is removed. Undo is available.”), not by the status as well, and the toast after the close isn't announced again. Under reduced motion the receipt still fades and holds, and the gap closes at once.
59
+
60
+ ## Props
61
+
62
+ | Prop | |
63
+ |---|---|
64
+ | `items`, `getKey`, `getLabel`, `renderItem` | the slats; `renderItem` gets `edit`, `remove`, and `editProps`/`removeProps` to spread onto visible buttons (name, handler, focus repair) |
65
+ | `renderEditor`, `onSave`, `onItemChange` | the editor's fields; EditRegion's save contract; publish the canonical record |
66
+ | `newItem`, `onCreate`, `addLabel` | the add slat |
67
+ | `onReorder` | grips; resolve after `items` is in the new order |
68
+ | `onRemove`, `removal` | removal and its stated recovery policy |
69
+ | `empty`, `aria-label`, `aria-labelledby` | |
70
+
71
+ Parts: `Item`, `ItemMedia` (`variant` icon, image or avatar), `ItemContent`, `ItemTitle`, `ItemDescription`, `ItemActions`, `ItemSeparator`.
72
+
73
+ ## You provide
74
+
75
+ The records, the writes and their concurrency guards, and the recovery policy and its inverse operation. Pointer reordering, multi-select and bulk actions aren't covered yet.
76
+
77
+ ## Do and don't
78
+
79
+ - **Do** state the removal policy honestly; offer Undo only when you can really reverse it.
80
+ - **Do** keep the editor short; past about five fields, open the record in a `Sheet`.
81
+ - **Don't** hide Edit and Remove behind hover or a “…” menu when there are only one or two.
82
+
83
+ ## Demo and verification
84
+
85
+ The `ItemGroup` section of `build/previews.txt` shows webhooks (edit, add, reorder, recoverable removal) and a form builder's answer options (empty state, confirmed removal). `node checks/item-group.mjs` (from `build/`; `ALT_FIXTURE_ROOT` selects React 19 fixtures) covers editing and focus return, one editor at a time, adding with field errors and several in a row, keyboard reordering with cancel, confirmed removal with Cancel focused, the empty state, and removal. For removal it checks:
86
+ - the receipt in place with focus repair and a single announcement, and Undo during the hold, with every frame sampled to show nothing below moves;
87
+ - the hold of `dur-linger`, then the gap gliding shut;
88
+ - the toast's unannounced Undo gliding the slot back open;
89
+ - the hold pausing on focus, several removals closing together, reduced motion, and a confirmed slat gliding shut.
90
+
91
+ It also renders and 320/960px layouts in both schemes under reduced motion, forced colours and `contrast="more"`.
92
+ **Narrow.** When the group is under 480px wide, `ItemActions` moves to its own row under the content, so the title keeps its width; `narrow="inline"` keeps the actions beside it. `ItemDescription truncate` (or a number of lines) clamps a long description.
93
+
94
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
95
+ ## Example
96
+
97
+ `ItemGroup` as first used in its preview:
98
+
99
+ ```tsx
100
+ import { Button, Field, Input, Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle } from 'altitude-design';
101
+
102
+ <ItemGroup aria-labelledby="ig-hooks" items={hooks} getKey={h => h.id} getLabel={h => h.name}
103
+ renderItem={(h, { editProps, removeProps }) => <Item>
104
+ <ItemMedia><HookIcon /></ItemMedia>
105
+ <ItemContent><ItemTitle>{h.name}</ItemTitle><ItemDescription className="ig-url">{h.url}</ItemDescription></ItemContent>
106
+ <ItemActions><Button size="sm" variant="ghost" {...editProps}>Edit</Button><Button size="sm" variant="ghost" {...removeProps}>Remove</Button></ItemActions>
107
+ </Item>}
108
+ renderEditor={field => <>
109
+ <Field label="Name" required error={field.error('name')}><Input {...field('name')} /></Field>
110
+ <Field label="Endpoint URL" required error={field.error('url')}><Input {...field('url')} type="url" /></Field>
111
+ </>}
112
+ onSave={async (draft, { previousValue }) => {
113
+ const errors = validate(draft); if (Object.keys(errors).length) return { ok: false, error: 'Check the highlighted fields.', fieldErrors: errors };
114
+ await igWait(300); return { ok: true, value: { ...draft, name: draft.name.trim(), revision: previousValue.revision + 1 } };
115
+ }}
116
+ onItemChange={h => setHooks(list => list.map(x => x.id === h.id ? h : x))}
117
+ newItem={() => ({ id: '', name: '', url: 'https://', revision: 0 })}
118
+ onCreate={async draft => {
119
+ const errors = validate(draft); if (Object.keys(errors).length) return { ok: false, error: 'Check the highlighted fields.', fieldErrors: errors };
120
+ await igWait(300); const hook = { ...draft, id: 'n' + ++seq.current, name: draft.name.trim(), revision: 1 }; setHooks(list => [...list, hook]); return { ok: true, value: hook };
121
+ }}
122
+ addLabel="Add webhook"
123
+ onReorder={async next => { await igWait(200); setHooks(next); }}
124
+ onRemove={async h => { await igWait(200); trash.current.set(h.id, hooks.indexOf(h)); setHooks(list => list.filter(x => x.id !== h.id)); }}
125
+ removal={{ kind: 'recoverable', undo: h => setHooks(list => { const next = [...list]; next.splice(trash.current.get(h.id) ?? next.length, 0, h); return next; }) }} />
126
+ ```
127
+
128
+ The full preview, as rendered in Storybook and checked in CI (`ig-demo`, `ig-url` are the preview page's own layout classes, not Altitude's):
129
+
130
+ ```tsx
131
+ import * as React from 'react';
132
+ import { Button, EmptyState, Field, Input, Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle, Toaster } from 'altitude-design';
133
+
134
+ const IG_HOOKS = [
135
+ { id: 'h1', name: 'Production', url: 'https://api.northwind.example/hooks/prod', revision: 1 },
136
+ { id: 'h2', name: 'Staging', url: 'https://staging.northwind.example/hooks', revision: 1 },
137
+ { id: 'h3', name: 'Billing events', url: 'https://billing.northwind.example/in', revision: 1 },
138
+ ];
139
+ const igWait = ms => new Promise(resolve => setTimeout(resolve, ms));
140
+ const HookIcon = () => <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><path d="M6 4.5a2.5 2.5 0 1 1 3 2.4L7 11" /><path d="M4 12a2.5 2.5 0 1 1 2-4" /><path d="M12 12.5H8" /><circle cx="12" cy="12.5" r="1.5" /></svg>;
141
+ export default function App() {
142
+ const [hooks, setHooks] = React.useState(IG_HOOKS);
143
+ const [options, setOptions] = React.useState([]);
144
+ const trash = React.useRef(new Map());
145
+ const seq = React.useRef(0);
146
+ const validate = draft => ({ ...(draft.name.trim() ? {} : { name: 'Enter a name.' }), ...(/^https:\/\/[^\s/]+\.[^\s]+$/.test(draft.url) ? {} : { url: 'Use an https:// address.' }) });
147
+ return <div className="ig-demo">
148
+ <section>
149
+ <h3 id="ig-hooks">Webhooks</h3>
150
+ <ItemGroup aria-labelledby="ig-hooks" items={hooks} getKey={h => h.id} getLabel={h => h.name}
151
+ renderItem={(h, { editProps, removeProps }) => <Item>
152
+ <ItemMedia><HookIcon /></ItemMedia>
153
+ <ItemContent><ItemTitle>{h.name}</ItemTitle><ItemDescription className="ig-url">{h.url}</ItemDescription></ItemContent>
154
+ <ItemActions><Button size="sm" variant="ghost" {...editProps}>Edit</Button><Button size="sm" variant="ghost" {...removeProps}>Remove</Button></ItemActions>
155
+ </Item>}
156
+ renderEditor={field => <>
157
+ <Field label="Name" required error={field.error('name')}><Input {...field('name')} /></Field>
158
+ <Field label="Endpoint URL" required error={field.error('url')}><Input {...field('url')} type="url" /></Field>
159
+ </>}
160
+ onSave={async (draft, { previousValue }) => {
161
+ const errors = validate(draft); if (Object.keys(errors).length) return { ok: false, error: 'Check the highlighted fields.', fieldErrors: errors };
162
+ await igWait(300); return { ok: true, value: { ...draft, name: draft.name.trim(), revision: previousValue.revision + 1 } };
163
+ }}
164
+ onItemChange={h => setHooks(list => list.map(x => x.id === h.id ? h : x))}
165
+ newItem={() => ({ id: '', name: '', url: 'https://', revision: 0 })}
166
+ onCreate={async draft => {
167
+ const errors = validate(draft); if (Object.keys(errors).length) return { ok: false, error: 'Check the highlighted fields.', fieldErrors: errors };
168
+ await igWait(300); const hook = { ...draft, id: 'n' + ++seq.current, name: draft.name.trim(), revision: 1 }; setHooks(list => [...list, hook]); return { ok: true, value: hook };
169
+ }}
170
+ addLabel="Add webhook"
171
+ onReorder={async next => { await igWait(200); setHooks(next); }}
172
+ onRemove={async h => { await igWait(200); trash.current.set(h.id, hooks.indexOf(h)); setHooks(list => list.filter(x => x.id !== h.id)); }}
173
+ removal={{ kind: 'recoverable', undo: h => setHooks(list => { const next = [...list]; next.splice(trash.current.get(h.id) ?? next.length, 0, h); return next; }) }} />
174
+ <Toaster />
175
+ </section>
176
+ <section>
177
+ <h3 id="ig-options">Answer options</h3>
178
+ <ItemGroup aria-labelledby="ig-options" items={options} getKey={o => o.id} getLabel={o => o.label}
179
+ renderItem={(o, { editProps, removeProps }) => <Item><ItemContent><ItemTitle>{o.label}</ItemTitle></ItemContent>
180
+ <ItemActions><Button size="sm" variant="ghost" {...editProps}>Edit</Button><Button size="sm" variant="ghost" {...removeProps}>Delete</Button></ItemActions></Item>}
181
+ renderEditor={field => <Field label="Option" required error={field.error('label')}><Input {...field('label')} /></Field>}
182
+ onItemChange={o => setOptions(list => list.map(x => x.id === o.id ? o : x))}
183
+ newItem={() => ({ id: '', label: '' })}
184
+ onCreate={draft => { if (!draft.label.trim()) return { ok: false, error: 'Enter an option.', fieldErrors: { label: 'Enter an option.' } }; const o = { id: 'o' + ++seq.current, label: draft.label.trim() }; setOptions(list => [...list, o]); return { ok: true, value: o }; }}
185
+ addLabel="Add option" onReorder={setOptions}
186
+ onRemove={o => setOptions(list => list.filter(x => x.id !== o.id))}
187
+ removal={{ kind: 'confirm', title: o => `Delete “${o.label}” permanently?`, description: () => 'Responses that chose it keep their answer, but it can’t be chosen again.', confirmLabel: 'Delete option' }}
188
+ empty={<EmptyState size="sm" title="No options yet" description="Add the answers people can choose from." />} />
189
+ </section>
190
+ </div>;
191
+ }
192
+ ```
193
+ <!-- /example -->
@@ -0,0 +1,36 @@
1
+ # Kbd
2
+
3
+ Shows a keyboard key as a keycap: a tiny slab with a 2px wall that presses down when the person presses that key.
4
+
5
+ Keys sit at level 1 in the mono family with `wall-1` under them. With `listen`, the cap drops to the ground (wall gone) while the matching key (a `KeyboardEvent.key`, case-insensitive) is held: a small confirmation that a shortcut registered.
6
+
7
+ **On touch.** `hideOnTouch` hides a key cap on a touch-only device (no hover, a coarse pointer) until a key is pressed in the session, for a hint that only makes sense with a keyboard. It's off by default, because an app may be documenting its shortcuts on purpose; Altitude's own hints (menu shortcuts, CommandPalette's keys, InlineEdit's save hint) already do this. `useKeyboardLikely()` gives the same answer to your own components.
8
+
9
+ **You provide:** the key's glyph as children (one `Kbd` per key: `<Kbd>⌘</Kbd><Kbd>K</Kbd>`), and `listen` when the shortcut is live on this page.
10
+
11
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
12
+ ## Example
13
+
14
+ `Kbd` as first used in its preview:
15
+
16
+ ```tsx
17
+ import { Kbd } from 'altitude-design';
18
+
19
+ <Kbd listen="Meta">⌘</Kbd>
20
+ ```
21
+
22
+ The full preview, as rendered in Storybook and checked in CI (`col`, `row`, `body`, `cap` are the preview page's own layout classes, not Altitude's):
23
+
24
+ ```tsx
25
+ import { Kbd } from 'altitude-design';
26
+
27
+ export default function App() {
28
+ return (
29
+ <div className="col">
30
+ <div className="row"><span className="body">Open the command menu</span><span><Kbd listen="Meta">⌘</Kbd><Kbd listen="k">K</Kbd></span></div>
31
+ <p className="cap">Click into this card and press ⌘K, or hold K.</p>
32
+ </div>
33
+ );
34
+ }
35
+ ```
36
+ <!-- /example -->
@@ -0,0 +1,109 @@
1
+ # LineChart
2
+
3
+ Change over time: latency over a day, revenue over a year, one series or a few compared. It renders inside a [`ChartContainer`](../ChartContainer/README.md), which places x by time (ISO dates, timestamps) or by number.
4
+
5
+ **Why it's flat.** Lines are the record, so they lie on the plot: 2px strokes in their series' colours, round joins and caps, no fill, no glow, no shadow. A missing value (`null`) is a gap in the line, never bridged: nothing was recorded there, as `Await` tells `null` from not-yet. `curve="monotone"` curves through the points without overshooting them (Fritsch–Carlson), so a curve never invents a peak or a dip the data doesn't have. The last point wears an 8px marker, and each line is named at its end in `ink` (up to four series), so the lines are their own legend; end labels are nudged at least 16px apart where lines converge.
6
+
7
+ **The axis.** Unlike bars, a line's y axis doesn't have to include zero, but its range is always labelled. When zero isn't on it, the plot's foot is drawn as an ordinary gridline, not the ground line, so nothing reads as a baseline that isn't one.
8
+
9
+ **Without colour.** Under forced colours and in print, and with `patterns` on the container, each line takes its slot's dash and every point its slot's marker (circle, square, triangle, diamond, ring, cross), in `CanvasText` when forced. With `contrast="more"` lines are 3px.
10
+
11
+ **Motion.** When the data changes and the x values are the same, the line morphs on `ease-glide`; when they differ (a new range), the old chart and the new one cross-fade. A rescale comes first. There's no draw-on animation on first render ([Motion](../../visual/Motion.md), "Charts").
12
+
13
+ **Accessibility.** Without a written `summary`, the generated one says which way each series went between its first and last values ("p95 rises from 168 ms at 14 Mar, 12:00 AM to 207 ms…"), and its lowest and highest points. The keyboard path along x and between series is #63, the readout #52, and the table view #64.
14
+
15
+ **Props:** `data` and `x`; `curve` (`'linear'` or `'monotone'`); `y={{ format }}` to format the axis; `endLabels` to turn end labels on or off (default on for up to four series, and always under forced colours and in print). `status` and `boundary` (below); `band={{ low, high, series?, label? }}` for an uncertainty band. `linePath(points, curve)` is exported for your own marks: it draws the same path, gaps and all.
16
+
17
+ **Selecting and drilling.** `selected` and `onSelect` make the marks a selection that cross-filters the rest of a dashboard, and `drill` opens a level down inside a `Drilldown`. Hover only promises what a click does. See [ChartContainer](../ChartContainer/README.md), "Marks you can select or drill into".
18
+
19
+ **Filtering in place.** `relevance={(d) => 'match' | 'miss' | undefined}` recedes misses to 35% and keeps everything in place; `rescale` fits the axis to the matches. See [ChartContainer](../ChartContainer/README.md), "Filtering in place".
20
+
21
+ **Not yet the record.** `status={(d) => d.day > today ? 'projected' : undefined}` dashes the line (5 4) from the last settled point on, and draws those points hollow: ringed in the series colour on the plot's own. It's one path clipped two ways, so a curve doesn't kink where the record ends. A 1px `line-control` rule named "Today" marks the boundary. Point names, the readout's heading and the table say "Projected" or "So far", and the generated summary reads its trend from the settled points only, then says what's projected. While patterns are on, a line that has its own dash keeps it, and the hollow points carry the difference.
22
+
23
+ **A sliding window.** `window={{ minutes: 15 }}` (or `hours`, `days`, or `points` for the last N records) shows only the latest stretch, and the axis spans exactly that window, however sparse the data. In a `live` container, when the window moves on, x glides one step and the line rides along with it.
24
+
25
+ **A chosen window.** `window={{ start, end }}` shows a fixed stretch of x, both ends included; a `DateRange` fits as it is. Put a `ChartBrush` inside the chart to pick it on an overview, with `onWindowChange` to hear where it moved.
26
+
27
+ **Uncertainty is a band.** `band={{ low: 'p10', high: 'p90', label: 'Range 10th–90th percentile' }}` draws a flat fill of the series colour at 16% between the two keys, under the lines. It's never error bars, which readers misjudge (Correll & Gleicher 2014), and never a gradient. Its edges count toward the y axis. The readout adds a row for it ("499–787 Range 10th–90th percentile"), and so do point names and the table.
28
+
29
+ **Banked to 45°.** `aspect="banked"` lets the data choose the chart's height: slopes read best when the segments average about 45° (Cleveland's banking), so the plot takes the width ÷ height that sets their mean absolute orientation there, held between 1 and 4 for the data area so a card is never a sliver or a tower. Steep, jagged data banks wide; a gentle trend banks tall. The width stays the card's. It's read from every shown series, and kept while values glide; only new x values, which cross-fade as a new chart, bank it afresh, and the height change itself never animates. Put it on the `LineChart` or on the container. In a grid of equal cards, keep a fixed `aspect`: banked neighbours would differ in height.
30
+
31
+ **Focus.** `focus={['team']}` keeps those lines' colours and draws the rest in `data-context`, their end labels in `ink-muted`; the readout and the keyboard path list the focused lines first. See [ChartContainer](../ChartContainer/README.md), "Focus".
32
+
33
+ **Against the record before.** `compare={{ data: lastWeek, label: 'Last week' }}` matches records by position (or `key` reads a field on each record) and draws the earlier period as a 1.5px solid `ink-muted` trace under the compared series (`series`, default the first), without points, named at its end in `ink-muted`. It's solid because dashes mean projected, and it follows the data, which a 1px straight reference line never does. The readout, point names, the table and the summary say the difference.
34
+
35
+ **You provide:** a `ChartContainer` with a `config` naming each series, the `data` in x order, and usually a `summary`.
36
+
37
+ ```tsx
38
+ <ChartContainer config={{ p50: { label: 'p50' }, p95: { label: 'p95' } }} aspect={2.5}>
39
+ <LineChart data={latency} x="minute" curve="monotone" y={{ format: (ms) => `${ms} ms` }} />
40
+ </ChartContainer>
41
+ ```
42
+
43
+ - Do keep it to four series or fewer; past that, use small multiples, or a legend and the readout.
44
+ - Do leave missing values as `null`; don't fill them with zero or the last value.
45
+ - Do use `monotone` for smooth measures, and `linear` when each point is a discrete count.
46
+ - Don't fill the area under a line to make it look richer; that's `AreaChart`, for a total and its parts.
47
+ - Don't put two measures of different scale on one plot; that's two charts.
48
+
49
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
50
+ ## Example
51
+
52
+ `LineChart` as first used in its preview:
53
+
54
+ ```tsx
55
+ import { LineChart } from 'altitude-design';
56
+
57
+ <LineChart data={latency} x="t" y={{ format: (ms) => `${ms} ms` }} />
58
+ ```
59
+
60
+ The full preview, as rendered in Storybook and checked in CI (`lc` is the preview page's own layout class, not Altitude's):
61
+
62
+ ```tsx
63
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle, ChartContainer, LineChart } from 'altitude-design';
64
+
65
+ const t0 = Date.UTC(2026, 2, 14, 0, 0);
66
+ const latency = Array.from({ length: 49 }, (_, i) => ({ t: t0 + i * 30 * 60e3, p50: 92 + Math.round(18 * Math.sin(i / 6) + (i % 5)), p95: 168 + Math.round(30 * Math.sin(i / 6 + 0.6) + (i % 7) * 3) }));
67
+ const revenue = ['2025-07-01', '2025-08-01', '2025-09-01', '2025-10-01', '2025-11-01', '2025-12-01', '2026-01-01', '2026-02-01', '2026-03-01', '2026-04-01', '2026-05-01', '2026-06-01']
68
+ .map((month, i) => ({ month, revenue: [61, 64, 63, 70, 74, 72, 79, 85, 83, 92, 98, 104][i] * 1000 }));
69
+ const signups = Array.from({ length: 14 }, (_, i) => ({ day: `2026-04-${String(i + 1).padStart(2, '0')}`, signups: [42, 48, 51, null, null, 55, 61, 58, 63, null, 66, 70, 68, 74][i] }));
70
+ const signupsBefore = [38, 41, 40, 44, 47, 45, 49, 52, 50, 53, 55, 54, 57, 58].map((signups) => ({ signups }));
71
+ 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 }));
72
+ const usd = new Intl.NumberFormat('en', { style: 'currency', currency: 'USD', notation: 'compact' });
73
+ function Chart({ id, title, sub, children, ...props }) {
74
+ // With a takeaway the figure names itself: the title becomes the label over the takeaway, the heading.
75
+ if (props.takeaway) return (
76
+ <Card>
77
+ <CardContent style={{ paddingTop: 'var(--space-5)' }}><ChartContainer title={title} description={sub} aspect={1.9} {...props}>{children}</ChartContainer></CardContent>
78
+ </Card>
79
+ );
80
+ return (
81
+ <Card>
82
+ <CardHeader><CardTitle id={id}>{title}</CardTitle><CardDescription>{sub}</CardDescription></CardHeader>
83
+ <CardContent><ChartContainer aria-labelledby={id} aspect={1.9} {...props}>{children}</ChartContainer></CardContent>
84
+ </Card>
85
+ );
86
+ }
87
+ export default function App() {
88
+ return (
89
+ <div className="lc">
90
+ <Chart id="l1" title="Response time" sub='14 Mar, every 30 minutes; yLabels="inline"' yLabels="inline" config={{ p50: { label: 'p50' }, p95: { label: 'p95' } }}>
91
+ <LineChart data={latency} x="t" y={{ format: (ms) => `${ms} ms` }} />
92
+ </Chart>
93
+ <Chart id="l2" title="Monthly revenue" takeaway="Revenue rose 70% over the year" sub='curve="monotone": no overshoot' config={{ revenue: { label: 'Revenue', format: usd.format } }} summary="Revenue rose 70% over the year, with small dips in September, December and March.">
94
+ <LineChart data={revenue} x="month" curve="monotone" />
95
+ </Chart>
96
+ <Chart id="l3" title="Daily signups" sub="Gaps stay gaps; read against the two weeks before" config={{ signups: { label: 'Signups' } }}>
97
+ <LineChart data={signups} x="day" compare={{ data: signupsBefore, label: '2 weeks before' }} />
98
+ </Chart>
99
+ <Chart id="l4" title="Seats by plan" sub="Four series, focus on Team: the rest in data-context" config={{ free: { label: 'Free' }, team: { label: 'Team' }, business: { label: 'Business' }, enterprise: { label: 'Enterprise' } }}>
100
+ <LineChart data={plans} x="month" focus={['team']} />
101
+ </Chart>
102
+ <Chart id="l5" title="Monthly revenue, banked" sub='aspect="banked": the segments average 45°' aspect="banked" config={{ revenue: { label: 'Revenue', format: usd.format } }}>
103
+ <LineChart data={revenue} x="month" curve="monotone" />
104
+ </Chart>
105
+ </div>
106
+ );
107
+ }
108
+ ```
109
+ <!-- /example -->
@@ -0,0 +1,69 @@
1
+ # Link
2
+
3
+ Navigation: a link in prose, "Learn more" after a hint, "View all invoices →" at the end of a card, an external link to docs.
4
+
5
+ **Elevation.** A link is text in the record, so it lies on the ground and **never lifts**. Lifting a word would break its line, and height means "press it", which is what a `Button` is for. It's `accent-ink` with an underline, so it's never told apart by colour alone. Hover thickens the underline from 1px to 2px over `dur-travel-1`. Focus is the 2px `accent-ink` ring at a 2px offset. A standalone link ("View all invoices →") is still ground.
6
+
7
+ **Link or button?** A link goes somewhere (it has an `href`). A button does something. Don't use a ghost `Button` for navigation: it has the wrong semantics, and it lifts on hover like a slab.
8
+
9
+ **External links.** `external` opens a new tab with `rel="noopener noreferrer"`, ends with an external-link icon, and adds "(opens in a new tab)" for screen readers.
10
+
11
+ **Router links.** `asChild` puts the styles and behaviour on your own element (a router's `Link`) instead of rendering an `<a>`.
12
+
13
+ **Accessibility.** It's a plain `<a href>`, so Enter follows it. Under forced colours it's `LinkText`. Under `contrast="more"` the underline is always 2px. `visited` turns a followed link's underline `ink-muted` (off by default; it helps in long lists of documents).
14
+
15
+ **Props:** `href` and every anchor attribute; `external`; `asChild`; `visited`.
16
+
17
+ **You provide:** where it goes, and link text that says where (not "click here").
18
+
19
+ ```tsx
20
+ <Link href="/docs/webhooks">Read about webhooks</Link>
21
+ <Link href="https://status.example.com" external>Status page</Link>
22
+ <Link asChild><NextLink href="/billing">Manage billing</NextLink></Link>
23
+ ```
24
+
25
+ - Do write link text that makes sense on its own ("Read about webhooks", not "here").
26
+ - Do use `external` for anything that leaves your app.
27
+ - Don't use a link for an action (Save, Delete); that's a `Button`.
28
+ - Don't colour a link yourself, or take its underline away inside prose.
29
+
30
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
31
+ ## Example
32
+
33
+ `Link` as first used in its preview:
34
+
35
+ ```tsx
36
+ import { Link } from 'altitude-design';
37
+
38
+ <Link href="#webhooks">Read about webhooks</Link>
39
+ ```
40
+
41
+ The full preview, as rendered in Storybook and checked in CI (`lk`, `col`, `cap` are the preview page's own layout classes, not Altitude's):
42
+
43
+ ```tsx
44
+ import * as React from 'react';
45
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle, Link } from 'altitude-design';
46
+
47
+ // A router's link, to show asChild: Link styles it and it keeps its own behaviour.
48
+ const RouterLink = React.forwardRef(({ to, ...rest }, ref) => <a ref={ref} href={'#' + to} {...rest} />);
49
+ export default function App() {
50
+ return (
51
+ <div className="lk">
52
+ <div className="col">
53
+ <p>Webhooks send a signed POST for each event. <Link href="#webhooks">Read about webhooks</Link> before you add an endpoint, or check the <Link href="https://status.example.com" external>status page</Link> if deliveries stop.</p>
54
+ <p className="cap">In prose, the underline thickens on hover. Nothing lifts, so the line of text stays put.</p>
55
+ </div>
56
+ <Card>
57
+ <CardHeader><CardTitle>Invoices</CardTitle><CardDescription>Your last three invoices</CardDescription></CardHeader>
58
+ <CardContent>
59
+ <div className="col">
60
+ <span>INV-0042 · €49.00 · Paid</span>
61
+ <Link asChild><RouterLink to="/billing/invoices">View all invoices →</RouterLink></Link>
62
+ </div>
63
+ </CardContent>
64
+ </Card>
65
+ </div>
66
+ );
67
+ }
68
+ ```
69
+ <!-- /example -->
@@ -0,0 +1,113 @@
1
+ # Markdown
2
+
3
+ An answer's Markdown, set as Altitude prose, as it streams. Models answer in Markdown; `Markdown` renders it in the system's type and hands the parts Altitude already has to their components: fenced code to `CodeBlock`, links to `Link`, `[n]` markers to `InlineCitation`, tables to `Table`'s flush rows, `---` to `Separator`.
4
+
5
+ **Elevation: the record's typography, adding no height.**
6
+ - **Paragraphs, lists and headings** are on the ground. A `#` heading is the heading style (never the title, which belongs to the page), weight 600; `##` and below step down.
7
+ - **Inline code** has a 1px `line` rim and no fill. **Fenced code** is a `CodeBlock` titled with its language.
8
+ - **Tables** are `Table`'s flush rows with `line` dividers, in a frame that scrolls sideways when the table is wider than the message.
9
+ - **Blockquotes** have a 2px `line` rule at their start. **Task items** have a read-only stamped check.
10
+ - **A citation** is the one part that lifts, by one, when pointed at.
11
+ - **An image** waits in a sunk imprint at a 16:9 size and rises out of it when it loads; if it fails, its alt text shows.
12
+ - **While streaming** an open fence is a streaming `CodeBlock` (dashed rule, "Writing…", `aria-busy`), a table shows nothing until its delimiter row arrives and never a half-received row, and an unclosed `**`, backtick or link shows its words without the markers. The answer as a whole is still carried by `Message status="streaming"`.
13
+
14
+ **Accessibility.** Plain HTML: headings offset by `headingLevel` (default 3, so `#` is `h3` under the conversation's), lists, `table` with a header row, `blockquote`, `pre`/`code`, `a`. A table is in a focusable region named after its first columns, so it can be scrolled by keyboard. Markdown announces nothing; `Conversation` announces an answer once it completes. Off-site links say "(opens in a new tab)".
15
+
16
+ **Safety.** Raw HTML in the source is shown as text unless `allowHtml`. URLs pass through `transformUrl`, which by default refuses `javascript:`, `vbscript:`, `file:` and `data:` (except images); a refused link keeps its words.
17
+
18
+ **Props:** `children` (the source), `streaming`, `citations`, `idPrefix`, `syntax`, `headingLevel`, `components` (`a`, `img`, `code`, `pre`, `table`, `blockquote`), `allowHtml`, `transformUrl`. Also exported: `parseMarkdown`, `safeUrl`.
19
+
20
+ **You provide:** the text, `streaming` while it arrives, and the sources its `[n]` markers refer to. It covers CommonMark plus GFM tables, task lists, strikethrough and bare links, with no dependencies; footnotes, math and reference-style links aren't parsed. For those, render with your own parser and pass its parts through `components`.
21
+
22
+ ```tsx
23
+ <Message from="assistant" status={m.status}>
24
+ <MessageContent>
25
+ <Markdown streaming={m.status === 'streaming'} citations={m.sources} syntax={highlight}>{m.text}</Markdown>
26
+ </MessageContent>
27
+ </Message>
28
+ ```
29
+
30
+ - Do pass `streaming` while the answer arrives, so half-received constructs don't flash as raw text.
31
+ - Do give each answer its own `idPrefix` when several cite sources on one page.
32
+ - Don't turn on `allowHtml` for model output or anything else you don't control.
33
+
34
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
35
+ ## Example
36
+
37
+ `Markdown` as first used in its preview:
38
+
39
+ ```tsx
40
+ import { Markdown } from 'altitude-design';
41
+
42
+ <Markdown citations={sources}>{done}</Markdown>
43
+ ```
44
+
45
+ The full preview, as rendered in Storybook and checked in CI (`md-demo`, `lbl`, `col` are the preview page's own layout classes, not Altitude's):
46
+
47
+ ```tsx
48
+ import { Markdown } from 'altitude-design';
49
+
50
+ const sources = [
51
+ { title: 'Billing FAQ', url: 'https://help.example.com/billing/faq', excerpt: 'Annual plans can be refunded in full within 30 days of purchase.' },
52
+ { title: 'Plans and pricing', url: 'https://example.com/pricing' },
53
+ ];
54
+ const done = [
55
+ '# Refunds on annual plans',
56
+ '',
57
+ 'Annual plans are refunded **in full** within 30 days [1]; after that, the unused months are credited as *account balance*, not cash [2].',
58
+ '',
59
+ '## What to check first',
60
+ '',
61
+ '- [x] The plan was bought less than 30 days ago',
62
+ '- [ ] No seats were added since',
63
+ '- [ ] The invoice is `paid`, not ~~pending~~',
64
+ '',
65
+ '| Plan | Window | Refund |',
66
+ '| --- | :---: | ---: |',
67
+ '| Monthly | none | $0 |',
68
+ '| Annual | 30 days | 100% |',
69
+ '',
70
+ '> Credits never expire, but they can\'t be moved to another workspace.',
71
+ '',
72
+ 'Issue it from the console:',
73
+ '',
74
+ '```sh',
75
+ 'altitude billing refund INV-0042 --full',
76
+ '```',
77
+ '',
78
+ 'See [the billing guide](https://help.example.com/billing) for partial refunds.',
79
+ ].join('\n');
80
+ const streaming = [
81
+ '## Steps',
82
+ '',
83
+ '1. Find the invoice',
84
+ '2. Check the **refund window',
85
+ '',
86
+ '| Invoice | Amount | Status |',
87
+ '| --- | ---: | --- |',
88
+ '| INV-0042 | $480 | paid |',
89
+ '| INV-0043 | $1',
90
+ ].join('\n') + '';
91
+ const streamingCode = ['Run this:', '', '```ts', 'const refund = await billing.refund({', " invoice: 'INV-0042',"].join('\n');
92
+ export default function App() {
93
+ return (
94
+ <div className="md-demo">
95
+ <div>
96
+ <p className="lbl">Complete</p>
97
+ <Markdown citations={sources}>{done}</Markdown>
98
+ </div>
99
+ <div className="col">
100
+ <div>
101
+ <p className="lbl">Mid-stream: an unclosed bold and a half-received row</p>
102
+ <Markdown streaming>{streaming}</Markdown>
103
+ </div>
104
+ <div>
105
+ <p className="lbl">Mid-stream: an open fence</p>
106
+ <Markdown streaming>{streamingCode}</Markdown>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ );
111
+ }
112
+ ```
113
+ <!-- /example -->
@@ -0,0 +1,81 @@
1
+ # Meter
2
+
3
+ Where a reading sits in a known range: storage used of a quota, seats used of a plan, CPU, a score. It isn't a task's progress: a meter doesn't move toward done, and being near the top can be bad. With `segments` it shows parts of a whole (storage by type), the honest replacement for a donut chart.
4
+
5
+ **Why it's flush, not a trench.** `Progress` and `Meter` look alike and mean different things (see [Charts](../../visual/Charts.md)). `Progress` is a task under way, which is live, so its fill rises out of a trench. A meter's reading is the record, so it lies on the ground: a flush track of `line` with the reading drawn across it as flat ink (`data-1`), with no shadow, wall or sheen.
6
+
7
+ **Thresholds.** `high` is where the reading is near its limit and `critical` where it's over (by default, anything above `max`). Each threshold inside the range is a 1px `ink-muted` notch standing at the track's rim. Past `high` the fill turns `caution`, and past the limit `critical`, and the meter says so in words ("Near limit", "Over limit") on a `Badge` whose lamp relights when the state changes. Thresholds assume that higher is worse, as for quotas and load. For a score where lower is worse, leave them off and say the state in your own words.
8
+
9
+ **Segments.** Each part is a flat `data-*` fill in config order (`data-1`, `data-2`, …), parted from the one before by a 2px cut in the surface's own colour, with a legend of each part's value beneath. The segments add up to the reading, and thresholds still apply to the total.
10
+
11
+ **Motion.** The fill glides to a new reading on `ease-glide` over `dur-travel-3`, as Progress does, and its colour cross-fades when it crosses a threshold. Under reduced motion it changes in place. While the reading loads (`value` undefined) the track is an imprint and lands like `Await` content.
12
+
13
+ **Accessibility.** The [APG meter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/meter/): `role="meter"` named by its label, with `aria-valuemin`, `aria-valuemax` and `aria-valuenow` (kept within the range when the reading is over it) and an `aria-valuetext` in words: "Storage, 62 GB of 100 GB", "11,800 of 10,000, over limit", "62 GB of 100 GB, Media 38 GB, Documents 14 GB…". Under forced colours the track is outlined, the fill is `Highlight`, the notches are `CanvasText`, and the state is in the words. Under `contrast="more"` the track gets a 1px `ink` edge and the notches are `ink`.
14
+
15
+ **Props:** `label`; `value`; `min` (default 0); `max` (default 100); `high`; `critical`; `segments` (`{ key, label, value, color? }[]`); `format`; `states` (`{ high, critical }` words); `hideValue`; `locale`.
16
+
17
+ **You provide:** the reading and its range, the thresholds that matter, and a `format` that names the unit.
18
+
19
+ ```tsx
20
+ <Meter label="Storage" value={62} max={100} high={80} format={(n) => `${n} GB`} />
21
+ <Meter label="Storage by type" max={100} format={(n) => `${n} GB`}
22
+ segments={[{ key: 'media', label: 'Media', value: 38 }, { key: 'docs', label: 'Documents', value: 14 }, { key: 'other', label: 'Other', value: 10 }]} />
23
+ ```
24
+
25
+ - Do use `Progress` for a task moving toward done, and `Meter` for a reading.
26
+ - Do name the unit in `format` ("62 GB of 100 GB"), not only a percentage.
27
+ - Do use segments for parts of a whole instead of a pie or donut.
28
+ - Don't give it more than six segments; fold the rest into "Other".
29
+ - Don't colour a reading caution or critical yourself; set `high` and `critical`, so the words come with the colour.
30
+
31
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
32
+ ## Example
33
+
34
+ `Meter` as first used in its preview:
35
+
36
+ ```tsx
37
+ import { Meter } from 'altitude-design';
38
+
39
+ <Meter label="Storage" value={used} max={100} high={80} format={gb} />
40
+ ```
41
+
42
+ The full preview, as rendered in Storybook and checked in CI (`mt`, `stack`, `row` are the preview page's own layout classes, not Altitude's):
43
+
44
+ ```tsx
45
+ import * as React from 'react';
46
+ import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Meter } from 'altitude-design';
47
+
48
+ const gb = (n) => `${n} GB`;
49
+ export default function App() {
50
+ const [used, setUsed] = React.useState(62);
51
+ const [cpu, setCpu] = React.useState(undefined);
52
+ React.useEffect(() => { const t = setTimeout(() => setCpu(37), 2400); return () => clearTimeout(t); }, []);
53
+ return (
54
+ <div className="mt">
55
+ <Card>
56
+ <CardHeader><CardTitle>Plan usage</CardTitle><CardDescription>Thresholds at 80% and the limit</CardDescription></CardHeader>
57
+ <CardContent className="stack">
58
+ <Meter label="Storage" value={used} max={100} high={80} format={gb} />
59
+ <Meter label="Seats" value={24} max={25} high={20} format={(n) => `${n}`} />
60
+ <Meter label="API calls today" value={11800} max={10000} high={8000} format={(n) => n.toLocaleString('en')} />
61
+ <Meter label="CPU" value={cpu} format={(n) => `${n}%`} />
62
+ <div className="row">
63
+ <Button size="sm" variant="secondary" onClick={() => setUsed((u) => Math.max(0, u - 15))}>Free 15 GB</Button>
64
+ <Button size="sm" variant="secondary" onClick={() => setUsed((u) => u + 15)}>Use 15 GB</Button>
65
+ </div>
66
+ </CardContent>
67
+ </Card>
68
+ <Card>
69
+ <CardHeader><CardTitle>Storage by type</CardTitle><CardDescription>Parts of a whole, instead of a donut</CardDescription></CardHeader>
70
+ <CardContent className="stack">
71
+ <Meter label="Storage" max={100} format={gb}
72
+ segments={[{ key: 'media', label: 'Media', value: 38 }, { key: 'docs', label: 'Documents', value: 14 }, { key: 'mail', label: 'Mail', value: 6 }, { key: 'other', label: 'Other', value: 4 }]} />
73
+ <Meter label="Team seats" max={50} high={45} format={(n) => `${n}`}
74
+ segments={[{ key: 'admin', label: 'Admins', value: 4 }, { key: 'members', label: 'Members', value: 31 }, { key: 'guests', label: 'Guests', value: 12 }]} />
75
+ </CardContent>
76
+ </Card>
77
+ </div>
78
+ );
79
+ }
80
+ ```
81
+ <!-- /example -->