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
package/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  A design and HCI framework for agent-built React web applications, with a component foundation where **height is the interface**. Its one idea: the ground is the record. Settled content sits on the ground; anything live rises above it; anything waiting to receive sinks below it.
4
4
 
5
5
  - **Site:** https://richlaconte.github.io/altitude/
6
+ - **For agents:** https://richlaconte.github.io/altitude/llms.txt, an index of the guides, task patterns and every component's guide as plain Markdown ([llmstxt.org](https://llmstxt.org)), and `llms-full.txt` beside it with every guide in one file
6
7
  - **Storybook:** every component, with its guidelines and a live preview in both schemes: https://richlaconte.github.io/altitude/storybook/
7
8
  - **The design guide:** [`project/README.md`](https://github.com/richlaconte/altitude/blob/main/project/README.md), then [Elevation](https://github.com/richlaconte/altitude/blob/main/project/Elevation.md), [Motion](https://github.com/richlaconte/altitude/blob/main/project/Motion.md), [Charts](https://github.com/richlaconte/altitude/blob/main/project/Charts.md), [Theming](https://github.com/richlaconte/altitude/blob/main/project/Theming.md), [Fundamentals](https://github.com/richlaconte/altitude/blob/main/project/Fundamentals.md), [Distinctiveness](https://github.com/richlaconte/altitude/blob/main/project/Distinctiveness.md) and [Research](https://github.com/richlaconte/altitude/blob/main/project/Research.md)
8
9
 
@@ -117,6 +118,7 @@ npx altitude init --client codex # preview exact changes
117
118
  npx altitude init --client codex --apply # apply after reviewing the preview
118
119
  # Claude Code: --client claude. Both: --client all.
119
120
  npx altitude doctor --json
121
+ npx altitude check # Altitude's rules that are certain from your source
120
122
  ```
121
123
 
122
124
  Setup preserves existing instructions and adds a small versioned pointer to the installed guidance. It never runs on package installation. Preview removal with `npx altitude init --client codex --remove`, then add `--apply` to remove only its managed content. Edited blocks are preserved and require reconciliation. Codex uses a nonempty root `AGENTS.override.md` when present; otherwise `AGENTS.md`. Claude Code uses `CLAUDE.md`. Restart the client after setup. See [setup details](docs/framework/AGENT-SETUP.md).
@@ -1,10 +1,85 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // scripts/altitude-cli.mjs
4
- import { existsSync, lstatSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
4
+ import { existsSync, lstatSync, readFileSync as readFileSync2, writeFileSync, unlinkSync } from "node:fs";
5
5
  import { createRequire } from "node:module";
6
6
  import { createHash } from "node:crypto";
7
- import { resolve, dirname, relative, basename } from "node:path";
7
+ import { resolve, dirname, relative as relative2, basename } from "node:path";
8
+
9
+ // scripts/source-check.mjs
10
+ import { readdirSync, readFileSync, statSync } from "node:fs";
11
+ import { extname, join, relative } from "node:path";
12
+ var SOURCE = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js", ".mjs", ".css", ".scss", ".html"]);
13
+ var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", ".next", ".git", "coverage", "storybook-static"]);
14
+ var COLOUR = /#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b|\b(?:rgba?|hsla?|oklch|oklab|lab|lch)\(/;
15
+ var TAILWIND_COLOUR = /(?:^|[\s"'`:])(?:text|bg|border|ring|outline|fill|stroke|from|via|to|divide|placeholder|decoration|caret|accent)-(?:(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|[1-9]00|950)|white|black|\[(?:#|rgb|hsl|oklch))/;
16
+ function files(dir) {
17
+ const out = [];
18
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
19
+ if (SKIP.has(entry.name) || entry.name.startsWith(".")) continue;
20
+ const path = join(dir, entry.name);
21
+ if (entry.isDirectory()) out.push(...files(path));
22
+ else if (SOURCE.has(extname(entry.name)) && !/\.(test|spec|stories)\.[jt]sx?$|\.d\.ts$/.test(entry.name) && statSync(path).size < 1e6) out.push(path);
23
+ }
24
+ return out;
25
+ }
26
+ var ignored = (lines, i, id) => [lines[i], lines[i - 1] || ""].some((l) => new RegExp(`altitude-ignore:\\s*${id}\\b`).test(l));
27
+ var uncomment = (text) => text.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")).replace(/(^|[^:'"`])\/\/.*$/gm, "$1");
28
+ var RULES = {
29
+ "ALT-TOKEN-001": { kind: "deterministic", title: "Colours come from the tokens, not literals" },
30
+ "ALT-SETUP-002": { kind: "deterministic", title: "Altitude's stylesheet is imported" },
31
+ "ALT-SETUP-003": { kind: "deterministic", title: "The app is wrapped in AltitudeProvider" },
32
+ "ALT-CHART-001": { kind: "deterministic", title: "No pie or donut charts" },
33
+ "ALT-COPY-001": { kind: "deterministic", title: "Buttons name their outcome, never OK or Submit" },
34
+ "ALT-PRIMARY-001": { kind: "contextual-review", title: "One primary action per view" },
35
+ "ALT-VISUAL-004": { kind: "contextual-review", title: "Controls keep Altitude's elevation" }
36
+ };
37
+ function scan(root, { tokenNames = /* @__PURE__ */ new Set() } = {}) {
38
+ const findings = [];
39
+ const add = (id, file, line, observed, repair) => findings.push({ ruleId: id, kind: RULES[id].kind, status: RULES[id].kind === "deterministic" ? "fail" : "review", file: file && relative(root, file), line, observed, expected: RULES[id].title, repair, verification: `altitude explain ${id}` });
40
+ const sources = files(root);
41
+ let usesAltitude = false, stylesheet = false, providers = [];
42
+ for (const file of sources) {
43
+ const raw = readFileSync(file, "utf8");
44
+ const text = uncomment(raw);
45
+ const lines = text.split("\n"), rawLines = raw.split("\n");
46
+ const css = /\.(s?css|html)$/.test(file);
47
+ if (/from\s+['"]altitude-design['"]|require\(\s*['"]altitude-design['"]\s*\)/.test(text)) usesAltitude = true;
48
+ if (/['"]altitude-design\/altitude(\.no-fonts)?\.css['"]/.test(text)) stylesheet = true;
49
+ lines.forEach((line, i) => {
50
+ const at = i + 1;
51
+ const skip = (id) => ignored(rawLines, i, id);
52
+ if (COLOUR.test(line) && !skip("ALT-TOKEN-001")) {
53
+ const definesToken = /^\s*--[\w-]+\s*:/.test(line) || [...line.matchAll(/['"]?([a-z][\w-]*)['"]?\s*:\s*['"`]?(?:#|rgb|hsl|oklch)/g)].some((m) => tokenNames.has(m[1]));
54
+ const accent = /\baccent\s*=\s*\{?\s*['"`]#/.test(line);
55
+ const inStyle = css ? [...line.matchAll(/(?:^|[{;"'])\s*([a-z-]+)\s*:\s*([^;{}"']+)/g)].some((m) => !m[1].startsWith("--") && COLOUR.test(m[2])) : /\bstyle=\{\{|(?:color|background|backgroundColor|borderColor|fill|stroke|boxShadow|outlineColor)\s*[:=]/.test(line);
56
+ if (!definesToken && !accent && inStyle) add("ALT-TOKEN-001", file, at, line.trim().slice(0, 160), "Use a token instead: var(--ink-muted), var(--accent), var(--surface-raised) and the rest of tokens.css. A brand colour goes to AltitudeProvider accent, and a full palette to a custom scheme.");
57
+ } else if (!css && TAILWIND_COLOUR.test(line) && /\bclass(Name)?=/.test(line) && !skip("ALT-TOKEN-001")) {
58
+ add("ALT-TOKEN-001", file, at, line.trim().slice(0, 160), 'Use a token instead of a palette class: style={{ color: "var(--ink-muted)" }} or a class whose CSS uses the tokens.');
59
+ }
60
+ if (css) return;
61
+ if (!skip("ALT-CHART-001") && (/import\s*\{[^}]*\b(PieChart|Pie|Doughnut|RadialBarChart)\b[^}]*\}\s*from\s*['"](recharts|react-chartjs-2)['"]/.test(line) || /from\s*['"]@nivo\/pie['"]/.test(line) || /\btype\s*:\s*['"](pie|doughnut)['"]/.test(line)))
62
+ add("ALT-CHART-001", file, at, line.trim().slice(0, 160), 'Show a part of a whole as a stacked BarChart or a segmented Meter: angle and area are read less accurately than length (Charts.md, "Choosing a form").');
63
+ const copy = line.match(/<(?:\w+\.)?Button\b[^>]*>\s*(OK|Ok|Okay|Submit)\s*<\/(?:\w+\.)?Button>/);
64
+ if (copy && !skip("ALT-COPY-001")) add("ALT-COPY-001", file, at, copy[0].slice(0, 160), `Start with a verb and name the outcome: "Save changes", "Send invite", "Create project", not "${copy[1]}".`);
65
+ const lift = line.match(/<(?:\w+\.)?(Input|Textarea|Select|Combobox|Button|Switch|Checkbox)\b[^>]*(boxShadow|box-shadow|\bshadow-(?!none)[\w-]+|\btranslate-y-|\btransform\s*:)/);
66
+ if (lift && !skip("ALT-VISUAL-004")) add("ALT-VISUAL-004", file, at, line.trim().slice(0, 160), `Remove the override: a ${lift[1]}'s height is how it says whether you press it or fill it. Use the component's variant or size props instead.`);
67
+ });
68
+ if (!css) {
69
+ const n = (text.match(/<(?:\w+\.)?AltitudeProvider\b/g) || []).length;
70
+ if (n) providers.push(...Array(n).fill(relative(root, file)));
71
+ const primaries = [...text.matchAll(/<(?:\w+\.)?Button\b[^>]*\bvariant=["']primary["']/g)];
72
+ if (primaries.length > 1 && !ignored(rawLines, text.slice(0, primaries[1].index).split("\n").length - 1, "ALT-PRIMARY-001"))
73
+ add("ALT-PRIMARY-001", file, text.slice(0, primaries[1].index).split("\n").length, `${primaries.length} primary buttons in this file`, "If they can be on screen together, keep one primary (the one action the task leads to) and make the others secondary or ghost.");
74
+ }
75
+ }
76
+ if (usesAltitude && !stylesheet) add("ALT-SETUP-002", null, null, "No file imports altitude-design/altitude.css or altitude-design/altitude.no-fonts.css", "Import one of them once, at the app's entry: import 'altitude-design/altitude.css';");
77
+ if (usesAltitude && !providers.length) add("ALT-SETUP-003", null, null, "Components from altitude-design are used, but nothing renders <AltitudeProvider>", "Render one AltitudeProvider around the whole app. It sets the scheme and hosts dialogs and toasts.");
78
+ if (providers.length > 1) findings.push({ ruleId: "ALT-SETUP-003", kind: "contextual-review", status: "review", file: null, line: null, observed: `<AltitudeProvider> is rendered in ${providers.length} places (${[...new Set(providers)].join(", ")})`, expected: "One AltitudeProvider around the app", repair: "Keep one around the app unless the others are deliberate (separate roots, previews).", verification: "altitude explain ALT-SETUP-003" });
79
+ return { files: sources.length, findings };
80
+ }
81
+
82
+ // scripts/altitude-cli.mjs
8
83
  var sha = (value2) => createHash("sha256").update(value2).digest("hex");
9
84
  var args = process.argv.slice(2);
10
85
  var command = args.shift() || "help";
@@ -17,9 +92,9 @@ var json = args.includes("--json");
17
92
  var emit = (result) => console.log(JSON.stringify(result, null, 2));
18
93
  function installed() {
19
94
  const path = createRequire(resolve(cwd, "package.json")).resolve("altitude-design/package.json");
20
- const pkg = JSON.parse(readFileSync(path, "utf8"));
95
+ const pkg = JSON.parse(readFileSync2(path, "utf8"));
21
96
  const guidance = resolve(dirname(path), "dist/guidance");
22
- const index = JSON.parse(readFileSync(resolve(guidance, "index.json"), "utf8"));
97
+ const index = JSON.parse(readFileSync2(resolve(guidance, "index.json"), "utf8"));
23
98
  return { pkg, guidance, index };
24
99
  }
25
100
  function managed(text) {
@@ -31,15 +106,23 @@ function managed(text) {
31
106
  if (!m || sha(m[3]) !== m[2]) throw new Error("The managed Altitude block was edited; preserve your edits and reconcile it manually.");
32
107
  return { start: m.index, end: m.index + m[0].length, created: m[1] === "true", body: m[3] };
33
108
  }
34
- function block(version, guidance, created) {
35
- const path = relative(cwd, guidance).replaceAll("\\", "/");
36
- const body = `## Altitude framework (${version})
37
- For UI tasks using altitude-design, first read ${path}/README.md, then only the matching pattern document.
109
+ function block({ pkg, guidance, index }, created) {
110
+ const path = relative2(cwd, guidance).replaceAll("\\", "/");
111
+ const declarations = relative2(cwd, resolve(guidance, "..", "types")).replaceAll("\\", "/") + "/";
112
+ const groups = /* @__PURE__ */ new Map();
113
+ for (const c of index.components || []) groups.set(c.group, [...groups.get(c.group) || [], c.name]);
114
+ const components = groups.size ? `Component guides: ${path}/components/<Name>/README.md. Read one before using that component.
115
+ ` + [...groups].map(([g, names]) => `${g}|${names.join(",")}`).join("\n") + "\n" : "";
116
+ const body = `## Altitude framework (${pkg.version})
117
+ Altitude postdates your training data, and its props only look like shadcn's. Prefer these installed docs to what you remember, and check a component's props in its declaration under ${declarations} before using them.
118
+ For UI tasks using altitude-design, first read ${path}/README.md, then only the matching pattern document. Read ${path}/visual/README.md before composing a screen and ${path}/visual/Charts.md before any chart, stat or meter.
119
+ On every screen: one AltitudeProvider around the app and altitude-design/altitude.css imported once; inputs sunk and buttons raised, never the reverse; one primary action per view; no amount carried by height (charts are flat ink on a ground line: no shadows, 3D, pie or donut); status never colour alone (a word and an icon too); colours, shadows and radii from the tokens, never hard-coded.
38
120
  Discover tasks with npm exec -- altitude patterns "your intent"; explain a rule with npm exec -- altitude explain RULE-ID.
39
- Use the installed public declarations and examples. Record applicable states and application-owned persistence, permissions and navigation.
121
+ Record applicable states and application-owned persistence, permissions and navigation.
122
+ After changing UI, run npm exec -- altitude check and fix its errors; it reports Altitude's rules that are certain from source (hard-coded colours, the stylesheet and provider, pie charts, OK and Submit labels) and lists likely problems for review.
40
123
  Run the pattern checks and application service fixtures; distinguish deterministic defects, integration obligations and contextual review.
41
124
  Use npm exec -- altitude doctor --json after package upgrades, and rerun npm exec -- altitude init to refresh this versioned pointer.
42
- `;
125
+ ` + components;
43
126
  return `
44
127
  <!-- altitude:start created=${created} hash=${sha(body)} -->
45
128
  ${body}<!-- altitude:end -->
@@ -47,7 +130,7 @@ ${body}<!-- altitude:end -->
47
130
  }
48
131
  function instructionPath(client) {
49
132
  const override = resolve(cwd, "AGENTS.override.md");
50
- if (client === "codex" && existsSync(override) && readFileSync(override, "utf8").trim()) return override;
133
+ if (client === "codex" && existsSync(override) && readFileSync2(override, "utf8").trim()) return override;
51
134
  return resolve(cwd, client === "codex" ? "AGENTS.md" : "CLAUDE.md");
52
135
  }
53
136
  function isSymlink(path) {
@@ -67,9 +150,9 @@ function init(context) {
67
150
  const targets = clients.flatMap((client2) => (remove && client2 === "codex" ? [resolve(cwd, "AGENTS.md"), resolve(cwd, "AGENTS.override.md")] : [instructionPath(client2)]).map((path) => ({ client: client2, path })));
68
151
  const changes = targets.map(({ client: c, path }) => {
69
152
  if (isSymlink(path)) throw new Error(`Refusing instruction symlink: ${path}`);
70
- const existed = existsSync(path), before = existed ? readFileSync(path, "utf8") : "";
153
+ const existed = existsSync(path), before = existed ? readFileSync2(path, "utf8") : "";
71
154
  const old = managed(before);
72
- const replacement = remove ? "" : block(context.pkg.version, context.guidance, old?.created ?? !existed);
155
+ const replacement = remove ? "" : block(context, old?.created ?? !existed);
73
156
  const after = old ? before.slice(0, old.start) + replacement + before.slice(old.end) : before + (remove ? "" : replacement);
74
157
  const deleteFile = remove && old?.created && after === "";
75
158
  return { client: c, path, before, after, action: deleteFile ? "delete" : before === after ? "unchanged" : existed ? "update" : "create" };
@@ -86,35 +169,52 @@ function finding(ruleId, kind, status, observed, expected, repair, verification)
86
169
  function doctor(context) {
87
170
  const findings = [];
88
171
  if (context.pkg.version !== context.index.version) findings.push(finding("ALT-SETUP-001", "deterministic", "fail", `Package ${context.pkg.version}; guidance ${context.index.version}`, "Matching versions", "Reinstall a coherent package.", "altitude doctor --json"));
89
- const manifest = JSON.parse(readFileSync(resolve(context.guidance, "manifest.json"), "utf8"));
172
+ const manifest = JSON.parse(readFileSync2(resolve(context.guidance, "manifest.json"), "utf8"));
90
173
  if (manifest.version !== context.pkg.version) findings.push(finding("ALT-SETUP-001", "deterministic", "fail", `Manifest ${manifest.version}`, "Manifest matches installed package", "Reinstall the package.", "altitude doctor --json"));
91
174
  for (const [file, digest] of Object.entries(manifest.files)) {
92
175
  const path = resolve(context.guidance, file);
93
- if (!path.startsWith(context.guidance + "/") || !existsSync(path) || sha(readFileSync(path)) !== digest) findings.push(finding("ALT-SETUP-001", "deterministic", "fail", `Missing or modified guidance: ${file}`, "Unmodified version-matched guidance", "Reinstall the package.", "altitude doctor --json"));
176
+ if (!path.startsWith(context.guidance + "/") || !existsSync(path) || sha(readFileSync2(path)) !== digest) findings.push(finding("ALT-SETUP-001", "deterministic", "fail", `Missing or modified guidance: ${file}`, "Unmodified version-matched guidance", "Reinstall the package.", "altitude doctor --json"));
94
177
  }
95
178
  for (const client of ["codex", "claude"]) {
96
179
  const path = instructionPath(client), file = basename(path);
97
180
  try {
98
- const text = existsSync(path) ? readFileSync(path, "utf8") : "";
181
+ const text = existsSync(path) ? readFileSync2(path, "utf8") : "";
99
182
  const current = managed(text);
100
- if (current && text.slice(current.start, current.end) !== block(context.pkg.version, context.guidance, current.created)) findings.push(finding("ALT-SETUP-001", "deterministic", "fail", `${file} points to different guidance`, "Setup matches installed version and location", `altitude init --client ${client} --apply`, "altitude doctor --json"));
183
+ if (current && text.slice(current.start, current.end) !== block(context, current.created)) findings.push(finding("ALT-SETUP-001", "deterministic", "fail", `${file} points to different guidance`, "Setup matches installed version and location", `altitude init --client ${client} --apply`, "altitude doctor --json"));
101
184
  else findings.push(finding("ALT-SETUP-001", "deterministic", current ? "pass" : "not-configured", `${client}: ${current ? "managed pointer matches" : "no managed pointer"}`, "Explicit setup when agent integration is desired", `altitude init --client ${client}`, "Restart client and inspect loaded instructions."));
102
185
  } catch (e) {
103
186
  findings.push(finding("ALT-SETUP-001", "deterministic", "fail", `${file}: ${e.message}`, "One intact managed block", "Reconcile edited/duplicate markers manually.", "altitude doctor --json"));
104
187
  }
105
188
  }
106
- const rules = JSON.parse(readFileSync(resolve(context.guidance, "rules.json"), "utf8")).rules;
189
+ const rules = JSON.parse(readFileSync2(resolve(context.guidance, "rules.json"), "utf8")).rules;
107
190
  for (const r of rules.filter((r2) => r2.diagnosticClass !== "deterministic")) findings.push(finding(r.id, r.diagnosticClass, "unverified", "CLI has not exercised application behavior or conducted human review.", r.title, r.repair, r.verification));
108
191
  const ok = !findings.some((f) => f.kind === "deterministic" && f.status === "fail");
109
192
  emit({ command: "doctor", packageVersion: context.pkg.version, guidanceVersion: context.index.version, ok, scope: "Installation and managed pointers only; not accessibility or usability certification.", findings });
110
193
  if (!ok) process.exitCode = 1;
111
194
  }
195
+ function check(context) {
196
+ const target = resolve(cwd, args.find((v, i) => !v.startsWith("--") && args[i - 1] !== "--cwd") || (existsSync(resolve(cwd, "src")) ? "src" : "."));
197
+ if (!existsSync(target)) throw new Error(`No such directory: ${target}`);
198
+ const tokensCss = resolve(context.guidance, "..", "tokens.css");
199
+ const tokenNames = new Set(existsSync(tokensCss) ? [...readFileSync2(tokensCss, "utf8").matchAll(/--([\w-]+)\s*:/g)].map((m) => m[1]) : []);
200
+ const { files: files2, findings } = scan(target, { tokenNames });
201
+ const ok = !findings.some((f) => f.kind === "deterministic");
202
+ const scope = "Altitude rules certain from source text only. Not checked: anything that needs the page rendered (contrast, overflow, focus) or a person (accessibility, usability).";
203
+ if (json) emit({ command: "check", version: context.pkg.version, target: relative2(cwd, target) || ".", files: files2, ok, scope, findings });
204
+ else {
205
+ for (const f of findings) console.log(`${f.ruleId} ${f.kind === "deterministic" ? "error " : "review"} ${f.file ? `${f.file}${f.line ? ":" + f.line : ""}` : "(project)"} ${f.observed}
206
+ ${f.repair}`);
207
+ console.log(`${findings.filter((f) => f.kind === "deterministic").length} errors, ${findings.filter((f) => f.kind !== "deterministic").length} to review in ${files2} files. ${scope} Explain a rule: npm exec -- altitude explain RULE-ID`);
208
+ }
209
+ if (!ok) process.exitCode = 1;
210
+ }
112
211
  try {
113
- if (command === "help" || command === "--help") console.log("altitude init --client codex|claude|all [--apply] [--remove]\naltitude patterns [intent] [--json]\naltitude explain RULE-ID [--json]\naltitude doctor [--json]\nAll commands resolve the installed package from --cwd PATH (default current directory).");
212
+ if (command === "help" || command === "--help") console.log("altitude init --client codex|claude|all [--apply] [--remove]\naltitude patterns [intent] [--json]\naltitude explain RULE-ID [--json]\naltitude doctor [--json]\naltitude check [dir] [--json]\nAll commands resolve the installed package from --cwd PATH (default current directory).");
114
213
  else {
115
214
  const context = installed();
116
215
  if (command === "init") init(context);
117
216
  else if (command === "doctor") doctor(context);
217
+ else if (command === "check") check(context);
118
218
  else if (command === "patterns") {
119
219
  const words = args.filter((v, i) => !v.startsWith("--") && args[i - 1] !== "--cwd").join(" ").toLowerCase().split(/\W+/).filter(Boolean);
120
220
  const patterns = context.index.patterns.map((p) => ({ ...p, score: words.reduce((n, w) => n + (p.intents.some((i) => i === w || i.startsWith(w)) ? 1 : 0), 0) })).filter((p) => !words.length || p.score).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
@@ -123,7 +223,7 @@ try {
123
223
  ${resolve(context.guidance, p.document)}`).join("\n") : "No pattern matched. Run altitude patterns to browse the catalog.");
124
224
  } else if (command === "explain") {
125
225
  const id = args[0];
126
- const rule = JSON.parse(readFileSync(resolve(context.guidance, "rules.json"), "utf8")).rules.find((r) => r.id === id);
226
+ const rule = JSON.parse(readFileSync2(resolve(context.guidance, "rules.json"), "utf8")).rules.find((r) => r.id === id);
127
227
  if (!rule) throw new Error(`Unknown rule: ${id}`);
128
228
  emit({ version: context.pkg.version, ...rule });
129
229
  } else throw new Error(`Unknown command: ${command}`);
@@ -2,6 +2,8 @@
2
2
 
3
3
  Start from the person's task. Read only the relevant pattern below, its linked rules, and the public declarations. The installed copy matches the package version; `altitude doctor --json` checks that connection. Guidance supports judgment; it does not certify an application.
4
4
 
5
+ Every screen also follows the visual rules. Read the [design guide](visual/README.md) before composing one, [Elevation](visual/Elevation.md) for what height means, [Charts](visual/Charts.md) before drawing any chart, stat or meter, and [Motion](visual/Motion.md) before animating. Each component has its own guide (what it's for, how it uses height, what you provide); read it before using the component: for example [Button](components/Button/README.md) or [Select](components/Select/README.md). The `components` list in `index.json` has every component with its group, a one-line summary and its guide.
6
+
5
7
  Composing a new screen? Choose its frame from the task with [screen layout](patterns/screen-layout.md) first: name the task, the one thing that leads and the one primary action. The [app skeleton](app-skeleton.md) is one of its frames, for working through many similar records: shell, page header, a filtered and paged table, a record in a sheet, and settings, composed correctly.
6
8
 
7
9
  | Intent | Focused contract |
@@ -29,4 +31,6 @@ Use `altitude patterns "edit settings" --json` to discover contracts and `altitu
29
31
 
30
32
  Compose existing primitives and small workflow hooks from the main `altitude-design` export inside one `AltitudeProvider`. Import a package stylesheet. The application supplies routes, business validation and services. Verify delayed operations, rejection, retry, cancellation and stale responses where applicable; include keyboard and manual assistive-technology review. Automated checks cannot establish complete accessibility or task suitability.
31
33
 
34
+ After changing UI, run `npm exec -- altitude check` (or `altitude check src --json`). It reports Altitude's rules that are certain from source text as errors (hard-coded colours instead of tokens, a missing stylesheet or `AltitudeProvider`, pie and donut charts, buttons labelled OK or Submit) and likely problems for review (several primary buttons in one file, elevation overrides on controls). Fix the errors; decide on the reviews. Each finding names a rule `altitude explain` describes. It doesn't check anything that needs the page rendered or a person.
35
+
32
36
  For explicit agent setup, preview `npm exec -- altitude init --client codex` (or `claude`), then use `--apply`. Full setup and removal instructions are shipped as `agent-setup.md`, generated from the repository’s `docs/framework/AGENT-SETUP.md`. No installation hook writes project instructions.
@@ -9,14 +9,15 @@ npm exec -- altitude init --client claude --apply
9
9
  npm exec -- altitude patterns "edit settings" --json
10
10
  npm exec -- altitude explain ALT-EDIT-001
11
11
  npm exec -- altitude doctor --json
12
+ npm exec -- altitude check --json
12
13
  npm exec -- altitude init --client codex --remove
13
14
  npm exec -- altitude init --client codex --remove --apply
14
15
  ```
15
16
 
16
17
  `init` defaults to a preview, including the exact before/after text. `--apply` applies that preview. `--client all` targets both clients. Repeating setup makes no change. Removal removes only the managed block, restoring surrounding bytes; a file created by setup is removed only if nothing else remains. User-edited managed blocks, malformed markers and symlinks are rejected for manual reconciliation. Existing instructions outside the block are preserved. Run setup again after upgrading the installed package. Use `--cwd PATH` to target an explicit project directory. Resolution uses that directory's installed `altitude-design/package.json`, never the version fetched by a different CLI invocation.
17
18
 
18
- The Codex adapter writes a compact routing block to project `AGENTS.md` (or an existing nonempty `AGENTS.override.md`, which Codex gives precedence); the Claude Code adapter writes it to project `CLAUDE.md`. Neither assumes instruction files under `node_modules` are automatically discovered. The routing block points at version-matched local focused documents rather than importing the complete manual. Restart the agent session after setup. Confirm Codex reports the project instruction file and Claude Code's `/context` lists it under Memory files; then ask an ordinary product question and check that the relevant pattern is read. Instructions are context, not enforcement, and broader or more specific instructions may affect discovery.
19
+ The Codex adapter writes a compact routing block to project `AGENTS.md` (or an existing nonempty `AGENTS.override.md`, which Codex gives precedence); the Claude Code adapter writes it to project `CLAUDE.md`. Neither assumes instruction files under `node_modules` are automatically discovered. The routing block points at version-matched local focused documents rather than importing the complete manual. Because it's read on every turn, it carries only what an agent can't work out for itself: a reminder to prefer the installed docs and declarations to its memory, the rules that hold on every screen, and a compact index of every component (by group, with the path pattern of each component's guide), read on demand. The `agent-setup` check keeps it under 8 KB and verifies that every path it names is installed. Restart the agent session after setup. Confirm Codex reports the project instruction file and Claude Code's `/context` lists it under Memory files; then ask an ordinary product question and check that the relevant pattern is read. Instructions are context, not enforcement, and broader or more specific instructions may affect discovery.
19
20
 
20
21
  Mechanisms verified against official documentation on 2026-10-23: [Codex AGENTS.md](https://learn.chatgpt.com/docs/agent-configuration/agents-md) describes root-to-working-directory instruction discovery, override precedence and size limits; [Claude Code memory](https://code.claude.com/docs/en/memory) documents project `CLAUDE.md` loading and `/context` inspection. The adapter tests verify files and round trips, not successful behavior of every client release. Claude Code also documents AGENTS.md support; CLAUDE.md remains the explicit adapter target.
21
22
 
22
- `doctor` reports stable rule IDs, observed evidence, expected behavior, repairs and verification. It returns a failing exit code only for deterministic installation/setup defects; service integration and contextual obligations remain visibly unverified. It does not inspect an application's service behavior or certify accessibility. Guidance remains usable offline through `altitude-design/guidance`, `altitude-design/guidance/*`, and physical `node_modules/altitude-design/dist/guidance` files. Tooling stays outside browser imports.
23
+ `doctor` reports stable rule IDs, observed evidence, expected behavior, repairs and verification. It returns a failing exit code only for deterministic installation/setup defects; service integration and contextual obligations remain visibly unverified. It does not inspect an application's service behavior or certify accessibility. `check [dir]` (default `src`, else the project) scans an application's own source for Altitude's rules that are certain from text: hard-coded colours instead of tokens (`ALT-TOKEN-001`), the stylesheet and `AltitudeProvider` (`ALT-SETUP-002`, `ALT-SETUP-003`), pie and donut charts (`ALT-CHART-001`) and buttons labelled OK or Submit (`ALT-COPY-001`). Those fail the run. Several primary buttons in one file (`ALT-PRIMARY-001`) and elevation overrides on controls (`ALT-VISUAL-004`) are listed for review and never fail. A finding a team keeps is marked `altitude-ignore: <ID> <reason>` on its line or the line above. It reads text only: nothing that needs the page rendered or a person is checked. Guidance remains usable offline through `altitude-design/guidance`, `altitude-design/guidance/*`, and physical `node_modules/altitude-design/dist/guidance` files. Tooling stays outside browser imports.
@@ -0,0 +1,60 @@
1
+ # AILabel
2
+
3
+ Says when content was generated or changed by AI, and explains how when asked: for trust, and under transparency rules such as the EU AI Act. Carbon for AI made its label the entry point to explainability; this follows that, without the gradient.
4
+
5
+ **Elevation: a label is part of the record.** It's a flush `Badge` plate, a small static icon and a word: "AI", or **"Drafted with AI"** once a person has reviewed it.
6
+ - **No gradient, no glow, no shimmer.** A glow is light from below, and the light never moves. A gradient rim is also easily mistaken for focus.
7
+ - **It acts**, so it lifts one level when pointed at, like any plate that acts.
8
+ - **The explanation** is a `Popover` at level 3, "About this content": the model, when it was generated (`RelativeTime`), confidence in words, who reviewed or edited it, and the sources it was based on.
9
+ - **Provenance stays.** Accepting AI content doesn't remove its label; the plate changes to "Drafted with AI" and the explanation says who reviewed it.
10
+
11
+ **Accessibility.** A button with `aria-haspopup="dialog"` opening a non-modal popover. Its name says what it marks ("Summary generated with AI. Show details", or "drafted with AI and reviewed"). Tab, Enter and Escape. Under forced colours the plate has a border, and the icon and word carry the meaning. Under `contrast="more"` the plate's border is `line-control`.
12
+
13
+ **Props:** `subject` (what it marks, default "Content"); `model`; `generatedAt`; `sources` (`{ title, url? }[]`); `confidence`; `reviewedBy`; `editedBy`; `children` (anything else about how it was made).
14
+
15
+ **You provide:** the provenance. Place the label beside what it marks, such as a heading or a field label.
16
+
17
+ ```tsx
18
+ <h3>Summary <AILabel subject="Summary" model="Opus 5.5" generatedAt={summary.at}
19
+ sources={summary.sources} reviewedBy={summary.reviewer} confidence="Medium: two sources disagreed" /></h3>
20
+ ```
21
+
22
+ - Do keep the label once a person accepts the content.
23
+ - Do say confidence in words.
24
+ - Don't decorate AI content with gradients, glows or shimmer.
25
+ - Don't use the label as the only place AI involvement is disclosed when your rules require more.
26
+
27
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
28
+ ## Example
29
+
30
+ `AILabel` as first used in its preview:
31
+
32
+ ```tsx
33
+ import { AILabel } from 'altitude-design';
34
+
35
+ <AILabel subject="Summary" model="Opus 5.5" generatedAt={new Date(Date.now() - 4 * 60000)} confidence="Medium: two sources disagreed about the renewal date"
36
+ sources={[{ title: 'Call with Jamie Lee, 28 Oct', url: '#/calls/28' }, { title: 'Contract v3.pdf', url: '#/files/contract' }]} />
37
+ ```
38
+
39
+ The full preview, as rendered in Storybook and checked in CI (`col`, `al`, `ttl` are the preview page's own layout classes, not Altitude's):
40
+
41
+ ```tsx
42
+ import { AILabel } from 'altitude-design';
43
+
44
+ export default function App() {
45
+ return (
46
+ <div className="col al">
47
+ <div>
48
+ <h3 className="ttl">Summary <AILabel subject="Summary" model="Opus 5.5" generatedAt={new Date(Date.now() - 4 * 60000)} confidence="Medium: two sources disagreed about the renewal date"
49
+ sources={[{ title: 'Call with Jamie Lee, 28 Oct', url: '#/calls/28' }, { title: 'Contract v3.pdf', url: '#/files/contract' }]} /></h3>
50
+ <p>Jamie wants to move to annual billing in November and asked whether the 30-day refund applies to the remaining monthly period.</p>
51
+ </div>
52
+ <div>
53
+ <h3 className="ttl">Reply <AILabel subject="Reply" model="Opus 5.5" generatedAt={new Date(Date.now() - 3 * 3600000)} reviewedBy="Maya Chen" editedBy="Maya Chen" /></h3>
54
+ <p>Hi Jamie, thanks for getting in touch. Your full refund of €240 is on its way.</p>
55
+ </div>
56
+ </div>
57
+ );
58
+ }
59
+ ```
60
+ <!-- /example -->
@@ -0,0 +1,32 @@
1
+ # Accordion
2
+
3
+ Reveals sections one at a time. Closed items are **lids** lying flush on the page; opening one lifts its lid into a raised card and opens the **compartment** underneath, where the content sits.
4
+
5
+ Closed rows sit on the ground divided by `line` hairlines. Hovering one raises a ghost of its lid (a faint raised tint with a hairline), so you can see what will open. Opening raises the item to level 2 (`surface-raised`, `elev-2`), opens a gap around it and hides the neighbouring dividers. Inside, the content sits in a compartment: an opening sunk into the lid (`surface-well`) with its back wall showing just below the rim, the same camera as an empty `Input`. The panel grows on `ease-glide`, the content emerges 60ms later, and the chevron turns on `ease-settle`. Closed panels are inert to keyboard and assistive tech.
6
+
7
+ **You provide:** `items` (`value`, `title`, `content`), `type` (`single` closes the others; `multiple` doesn't), `defaultValue` as an array of open values.
8
+
9
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
10
+ ## Example
11
+
12
+ `Accordion` as first used in its preview:
13
+
14
+ ```tsx
15
+ import { Accordion } from 'altitude-design';
16
+
17
+ <Accordion items={items} defaultValue={['b']} />
18
+ ```
19
+
20
+ The full preview, as rendered in Storybook and checked in CI (`wrap` is the preview page's own layout class, not Altitude's):
21
+
22
+ ```tsx
23
+ import { Accordion } from 'altitude-design';
24
+
25
+ const items = [
26
+ { value: 'a', title: 'What does elevation mean here?', content: 'Height says how much something matters right now and what you can do with it: raised things are pushed, sunk things are filled.' },
27
+ { value: 'b', title: 'Can I bring my own colour?', content: 'Pass any hex as accent to AltitudeProvider. The accent text and soft tints are derived to stay legible on every surface.' },
28
+ { value: 'c', title: 'Does it respect reduced motion?', content: 'Yes. Surfaces still change level, tint and shadow, but nothing travels or scales.' },
29
+ ];
30
+ export default function App() { return <div className="wrap"><Accordion items={items} defaultValue={['b']} /></div>; }
31
+ ```
32
+ <!-- /example -->
@@ -0,0 +1,81 @@
1
+ # AgentFleet
2
+
3
+ Every agent working on one job, as a roster: grouped by what they need from you (needs you, working, done), each row saying the agent's state in words, what it's doing in one line, how long it has waited or run, where it is in its plan and what it has spent, with warnings for trouble that's coming (two agents changing one file, an agent making no progress). For supervising several agents at once; see [the research](https://github.com/richlaconte/altitude/blob/v0.4.1/docs/research/agent-orchestration/README.md).
4
+
5
+ **Elevation: a record of agents, with live rows.**
6
+ - **Rows** are the record: flush at rest, lifting one level when pointed at because selecting one opens it. The open agent is selected, as a table's row is: `accent-soft`, raised one level, `aria-current`.
7
+ - **Working** isn't pending (nothing you sent is in flight), so it never hovers: a plate with a steady accent lamp says "Working". No spinner.
8
+ - **Needs you** ("Needs approval", "Has a question", "Ready for review") is a caution plate with a lamp, and "waiting 6 min". It doesn't rise: a row can't lift into a card inside a list, and `AgentRequests` holds the raised card.
9
+ - **Stalled** ("No progress for 7 min", "Retried the same step 3 times") is a caution plate and a clock mark, in the working group: a suspicion, not a request.
10
+ - **Blocked** says on what ("Waiting on Billing API"); **queued** is a neutral plate. **Failed** is a critical plate and leads with the agents that need you, since nothing happens until someone retries or stops it.
11
+ - **Done** is stamped: a check, the name in muted ink, "took 9 min". The done group folds away.
12
+ - **A conflict** is a flush `caution-soft` strip under the row naming the file and the other agent: never colour alone.
13
+ - **Agents an agent started** hang under it in a 1px `line-control` bracket, as `TraceParallel` draws subagents.
14
+
15
+ **Accessibility.** A region named by `aria-label` ("Agents"). Each group is a list named with its count ("Needs you, 2"); a collapsible group's heading is a button with `aria-expanded`. Each row is a button whose name says everything in order: "Billing API, waiting 6 min, Wants to run the migration on staging, Needs approval, Step 3 of 4, spent $0.38". Up and Down move between rows across groups; Home and End go to the first and last. An agent that starts needing you (or fails) is announced politely once; starting, progress, stalling and finishing aren't.
16
+
17
+ **Parts:** `AgentFleet` (`aria-label`, `now`, `summary`); `AgentFleetGroup` (`label`, `count`, `collapsible`, `defaultOpen`, `empty`); `AgentFleetItem` (`name`, `state`: `'queued' | 'working' | 'needs-input' | 'blocked' | 'stalled' | 'failed' | 'done' | 'stopped'`, `needs`: `'approval' | 'question' | 'review'`, `activity`, `since`, `startedAt`, `endedAt`, `step`, `blockedBy`, `conflict`, `stalledReason`, `cost`, `output`, `selected`, `onSelect`, `children`). Helpers: `groupAgents(agents, get)` sorts into needs you (longest wait first), working (stalled first) and finished (newest first); `summarizeFleet(states)` writes "2 need you · 3 working · 4 done"; `formatSpan(ms)` writes "6 min".
18
+
19
+ **You provide:** the agents and their states, a one-line `activity` for each (the agent's last status, or a summary from a small model), when each entered its state, and what selecting does (open it beside the fleet). Detecting a stall or a conflict is yours too: pass `state="stalled"` when an agent has made no progress for a while or repeats a step, and `conflict` when two agents change the same file.
20
+
21
+ ```tsx
22
+ const g = groupAgents(agents, (a) => a);
23
+ const row = (a) => <AgentFleetItem key={a.id} {...a} selected={a.id === open} onSelect={() => setOpen(a.id)} />;
24
+ <AgentFleet summary={summarizeFleet(agents.map((a) => a.state))}>
25
+ <AgentFleetGroup label="Needs you" empty="Nothing needs you.">{g.needsYou.map(row)}</AgentFleetGroup>
26
+ <AgentFleetGroup label="Working">{g.working.map(row)}</AgentFleetGroup>
27
+ <AgentFleetGroup label="Done" collapsible defaultOpen={false}>{g.finished.map(row)}</AgentFleetGroup>
28
+ </AgentFleet>
29
+ ```
30
+
31
+ - Do sort by need: the agent waiting longest on you goes first.
32
+ - Do say every state in words; the lamp and mark only repeat it.
33
+ - Don't show a spinner, a ticking counter or make a working row hover.
34
+ - Don't lay agents out as a grid of equal cards: the one that needs you gets lost among the ones that don't.
35
+
36
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
37
+ ## Example
38
+
39
+ `AgentFleet` as first used in its preview:
40
+
41
+ ```tsx
42
+ import { AgentFleet, AgentFleetGroup, summarizeFleet } from 'altitude-design';
43
+
44
+ <AgentFleet now={now} summary={summarizeFleet(seed.map((a) => a.state))}>
45
+ <AgentFleetGroup label="Needs you" empty="Nothing needs you.">{g.needsYou.map(row)}</AgentFleetGroup>
46
+ <AgentFleetGroup label="Working">{g.working.map(row)}</AgentFleetGroup>
47
+ <AgentFleetGroup label="Done" collapsible>{g.finished.map(row)}</AgentFleetGroup>
48
+ </AgentFleet>
49
+ ```
50
+
51
+ The full preview, as rendered in Storybook and checked in CI (`af` is the preview page's own layout class, not Altitude's):
52
+
53
+ ```tsx
54
+ import * as React from 'react';
55
+ import { AgentFleet, AgentFleetGroup, AgentFleetItem, groupAgents, summarizeFleet } from 'altitude-design';
56
+
57
+ const now = Date.UTC(2026, 9, 31, 14, 20), min = 60000;
58
+ const seed = [
59
+ { id: 'api', name: 'Billing API', state: 'needs-input', needs: 'approval', activity: 'Wants to run the migration on staging', since: now - 6 * min, startedAt: now - 17 * min, step: { current: 3, total: 4 }, cost: '$0.38' },
60
+ { id: 'qa', name: 'QA', state: 'failed', activity: '2 of 14 checkout tests failed', since: now - 2 * min, startedAt: now - 5 * min, cost: '$0.11' },
61
+ { id: 'docs', name: 'Docs', state: 'stalled', stalledReason: 'Retried the same step 3 times', activity: 'Link check timed out on /enterprise; retrying', since: now - 5 * min, startedAt: now - 17 * min, step: { current: 3, total: 3 }, cost: '$0.31' },
62
+ { id: 'pricing', name: 'Pricing page', state: 'working', activity: 'Writing the annual toggle in Pricing.tsx', startedAt: now - 17 * min, step: { current: 2, total: 4 }, cost: '$0.41', conflict: { file: 'src/content/pricing.ts', with: 'Docs' } },
63
+ { id: 'changelog', name: 'Changelog', state: 'blocked', blockedBy: 'Pricing page' },
64
+ { id: 'emails', name: 'Renewal emails', state: 'done', activity: 'Scheduled for Tuesday 9:00 to 1,240 customers', startedAt: now - 17 * min, endedAt: now - 3 * min, cost: '$0.29' },
65
+ ];
66
+ export default function App() {
67
+ const [open, setOpen] = React.useState('pricing');
68
+ const g = groupAgents(seed, (a) => a);
69
+ const row = (a) => <AgentFleetItem key={a.id} {...a} selected={a.id === open} onSelect={() => setOpen(a.id)} />;
70
+ return (
71
+ <div className="af">
72
+ <AgentFleet now={now} summary={summarizeFleet(seed.map((a) => a.state))}>
73
+ <AgentFleetGroup label="Needs you" empty="Nothing needs you.">{g.needsYou.map(row)}</AgentFleetGroup>
74
+ <AgentFleetGroup label="Working">{g.working.map(row)}</AgentFleetGroup>
75
+ <AgentFleetGroup label="Done" collapsible>{g.finished.map(row)}</AgentFleetGroup>
76
+ </AgentFleet>
77
+ </div>
78
+ );
79
+ }
80
+ ```
81
+ <!-- /example -->
@@ -0,0 +1,53 @@
1
+ # AgentLanes
2
+
3
+ Where each agent's time went, one lane per agent on a shared clock: working, waiting on you, waiting on another agent, failed. Its takeaway leads with how long agents have waited on you, the bottleneck a fleet of agents hides: once several run, the person is usually the slowest part.
4
+
5
+ **Elevation: a chart, so nothing has height** (see [Charts](../../visual/Charts.md)).
6
+ - **Stretches** are equal-height flat bars lying on each lane's ground line: working is `data-1`; waiting on you is `caution` hatched at 45°; waiting on another agent is a 1px dashed outline; failed is `critical`. A gap is time the agent wasn't running.
7
+ - **Every fill is named** in the key, in words, and each bar's title says its agent, state and span.
8
+ - **"Now"** is a 1px `line-control` rule at the end of the record; the axis says "31 min ago", "15 min ago", "Now".
9
+ - **The selected lane** is its name in `accent-ink` and 650, with `aria-pressed`; lanes never lift.
10
+ - **Forced colours**: working solid, waiting hatched, blocked dashed, failed cross-hatched with a thick border.
11
+
12
+ **Accessibility.** A `figure` captioned by its takeaway. The lane names are buttons when `onSelect` is set; the bars and axis are hidden from assistive technology and a table gives the same data (agent; time working, waiting on you, waiting on another agent, failed).
13
+
14
+ **Props:** `lanes` (`{ id, name, segments: { from, to?, state }[] }[]`; a segment without `to` runs to now), `start` (default the earliest segment), `now`, `takeaway` (default "Agents have waited on you for 15 min in all; the longest wait was Pricing page, 5 min."), `aria-label`, `selected`, `onSelect`. Time spent failed counts as waiting on you in the takeaway, since a failed agent waits for a retry.
15
+
16
+ ```tsx
17
+ <AgentLanes start={job.startedAt} lanes={agents.map((a) => ({ id: a.id, name: a.name, segments: a.history }))} selected={open} onSelect={setOpen} />
18
+ ```
19
+
20
+ - Do keep it below the roster: it explains the past, the roster and requests are for now.
21
+ - Don't draw dependencies as arrows between lanes; say them on the waiting agent ("Waiting on Billing API").
22
+
23
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
24
+ ## Example
25
+
26
+ `AgentLanes` as first used in its preview:
27
+
28
+ ```tsx
29
+ import { AgentLanes } from 'altitude-design';
30
+
31
+ <AgentLanes lanes={lanes} start={t0} now={m(31)} selected={sel} onSelect={setSel} />
32
+ ```
33
+
34
+ The full preview, as rendered in Storybook and checked in CI (`al` is the preview page's own layout class, not Altitude's):
35
+
36
+ ```tsx
37
+ import * as React from 'react';
38
+ import { AgentLanes } from 'altitude-design';
39
+
40
+ const t0 = Date.UTC(2026, 9, 31, 14, 0), m = (n) => t0 + n * 60000;
41
+ const lanes = [
42
+ { id: 'pricing', name: 'Pricing page', segments: [{ from: m(3), to: m(20), state: 'working' }, { from: m(20), to: m(25), state: 'waiting' }] },
43
+ { id: 'api', name: 'Billing API', segments: [{ from: m(3), to: m(7), state: 'working' }, { from: m(7), to: m(10), state: 'waiting' }, { from: m(10), to: m(19), state: 'working' }] },
44
+ { id: 'emails', name: 'Renewal emails', segments: [{ from: m(3), to: m(8), state: 'working' }, { from: m(8), to: m(13), state: 'waiting' }, { from: m(13), to: m(25), state: 'working' }] },
45
+ { id: 'qa', name: 'QA', segments: [{ from: m(3), to: m(19), state: 'blocked' }, { from: m(19), to: m(21), state: 'working' }, { from: m(21), to: m(23), state: 'failed' }, { from: m(23), to: m(29), state: 'working' }] },
46
+ { id: 'changelog', name: 'Changelog', segments: [{ from: m(27), to: m(30), state: 'working' }] },
47
+ ];
48
+ export default function App() {
49
+ const [sel, setSel] = React.useState('qa');
50
+ return <div className="al"><AgentLanes lanes={lanes} start={t0} now={m(31)} selected={sel} onSelect={setSel} /></div>;
51
+ }
52
+ ```
53
+ <!-- /example -->
@@ -0,0 +1,76 @@
1
+ # AgentRequests
2
+
3
+ Every decision your agents are waiting on, in one queue: approvals, questions, reviews and failures, oldest first, each answerable where it is. Beside an `AgentFleet`, it lets a person supervising several agents clear what's waiting in a row, without opening each agent.
4
+
5
+ **Elevation: status that asks for action.**
6
+ - **A request** is a raised card (level 1), because it asks for action; it arrives lifted and settles with the afterglow. It isn't pending: nothing has been dispatched until it's answered.
7
+ - **Its kind** is a plate in words: "Needs approval", "Has a question", "Ready for review" (caution, with a lamp) or "Couldn't finish" (critical). Who's asking and "waiting 6 min" sit beside it.
8
+ - **Its answers** are secondary and ghost buttons: the view keeps its one primary. Open is a ghost at the end.
9
+ - **Nothing waiting** is said flush with a check ("Nothing needs you"): an all-clear is settled, not an opening.
10
+ - **The count** beside the heading is the one part that floats, as any count that wants you does.
11
+
12
+ **Accessibility.** A section named by its heading ("Needs you"). Requests are an ordered list, longest wait first; each is an `article` named by its ask and described by its kind, agent and wait. Open is named for the agent ("Open Billing API"). A request that arrives is announced politely once ("Billing API: Run the migration on staging?").
13
+
14
+ **Parts:** `AgentRequests` (`title`, `now`, `empty`); `AgentRequest` (`agent`, `kind`: `'approval' | 'question' | 'review' | 'failed'`, `title`, `detail`, `since`, `actions`, `onOpen`).
15
+
16
+ **You provide:** the requests (give each a stable `key`), what each answer does, and removing a request once it's answered. For a question with many options or your own answer, or an approval with scopes or edits, use Open and answer in the agent's own `Question` or `Confirmation`.
17
+
18
+ ```tsx
19
+ <AgentRequests>
20
+ {waiting.map((r) => (
21
+ <AgentRequest key={r.id} agent={r.agent} kind={r.kind} since={r.since} title={r.title} detail={r.consequence} onOpen={() => open(r.agentId)}
22
+ actions={<><Button size="sm" variant="secondary" onClick={() => approve(r)}>Run migration</Button><Button size="sm" variant="ghost" onClick={() => reject(r)}>Don't run</Button></>} />
23
+ ))}
24
+ </AgentRequests>
25
+ ```
26
+
27
+ - Do say the consequence of an approval in `detail`.
28
+ - Don't offer "approve all": answer each with its consequence in view.
29
+ - Don't make an answer primary; the view's one primary stays where it is.
30
+
31
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
32
+ ## Example
33
+
34
+ `AgentRequests` as first used in its preview:
35
+
36
+ ```tsx
37
+ import { AgentRequest, AgentRequests, Button } from 'altitude-design';
38
+
39
+ <AgentRequests now={now}>
40
+ {left.map((r) => (
41
+ <AgentRequest key={r.id} agent={r.agent} kind={r.kind} since={r.since} title={r.title} detail={r.detail} onOpen={() => {}}
42
+ actions={<>{r.answers.map((a) => <Button key={a} size="sm" variant="secondary" onClick={() => done(r.id)}>{a}</Button>)}
43
+ {r.decline && <Button size="sm" variant="ghost" onClick={() => done(r.id)}>{r.decline}</Button>}</>} />
44
+ ))}
45
+ </AgentRequests>
46
+ ```
47
+
48
+ The full preview, as rendered in Storybook and checked in CI (`ar` is the preview page's own layout class, not Altitude's):
49
+
50
+ ```tsx
51
+ import * as React from 'react';
52
+ import { AgentRequest, AgentRequests, Button } from 'altitude-design';
53
+
54
+ const now = Date.UTC(2026, 9, 31, 14, 20), min = 60000;
55
+ const seed = [
56
+ { id: 'emails', agent: 'Renewal emails', kind: 'question', since: now - 4 * min, title: 'Send the offer to every monthly customer, or only the active ones?', answers: ['Active only', 'Everyone'] },
57
+ { id: 'api', agent: 'Billing API', kind: 'approval', since: now - 9 * min, title: 'Run the plans-table migration on staging?', detail: 'Adds annual price columns to 4 plans on staging. Production isn\'t touched.', answers: ['Run migration'], decline: "Don't run" },
58
+ { id: 'qa', agent: 'QA', kind: 'failed', since: now - 1 * min, title: 'Checkout tests failed', detail: '2 of 14 failed: the annual total shows $290/yr, expected $290/year.', answers: ['Retry'] },
59
+ ];
60
+ export default function App() {
61
+ const [left, setLeft] = React.useState(seed);
62
+ const done = (id) => setLeft((l) => l.filter((r) => r.id !== id));
63
+ return (
64
+ <div className="ar">
65
+ <AgentRequests now={now}>
66
+ {left.map((r) => (
67
+ <AgentRequest key={r.id} agent={r.agent} kind={r.kind} since={r.since} title={r.title} detail={r.detail} onOpen={() => {}}
68
+ actions={<>{r.answers.map((a) => <Button key={a} size="sm" variant="secondary" onClick={() => done(r.id)}>{a}</Button>)}
69
+ {r.decline && <Button size="sm" variant="ghost" onClick={() => done(r.id)}>{r.decline}</Button>}</>} />
70
+ ))}
71
+ </AgentRequests>
72
+ </div>
73
+ );
74
+ }
75
+ ```
76
+ <!-- /example -->