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,92 @@
1
+ # Stat
2
+
3
+ The tile at the top of a dashboard: a label, a big number, how it changed and against what ("Active users · 12,480 · Up 8% vs last month: 11,556"). It's often the most-read thing on the page, and sometimes the right answer isn't a chart at all.
4
+
5
+ **Why it's flush.** A number is the record. A stat has no level of its own: inside a `Card` it lies on the card, on its own it lies on the page. The label is `text-label` in `ink-muted`; the value is `text-title` in `ink`, weight 600, with tabular figures. `size="hero"` sets it in `text-display`, for the one number a view leads with (once per view, as the type scale says). A `trend` sits beside the value, usually a decorative [`Sparkline`](../Sparkline/README.md).
6
+
7
+ **Up isn't always good.** Without `good`, the change is neutral: `ink-muted` text with an arrow ("↘ Down 12 ms"). Say which direction is good (`good="up"` for users, `good="down"` for an error rate) and the change becomes a `Badge` plate whose lamp is `positive` or `critical`. The word says the direction, the arrow shows it, and the colour says whether it's good, so no cue stands alone. No change reads "No change" and stays neutral.
8
+
9
+ **Motion.** A changed value cross-fades (`dur-travel-2`) and the stat takes the afterglow (`settle`), within the attention budget. It never counts up: that delays the number for decoration. While the value loads (`undefined`, or a promise) it's an imprint of a number's width under a label already in place, and it lands like any `Await` content. In a `StatGroup` the values land together.
10
+
11
+ **Accessibility.** Plain HTML: a `section` named by its label, so it reads "Active users, 12,480, Up 8%, vs last month: 11,556". Direction is always a word; the arrow is decorative. With `live`, a changed value is announced politely: "Active users, 12,792, up 10.7% vs last month." Under forced colours the change plate gets a 1px border and the arrow carries direction; under `contrast="more"` it's drawn as `Badge` is.
12
+
13
+ **Props:** `label`; `value` (a number, a string, `null` for an em dash, `undefined` or a promise while it loads); `format`; `change` (`{ value, vs, previous, format }`: `value` is a fraction by default, so `0.08` reads "Up 8%"; with `format` it's in your units); `good` (`'up'` or `'down'`); `trend`; `help` (a caption of context); `size` (`'default'` or `'hero'`); `live`; `locale`. `StatGroup` lays a row of stats out to wrap (each at least 180px) and lands them together.
14
+
15
+ **You provide:** the value and, if it changed, by how much and against what, in words. Put each stat in a `Card` on a dashboard; make the card `interactive` if the whole stat opens a report.
16
+
17
+ ```tsx
18
+ <StatGroup aria-label="Last 30 days">
19
+ <Card><CardContent>
20
+ <Stat label="Active users" value={metrics?.active} good="up"
21
+ change={{ value: 0.08, vs: 'last month', previous: 11556 }}
22
+ trend={<Sparkline data={metrics?.daily} y="active" decorative />} />
23
+ </CardContent></Card>
24
+ </StatGroup>
25
+ ```
26
+
27
+ - Do say what the change is against ("vs last month"); a percentage alone has no anchor.
28
+ - Do set `good` only when a direction really is good or bad; otherwise leave the change neutral.
29
+ - Do lead a dashboard with stats, then the charts that explain them, then the table that is the record.
30
+ - Don't use `size="hero"` more than once in a view.
31
+ - Don't animate the number counting up, or colour the value itself: colour belongs to the change, and only with `good`.
32
+
33
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
34
+ ## Example
35
+
36
+ `Stat` as first used in its preview:
37
+
38
+ ```tsx
39
+ import { Stat } from 'altitude-design';
40
+
41
+ <Stat label="Monthly recurring revenue" size="hero" value={mrr} format={usd.format} good="up"
42
+ change={{ value: 0.042, vs: 'September', previous: 176800 }} help="Paid plans only, before refunds." />
43
+ ```
44
+
45
+ The full preview, as rendered in Storybook and checked in CI (`st`, `st-bar` are the preview page's own layout classes, not Altitude's):
46
+
47
+ ```tsx
48
+ import * as React from 'react';
49
+ import { Button, Card, CardContent, Sparkline, Stat, StatGroup } from 'altitude-design';
50
+
51
+ const stDays = (vals) => vals.map((v, i) => ({ day: `2026-10-${String(i + 1).padStart(2, '0')}`, v }));
52
+ const active = stDays([11.2, 11.4, 11.3, 11.6, 11.5, 11.8, 11.9, 12.0, 11.9, 12.1, 12.2, 12.4, 12.3, 12.48].map((k) => k * 1000));
53
+ const errors = stDays([0.8, 0.9, 0.8, 1.0, 1.1, 1.0, 1.2, 1.3, 1.2, 1.4, 1.5, 1.4, 1.6, 1.7]);
54
+ const usd = new Intl.NumberFormat('en', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
55
+ export default function App() {
56
+ const [n, setN] = React.useState(0);
57
+ const [mrr, setMrr] = React.useState(undefined);
58
+ React.useEffect(() => { const t = setTimeout(() => setMrr(184200), 2400); return () => clearTimeout(t); }, []);
59
+ const users = 12480 + n * 312;
60
+ return (
61
+ <div className="st">
62
+ <Card>
63
+ <CardContent>
64
+ <Stat label="Monthly recurring revenue" size="hero" value={mrr} format={usd.format} good="up"
65
+ change={{ value: 0.042, vs: 'September', previous: 176800 }} help="Paid plans only, before refunds." />
66
+ </CardContent>
67
+ </Card>
68
+ <div className="st-bar">
69
+ <h3>Last 30 days</h3>
70
+ <Button variant="secondary" size="sm" onClick={() => setN((k) => k + 1)}>Refresh</Button>
71
+ </div>
72
+ <StatGroup aria-label="Last 30 days">
73
+ <Card><CardContent>
74
+ <Stat label="Active users" value={users} live good="up" change={{ value: users / 11556 - 1, vs: 'last month', previous: 11556 }}
75
+ trend={<Sparkline data={active} x="day" y="v" decorative width={72} />} />
76
+ </CardContent></Card>
77
+ <Card><CardContent>
78
+ <Stat label="Error rate" value={1.7 + n * 0.1} format={(v) => `${v.toFixed(1)}%`} good="down" change={{ value: 0.21 + n * 0.07, vs: 'last month' }}
79
+ trend={<Sparkline data={errors} x="day" y="v" tone="critical" decorative width={72} />} />
80
+ </CardContent></Card>
81
+ <Card><CardContent>
82
+ <Stat label="Median response" value={182 - n * 4} format={(v) => `${v} ms`} change={{ value: -12 - n * 4, vs: 'last week', format: (m) => `${m} ms` }} />
83
+ </CardContent></Card>
84
+ <Card><CardContent>
85
+ <Stat label="Refunds" value={null} help="No refunds this period." />
86
+ </CardContent></Card>
87
+ </StatGroup>
88
+ </div>
89
+ );
90
+ }
91
+ ```
92
+ <!-- /example -->
@@ -0,0 +1,90 @@
1
+ # Stepper
2
+
3
+ Shows where you are in a process of a few steps: done steps settle into the record, the current one stands raised, and steps to come are imprints.
4
+
5
+ **Elevation.** A stepper is a staircase of plates standing on one ground line, in the same vocabulary as Drilldown's breadcrumb, and each step's height says which of the three things it is.
6
+
7
+ - **Done: settled into the record.** A plate flush with the ground, its edges stamped like a Badge, wearing the Checkbox family's check: a raised white mark on the accent. Done steps are part of the record now, so they don't stand up.
8
+ - **Current: live.** A raised slab with a wall (`wall-2`, as tall as the primary action), `surface-raised`, its number ringed in `accent-ink`. It has no sheen: sheen means "press me", and the current step does nothing when pressed; its wall is staircase height, as on the breadcrumb's current step.
9
+ - **To come: not built yet.** Imprints pressed into the page (`surface-well`, a dark inner top edge), like Skeleton's.
10
+
11
+ Connectors between steps are `accent` behind you and `line-control` ahead.
12
+
13
+ **Advancing** is the record being written. The current step presses down into the ground (`ease-sink`) and its check is stamped in (as a Badge is re-stamped: down 3px, settling, while the tick draws); about 150ms later the next step rises out of its imprint (`dur-travel-3`, `ease-rise`), within the attention budget. Going back runs the other way: the current step sinks back into an imprint and the step you chose rises from the record. Every state uses the same six-layer shadow list, so each animates into the others. Reduced motion keeps the changes of level and drops the travel.
14
+
15
+ **Keyboard and screen readers.** An `<ol>` (named "Progress" unless you pass `aria-label`) with `aria-current="step"` on the current step. Every step also says its state in words ("Step 2 of 5, completed"), and a polite live region announces the new step when it changes. With `onStepChange`, completed steps become buttons (`role="button"`, Enter or Space) that go back to that step; hovering one shows a ghost of the slab it would become. Steps ahead can't be jumped to. In forced colours the current step has a solid border and steps to come a dashed one.
16
+
17
+ | Prop | Default | |
18
+ |---|---|---|
19
+ | `steps` | | `{ title, description?, id? }[]` |
20
+ | `current` | | index of the step being worked on; `steps.length` means all done |
21
+ | `onStepChange` | | makes completed steps buttons that go back |
22
+ | `orientation` | `horizontal` | `vertical` stacks the plates, with descriptions under the titles |
23
+
24
+ **Narrow.** `orientation="auto"` (recommended) lays the steps out in a row when they all fit on one line, and down the page when they don't, so a phone never shows connectors leading to the next line.
25
+
26
+ **You provide:** `steps` with short titles (one or two words, sentence case), `current`, and the Back and Continue buttons that change it; `onStepChange` if people may revisit completed steps.
27
+
28
+ - Do keep it to three to six steps; more than that is a list, not a staircase.
29
+ - Don't use it for places people move between freely; that's Tabs, or Drilldown for a hierarchy.
30
+
31
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
32
+ ## Example
33
+
34
+ `Stepper` as first used in its preview:
35
+
36
+ ```tsx
37
+ import { Stepper } from 'altitude-design';
38
+
39
+ <Stepper
40
+ steps={steps}
41
+ current={at}
42
+ onStepChange={setAt}
43
+ orientation="auto"
44
+ aria-label="Workspace setup"
45
+ />
46
+ ```
47
+
48
+ The full preview, as rendered in Storybook and checked in CI (`wrap`, `row`, `cap`, `pair` are the preview page's own layout classes, not Altitude's):
49
+
50
+ ```tsx
51
+ import * as React from 'react';
52
+ import { Button, Stepper } from 'altitude-design';
53
+
54
+ const steps = [{ title: "Account" }, { title: "Workspace" }, { title: "Invite team" }, { title: "Billing" }, { title: "Launch" }];
55
+ const setup = [
56
+ { title: "Connect repository", description: "github.com/acme/northwind" },
57
+ { title: "Configure build", description: "Framework, output directory, env" },
58
+ { title: "Deploy", description: "First production deployment" }
59
+ ];
60
+ export default function App() {
61
+ const [at, setAt] = React.useState(2);
62
+ return <div className="wrap">
63
+ <Stepper
64
+ steps={steps}
65
+ current={at}
66
+ onStepChange={setAt}
67
+ orientation="auto"
68
+ aria-label="Workspace setup"
69
+ />
70
+ <div className="row">
71
+ <Button variant="ghost" disabled={at === 0} onClick={() => setAt(at - 1)}>
72
+ Back
73
+ </Button>
74
+ <Button variant="primary" disabled={at === steps.length} onClick={() => setAt(at + 1)}>
75
+ {at >= steps.length - 1 ? "Finish" : "Continue"}
76
+ </Button>
77
+ <span className="cap">
78
+ Continue presses the current step into the record and raises the next one; click a completed step to go back.
79
+ </span>
80
+ </div>
81
+ <div className="pair">
82
+ <Stepper steps={setup} current={1} orientation="vertical" aria-label="Deployment setup" />
83
+ <p className="cap">
84
+ Vertical: the same plates, stacked. Completed steps are flush and checked, the current one stands on its wall, and steps to come are imprints.
85
+ </p>
86
+ </div>
87
+ </div>;
88
+ }
89
+ ```
90
+ <!-- /example -->
@@ -0,0 +1,87 @@
1
+ # Suggestions
2
+
3
+ Prompts to start from or follow up with. An empty conversation offers starting points ("Summarise last week's incidents"); a finished answer offers follow-ups ("Compare with Q2"). Choosing one either fills the composer to edit or sends it at once.
4
+
5
+ **Elevation: things you press.**
6
+ - **Each suggestion** is a small secondary `Button`: raised at level 1 with its wall, lifting to 2 when pointed at, pressed to the ground. Never level 2 at rest: the view's one primary is the composer's Send.
7
+ - **Starters** sit in an `EmptyState` (its sunk opening) and are simply there.
8
+ - **Follow-ups** sit under the answer, inside its `Message`, and cross-fade in over `dur-onset`; they don't rise, because they only became available.
9
+ - **A send-mode suggestion** shows an arrow and is named "…, sends now".
10
+
11
+ **Accessibility.** A labelled `group` of buttons; with more than four, the toolbar pattern with one tab stop and arrow keys. Enter or Space chooses. After choosing, focus goes to the composer, with the caret at the end when the text was filled in. Follow-ups aren't announced when they arrive; the answer was.
12
+
13
+ **Props:** `Suggestions`: `aria-label` (required), `mode` (`'fill'` default, or `'send'`), `onChoose(value)`, `arrive` (default true inside a `Message`). `Suggestion`: `value` (filled or sent instead of the label), and button props.
14
+
15
+ **You provide:** what choosing does. For `fill`, set the composer's value (`PromptInput value`); for `send`, send it.
16
+
17
+ ```tsx
18
+ <Message from="assistant">
19
+ <MessageContent>…</MessageContent>
20
+ <Suggestions aria-label="Suggested follow-ups" mode="send" onChoose={send}>
21
+ <Suggestion>Compare with Q2</Suggestion>
22
+ <Suggestion value="List the three biggest risks, with owners">Biggest risks</Suggestion>
23
+ </Suggestions>
24
+ </Message>
25
+ ```
26
+
27
+ - Do keep them short, and few (three is plenty).
28
+ - Do use `fill` when the person will likely want to adjust the prompt.
29
+ - Don't make a suggestion the primary button, or animate follow-ups in with a rise.
30
+
31
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
32
+ ## Example
33
+
34
+ `Suggestions` as first used in its preview:
35
+
36
+ ```tsx
37
+ import { Suggestion, Suggestions } from 'altitude-design';
38
+
39
+ <Suggestions aria-label="Start with" onChoose={setDraft}>
40
+ <Suggestion>Summarise last week's incidents</Suggestion>
41
+ <Suggestion>Draft a release note</Suggestion>
42
+ <Suggestion value="Which of my open pull requests are blocked, and by whom?">What's blocked?</Suggestion>
43
+ </Suggestions>
44
+ ```
45
+
46
+ The full preview, as rendered in Storybook and checked in CI (`sg`, `col`, `lbl` are the preview page's own layout classes, not Altitude's):
47
+
48
+ ```tsx
49
+ import * as React from 'react';
50
+ import { EmptyState, Message, MessageContent, PromptInput, PromptInputSubmit, PromptInputTextarea, PromptInputToolbar, Suggestion, Suggestions } from 'altitude-design';
51
+
52
+ export default function App() {
53
+ const [draft, setDraft] = React.useState('');
54
+ const [sent, setSent] = React.useState([]);
55
+ return (
56
+ <div className="sg">
57
+ <div className="col">
58
+ <p className="lbl">Starters in an empty conversation (fill the composer)</p>
59
+ <EmptyState title="Ask about this workspace" description="Pick a starting point, or write your own." size="sm">
60
+ <Suggestions aria-label="Start with" onChoose={setDraft}>
61
+ <Suggestion>Summarise last week's incidents</Suggestion>
62
+ <Suggestion>Draft a release note</Suggestion>
63
+ <Suggestion value="Which of my open pull requests are blocked, and by whom?">What's blocked?</Suggestion>
64
+ </Suggestions>
65
+ </EmptyState>
66
+ <PromptInput onSubmit={({ text }) => { setSent((s) => [...s, text]); setDraft(''); }} value={draft} onValueChange={setDraft}>
67
+ <PromptInputTextarea placeholder="Ask anything" />
68
+ <PromptInputToolbar><PromptInputSubmit /></PromptInputToolbar>
69
+ </PromptInput>
70
+ </div>
71
+ <div className="col">
72
+ <p className="lbl">Follow-ups under an answer (send at once)</p>
73
+ <Message from="assistant">
74
+ <MessageContent>Revenue grew 12% in Q3, led by the annual plan. Churn held at 2.1%.</MessageContent>
75
+ <Suggestions aria-label="Suggested follow-ups" mode="send" onChoose={(t) => setSent((s) => [...s, t])}>
76
+ <Suggestion>Compare with Q2</Suggestion>
77
+ <Suggestion>Make it shorter</Suggestion>
78
+ <Suggestion value="List the three biggest risks, with owners">Biggest risks</Suggestion>
79
+ </Suggestions>
80
+ </Message>
81
+ {sent.length > 0 && <p className="lbl">Sent: {sent[sent.length - 1]}</p>}
82
+ </div>
83
+ </div>
84
+ );
85
+ }
86
+ ```
87
+ <!-- /example -->
@@ -0,0 +1,48 @@
1
+ # Surface
2
+
3
+ The primitive every component is built from: a box placed on one rung of the elevation ladder.
4
+
5
+ `level` picks the tint and shadow together (`surface-raised` + `elev-1` at level 1, `surface-well` + `well` at −1, and so on), so the two can never drift apart. `interactive` makes it climb one level on hover and focus and drop to ground while pressed.
6
+
7
+ **You provide:** `level`, children, and padding and radius through `className`/`style` (the default radius is `radius-lg`). With `interactive` and `onClick`, a non-native surface receives button semantics, keyboard focus, Enter activation and Space activation on release. Consumer keyboard handlers can cancel that behavior with `preventDefault()`. Prefer `as="button"` for a single action (its default `type` is `button`) or `as="a" href="…"` for navigation; their native keyboard behavior is preserved. `disabled` or `aria-disabled` prevents activation, including native links without a click callback. Disabled links omit their destination and retain link semantics. Events from nested controls do not activate the containing surface. Keep interactive content outside a native button or link; use a separate named action when a card contains several controls.
8
+
9
+ **Depth signals** (also on `Card`, and available to your own components through `useDepth`):
10
+
11
+ | Prop | What it shows |
12
+ |---|---|
13
+ | `settle` | Afterglow. Pass any value; when it changes, the surface tints with `accent-soft`, lifts to `elev-2` and settles back over `dur-settle` (full for 1s, noticeable for ~4s). `true` settles once on mount, for newly added items. |
14
+ | `status` | Commitment. `pending` hovers above the ground after `dur-grace` (so fast saves never flicker) and sets `aria-busy`; changing to `committed` lands with a settle; `failed` drops into a trench on `critical-soft`. Pair it with visible text ("Saving…", "Couldn't save"). |
15
+ | `heldBy` | Possession. `{ name, presence: 1–3 }` lifts it to level 3, outlines it in that `presence-*` colour and shows the name. Use for collaborators editing or dragging it. |
16
+ | `relevance` | Focus+context. `match` rises one level, `miss` sinks into a well in `ink-muted`. Use for filtering in place instead of removing items. |
17
+
18
+ - Do use Surface for one-off panels, sidebars and app chrome so they share the ladder with the components.
19
+ - Don't set `interactive` on something that doesn't act when clicked: a lift is a promise.
20
+ - Don't stack more than three levels in one region; if you need a fourth, the layout probably wants a dialog or a page.
21
+
22
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
23
+ ## Example
24
+
25
+ `Surface` as first used in its preview:
26
+
27
+ ```tsx
28
+ import { Surface } from 'altitude-design';
29
+
30
+ <Surface level={-1} className="s"><b>level={'{-1}'}</b><span>A well. Holds things; never lifts.</span></Surface>
31
+ ```
32
+
33
+ The full preview, as rendered in Storybook and checked in CI (`grid`, `s` are the preview page's own layout classes, not Altitude's):
34
+
35
+ ```tsx
36
+ import { Surface } from 'altitude-design';
37
+
38
+ export default function App() {
39
+ return (
40
+ <div className="grid" data-tells-ignore="LA4 the three surfaces side by side, to compare">
41
+ <Surface level={-1} className="s"><b>level={'{-1}'}</b><span>A well. Holds things; never lifts.</span></Surface>
42
+ <Surface level={1} className="s"><b>level={'{1}'}</b><span>Static grouping. Doesn't react to the pointer.</span></Surface>
43
+ <Surface level={1} interactive className="s" onClick={() => {}}><b>level={'{1}'} interactive</b><span>Hover: climbs to 2. Press: drops to ground.</span></Surface>
44
+ </div>
45
+ );
46
+ }
47
+ ```
48
+ <!-- /example -->
@@ -0,0 +1,38 @@
1
+ # Switch
2
+
3
+ Turns a setting on or off immediately: a raised thumb riding in a trench.
4
+
5
+ The track is level −2 (`surface-trench`, `trench`). Turning it on pours the `accent` into the track behind the thumb (a clip-path that follows it), and the thumb travels on `ease-settle`. Pressing stretches the thumb before it moves. Hover lifts the thumb from level 1 to 2.
6
+
7
+ **You provide:** `label` (or `aria-label`), and `checked`/`onCheckedChange` or `defaultChecked`. It renders `role="switch"`.
8
+
9
+ - Do use it only for changes that apply instantly. If the change needs a Save, use a Checkbox.
10
+
11
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
12
+ ## Example
13
+
14
+ `Switch` as first used in its preview:
15
+
16
+ ```tsx
17
+ import { Switch } from 'altitude-design';
18
+
19
+ <Switch label="Sync across devices" defaultChecked />
20
+ ```
21
+
22
+ The full preview, as rendered in Storybook and checked in CI (`col`, `cap` are the preview page's own layout classes, not Altitude's):
23
+
24
+ ```tsx
25
+ import { Switch } from 'altitude-design';
26
+
27
+ export default function App() {
28
+ return (
29
+ <div className="col">
30
+ <Switch label="Sync across devices" defaultChecked />
31
+ <Switch label="Email me when someone comments" />
32
+ <Switch label="Disabled" disabled />
33
+ <p className="cap">Hold the pointer down: the thumb stretches before it travels.</p>
34
+ </div>
35
+ );
36
+ }
37
+ ```
38
+ <!-- /example -->
@@ -0,0 +1,78 @@
1
+ # SyncIndicator
2
+
3
+ One honest answer to "is my work saved?", for autosaving editors, optimistic UIs and local-first apps: "Saved", "Saving…", "Offline · 3 changes waiting", "Couldn't sync", "1 conflict to resolve". Write-ups on local-first apps name showing unsynced edits as saved as the most common failure.
4
+
5
+ **Elevation.** A status plate is a label, and labels never lift. It's a flush `Badge` plate whose lamp relights when the state changes: positive when saved (with "· 2 minutes ago"), pulsing while saving, caution when offline or in conflict, critical when a sync failed. A change of state cross-fades rather than rising, because "things that merely changed state cross-fade". The *items* it covers carry `status="pending"` and hover until they land, so the per-item and the global signals agree. With a `queue`, pressing the plate opens the waiting changes in a `Popover`. The plate then lifts when pointed at, because it acts.
6
+
7
+ **When it fails.** `failed` shows Retry (`onRetry`). `conflict` shows Resolve (`onOpenConflicts`). The patterns around it (offline queues, conflicts, session expiry) are a framework pattern of their own.
8
+
9
+ **Accessibility.** The words are a polite status region, said without chatter. "Saved" is said at most every `announceEvery` ms (default 5s), not "Saving… Saved" on every key. "Saving" isn't said. Going offline, failures and conflicts are said at once. With a queue, the plate is a button named by its state ("Offline · 2 changes waiting. Show changes"). Under forced colours the words and the lamp carry it. Under `contrast="more"` the plate gets a `line-control` edge. Under reduced motion the lamp doesn't pulse; the word says "Saving…".
10
+
11
+ **Props:** `state` (`'saved' | 'saving' | 'offline' | 'failed' | 'conflict'`); `pending`; `conflicts`; `lastSyncedAt`; `onRetry`; `onOpenConflicts`; `queue`; `announceEvery`.
12
+
13
+ **You provide:** the true state of your sync. Only say "Saved" once the server has acknowledged the change.
14
+
15
+ ```tsx
16
+ <SyncIndicator state={sync.state} pending={sync.queue.length} lastSyncedAt={sync.at}
17
+ onRetry={sync.retry} onOpenConflicts={openConflicts}
18
+ queue={sync.queue.map((c) => <Surface key={c.id} status="pending">{c.label}</Surface>)} />
19
+ ```
20
+
21
+ - Do show "Saved" only after the server confirms.
22
+ - Do count what's waiting when offline.
23
+ - Don't put a spinner beside it; the pulsing lamp and the word say it.
24
+ - Don't raise the plate to get attention when a sync fails; the lamp and the words change.
25
+
26
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
27
+ ## Example
28
+
29
+ `SyncIndicator` as first used in its preview:
30
+
31
+ ```tsx
32
+ import { Surface, SyncIndicator } from 'altitude-design';
33
+
34
+ <SyncIndicator state={state} pending={waiting.length} lastSyncedAt={at} conflicts={1} onRetry={() => { setState('saving'); setTimeout(() => { setState('saved'); setAt(new Date()); }, 700); }}
35
+ onOpenConflicts={() => setState('saved')}
36
+ queue={waiting.length ? waiting.map((w, i) => <Surface key={i} status="pending" className="change">{w}</Surface>) : undefined} />
37
+ ```
38
+
39
+ The full preview, as rendered in Storybook and checked in CI (`si`, `bar`, `change`, `col`, `cap` are the preview page's own layout classes, not Altitude's):
40
+
41
+ ```tsx
42
+ import * as React from 'react';
43
+ import { Button, Surface, Switch, SyncIndicator, Textarea } from 'altitude-design';
44
+
45
+ export default function App() {
46
+ const [text, setText] = React.useState('Q4 goals: grow self-serve revenue 20% and halve time to first value.');
47
+ const [state, setState] = React.useState('saved');
48
+ const [at, setAt] = React.useState(() => new Date(Date.now() - 120000));
49
+ const [offline, setOffline] = React.useState(false);
50
+ const [waiting, setWaiting] = React.useState([]);
51
+ const timer = React.useRef();
52
+ const edit = (v) => {
53
+ setText(v);
54
+ if (offline) { setWaiting((w) => [...w.slice(-2), 'Edited the goals paragraph']); setState('offline'); return; }
55
+ setState('saving'); clearTimeout(timer.current);
56
+ timer.current = setTimeout(() => { setState('saved'); setAt(new Date()); }, 700);
57
+ };
58
+ return (
59
+ <div className="si">
60
+ <div>
61
+ <div className="bar"><span>Q4 planning</span>
62
+ <SyncIndicator state={state} pending={waiting.length} lastSyncedAt={at} conflicts={1} onRetry={() => { setState('saving'); setTimeout(() => { setState('saved'); setAt(new Date()); }, 700); }}
63
+ onOpenConflicts={() => setState('saved')}
64
+ queue={waiting.length ? waiting.map((w, i) => <Surface key={i} status="pending" className="change">{w}</Surface>) : undefined} />
65
+ </div>
66
+ <Textarea value={text} onChange={(e) => edit(e.target.value)} aria-label="Q4 goals" />
67
+ </div>
68
+ <div className="col">
69
+ <Switch label="Offline" checked={offline} onCheckedChange={(on) => { setOffline(on); if (!on) { setWaiting([]); setState('saving'); setTimeout(() => { setState('saved'); setAt(new Date()); }, 700); } }} />
70
+ <Button size="sm" onClick={() => setState('failed')}>Make a save fail</Button>
71
+ <Button size="sm" onClick={() => setState('conflict')}>Cause a conflict</Button>
72
+ <p className="cap">Type to save. Go offline and the plate counts what's waiting; press it to see those changes, each hovering until it lands.</p>
73
+ </div>
74
+ </div>
75
+ );
76
+ }
77
+ ```
78
+ <!-- /example -->
@@ -0,0 +1,138 @@
1
+ # Table
2
+
3
+ Rows and columns of records: the rows sit on the ground with `line` dividers, and only the ones that are selected, matching, in hand or under your pointer rise.
4
+
5
+ **Elevation: the record.** A table is the plainest thing in Altitude: its rows are what has been settled, so they lie flush on the ground. Height is spent only on rows that mean something right now:
6
+
7
+ - **Selected** rows are tinted `accent-soft` and raised one level (`elev-1`), with a checked Checkbox; the toolbar counts them with a count Badge, the one badge that floats, because the count wants your attention.
8
+ - **Interactive** rows (ones that open something) lift one level on hover, a ghost lift; pressing drops them back to the ground. Rows that do nothing don't move at all.
9
+ - **Filtering in place** raises matches one level and sinks misses into a well (`ink-muted`), so the rows keep their places and you can still see what didn't match (Signals: relevance).
10
+ - **Carried** rows: when the order changes (a sort, a move), each row travels to its new place (`dur-travel-3`, `ease-glide`) instead of the table being reshuffled, so you can follow a row you were looking at.
11
+ - **In hand**: a row picked up to reorder floats at level 3, slightly tilted, over a slot that opens as a well where it will land; its neighbours glide aside, as in SortableList.
12
+
13
+ The header stays flush until rows scroll under it, then rises to `elev-2` (as in ScrollArea); where rows continue below, the bottom edge curves away (`lip-bottom`). Cells can't share one shadow, so each row's height is drawn as a single slab by its first cell, as wide as the table.
14
+
15
+ **Semantics.** A plain `<table>` with `<th scope="col">`: it's a table, not a grid, because its cells aren't edited or navigated one by one. Sortable headers are buttons, and the `th` carries `aria-sort`. Selection lives in the row's Checkbox (no `aria-selected` on rows of a plain table). An interactive row's first cell is a real button, so the keyboard reaches it; clicking anywhere else on the row does the same. Move handles are buttons: Space picks a row up, ↑ and ↓ move it, Space drops it, Escape puts it back; each step is announced. Selection counts and sort changes are announced too. Forced colours rule raised rows solid and sunk rows dashed.
16
+
17
+ **Two ways to build one.** The primitives mirror shadcn/ui: `Table` (with `maxHeight` for a scrolling body), `TableHeader`, `TableBody`, `TableFooter`, `TableRow` (`rowId`, `selected`, `interactive`, `relevance`, `settle`, `held`), `TableHead` (`align`, `sort`, `onSort`), `TableCell` (`align`) and `TableCaption`. `DataTable` builds the whole thing from data:
18
+
19
+ | Prop | |
20
+ |---|---|
21
+ | `columns` | `{ key, header, cell?, sortValue?, align?, width? }[]`; `sortValue` makes a column sortable (ascending, descending, none) |
22
+ | `rows`, `getRowId`, `getRowLabel` | the records, a stable id, and a name for screen readers |
23
+ | `selectable`, `selected`/`onSelectedChange`, `actions` | a checkbox column; actions appear beside the count while rows are selected |
24
+ | `onRowClick` | interactive rows |
25
+ | `relevance` | `(row) => 'match' \| 'miss' \| undefined` for filtering in place |
26
+ | `onReorder` | move handles; hidden while a sort is active |
27
+ | `renderExpanded`, `expanded`/`defaultExpanded`/`onExpandedChange`, `expandMode` | a disclosure button per row opening a full-width detail row (`'single'` by default: one open at a time) |
28
+ | `renderEditor`, `onSave`, `onRowChange` | an Edit button per row that opens the row's fields as an `EditRegion` in a full-width row beneath it (EditRegion's save contract; `onRowChange` publishes the canonical row) |
29
+ | `toolbar`, `maxHeight`, `empty` | a filter field or button at the toolbar's end (at narrow widths the toolbar wraps onto more lines and a field in it shrinks, so nothing scrolls sideways); scroll height; text for no rows |
30
+
31
+ **Loading.** While `DataTable`'s `rows` is undefined (your query hasn't returned) or a promise, the table keeps its real header over `skeleton.lines` rows of imprinted cells (default 5), so columns don't jump (see `Await`). An empty array is an empty table.
32
+
33
+ **Opening a row.** Read-only detail (`renderExpanded`) is a reveal of the record, so it stays on the ground: a full-width row under the record, inset, with the disclosure chevron turned and `aria-expanded`/`aria-controls` on its button. Editing (`renderEditor` + `onSave`) is live, so it rises: the row and its region become one raised slab (`elev-1`) holding a static `EditRegion`, with the row's cells still visible above it so the record stays identified. The first field takes focus; Enter in a single-line field saves and Escape cancels, and focus returns to the row's Edit button. While a row is in edit, the other rows' Edit buttons, sorting and reordering wait, so the edited row isn't carried away mid-edit; once saved it settles with an afterglow and, if its sort position changed, is carried to its new place. It stays a table, not a grid: the region is one `td` spanning the row. Above about eight fields, open the record in a `Sheet` beside the table instead (`onRowClick`).
34
+
35
+ **On a phone.** Under 480px of the frame's width, columns keep at least 6rem (or their `minWidth`) and the table scrolls inside its frame, fading the edge with more columns, instead of squeezing cells. `sticky: true` keeps a column in view over a flush 1px edge. Wide, nothing changes.
36
+
37
+ **You provide:** the data or the rows, short column headers in sentence case, numbers right-aligned (`align="end"`), and a `getRowLabel` that names each row the way people refer to it.
38
+
39
+ - Do filter in place with `relevance` when position matters (a ranked list, a schedule); remove rows instead when it doesn't.
40
+ - Don't make rows interactive unless clicking one does something; a lift that leads nowhere is a broken promise.
41
+ - Don't nest tables or put a table inside a raised card's padding without a reason; the table is already the record.
42
+
43
+ ## Columns and density
44
+
45
+ Wide record tables need a way to choose what shows. `columnsMenu` adds a **Columns** button to the toolbar (it says "· 3 hidden" when some are). It opens a `Popover` (level 3) holding a `SortableList` of `Checkbox`es (drag a column, or pick it up with Space and the arrows, to reorder it) and a density switch (a single-choice `ToggleGroup`: Comfortable or Compact). The first column in order can't be hidden: it names the row.
46
+
47
+ - `columnVisibility`, `onColumnVisibilityChange`: `{ [key]: false }` hides a column.
48
+ - `columnOrder`, `onColumnOrderChange`: keys in order; keys left out keep their place after the ones given.
49
+ - `density`, `onDensityChange`: `'comfortable'` (default) or `'compact'`. Compact rows keep every target at least 24px tall ([WCAG 2.5.8](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum)).
50
+
51
+ Each has an uncontrolled `default…` form. Showing, hiding or reordering a column doesn't animate the table: it's a change of layout, not of the record. Each change is announced ("Owner column hidden", "Compact rows").
52
+
53
+ ## Paging from the server
54
+
55
+ `pagination` puts paging in the table's footer, under a line:
56
+
57
+ - **Pages:** `{ page, pageCount, pageSize, total, onPageChange, hrefFor?, noun? }` renders `Pagination` with its range summary ("9–16 of 214 invoices"). Leave out `pageCount` for cursor paging (Previous, Next, `hasNext`).
58
+ - **Load more:** `{ mode: 'more', hasMore, onLoadMore, total?, noun? }` renders "6 of 46 events" and a Load more button. Loaded rows arrive with the afterglow (at most four begin at once), and "6 more loaded, 12 of 46" is announced.
59
+
60
+ **While the next page loads** (`loading: true`), the current rows stay on the ground at half strength (the reading a refetching chart uses), the table is `aria-busy`, and the page thumb is held pressed. Your app loads the rows (`useAsyncCollection` keeps selection across pages with `selection: 'preserve'`). After a page change, focus stays on the pagination control.
61
+
62
+ ## Selecting past the page
63
+
64
+ To bulk-act on a filtered set larger than a page ("Archive all 2,341 inactive users"), use the richer selection:
65
+
66
+ ```tsx
67
+ <DataTable … selectable selection={selection} onSelectionChange={setSelection} matchingTotal={2341} noun="inactive users"
68
+ actions={(ids, sel) => <Button onClick={() => archive(sel)}>Archive</Button>} actionsPlacement="auto" />
69
+ ```
70
+
71
+ - **A selection** is `{ kind: 'ids', ids }` or `{ kind: 'query', total, except }`. Actions get the loaded ids and the whole selection, so they can act on the *query*. It persists through filtering, sorting and paging, because it's yours.
72
+ - **Once the header checkbox selects a page**, a flush `accent-soft` row offers "Select all 2,341 matching inactive users". A query selection is said in words ("All 2,341 matching, except 1"), and unchecking a row adds it to the exceptions.
73
+ - **`actionsPlacement="floating"`**, or `auto` once the toolbar has scrolled away, docks the count and actions as **a bar floating at the bottom edge, level 3**: a selection is a set of records in your hand. It's a labelled region ("Selection"), and it rises into place.
74
+
75
+ `selected` and `onSelectedChange` keep working for selections of loaded rows. Report each record's outcome of a long bulk action in place (`RemovalReceipt`'s reading) and in a toast.
76
+
77
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
78
+ ## Example
79
+
80
+ The full preview, as rendered in Storybook and checked in CI (`col`, `filter`, `cap` are the preview page's own layout classes, not Altitude's):
81
+
82
+ ```tsx
83
+ import * as React from 'react';
84
+ import { Badge, Button, DataTable, Input } from 'altitude-design';
85
+
86
+ const data = [
87
+ { id: "nw", name: "Northwind", env: "Production", owner: "Mara Quist", updated: 2, req: 1240 },
88
+ { id: "bc", name: "Beacon", env: "Preview", owner: "Jon Reyes", updated: 5, req: 312 },
89
+ { id: "ci", name: "Cinder", env: "Production", owner: "Lena Park", updated: 26, req: 5906 },
90
+ { id: "dl", name: "Delta", env: "Staging", owner: "Sam Okafor", updated: 49, req: 88 },
91
+ { id: "em", name: "Ember", env: "Production", owner: "Ivy Chen", updated: 70, req: 2410 },
92
+ { id: "fb", name: "Fable", env: "Preview", owner: "Ada Obi", updated: 96, req: 19 },
93
+ { id: "gn", name: "Garnet", env: "Staging", owner: "Theo Marsh", updated: 120, req: 640 },
94
+ { id: "hb", name: "Harbor", env: "Production", owner: "Noa Levi", updated: 168, req: 3120 }
95
+ ];
96
+ const tone = { Production: "positive", Preview: "accent", Staging: "neutral" };
97
+ export default function App() {
98
+ const [rows, setRows] = React.useState(data);
99
+ const [q, setQ] = React.useState("");
100
+ const [opened, setOpened] = React.useState(null);
101
+ return <div className="col" style={{ gap: 8 }}>
102
+ <DataTable
103
+ aria-label="Projects"
104
+ rows={rows}
105
+ onReorder={setRows}
106
+ getRowId={(r) => r.id}
107
+ getRowLabel={(r) => r.name}
108
+ selectable={true}
109
+ defaultSelected={["bc", "ci"]}
110
+ maxHeight={300}
111
+ onRowClick={(r) => setOpened(r.name)}
112
+ relevance={(r) => q ? r.name.toLowerCase().includes(q.toLowerCase()) || r.owner.toLowerCase().includes(q.toLowerCase()) ? "match" : "miss" : void 0}
113
+ actions={() => <Button size="sm">Archive</Button>}
114
+ columnsMenu={true}
115
+ toolbar={<div className="filter">
116
+ <Input
117
+ size="sm"
118
+ placeholder="Filter in place"
119
+ value={q}
120
+ onChange={(e) => setQ(e.target.value)}
121
+ aria-label="Filter projects"
122
+ />
123
+ </div>}
124
+ columns={[
125
+ { key: "name", header: "Project", sortValue: (r) => r.name },
126
+ { key: "env", header: "Environment", cell: (r) => <Badge tone={tone[r.env]}>{r.env}</Badge>, sortValue: (r) => r.env },
127
+ { key: "owner", header: "Owner", sortValue: (r) => r.owner },
128
+ { key: "updated", header: "Updated", align: "end", sortValue: (r) => r.updated, cell: (r) => r.updated < 24 ? r.updated + "h ago" : Math.round(r.updated / 24) + "d ago" },
129
+ { key: "req", header: "Requests", align: "end", sortValue: (r) => r.req, cell: (r) => r.req.toLocaleString("en-US") }
130
+ ]}
131
+ />
132
+ <p className="cap">
133
+ {opened ? `Opened ${opened}.` : "Sort a column: rows travel to their places. Filter: matches rise, misses sink. Drag a handle, or focus it and press Space, then the arrow keys. Columns shows, hides and reorders columns, and sets the row density."}
134
+ </p>
135
+ </div>;
136
+ }
137
+ ```
138
+ <!-- /example -->