altitude-design 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +1 -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 +25 -0
  6. package/dist/guidance/components/Accordion/README.md +7 -0
  7. package/dist/guidance/components/AgentFleet/README.md +34 -0
  8. package/dist/guidance/components/AgentLanes/README.md +21 -0
  9. package/dist/guidance/components/AgentRequests/README.md +29 -0
  10. package/dist/guidance/components/Alert/README.md +22 -0
  11. package/dist/guidance/components/AltitudeProvider/README.md +48 -0
  12. package/dist/guidance/components/AppShell/README.md +51 -0
  13. package/dist/guidance/components/AreaChart/README.md +43 -0
  14. package/dist/guidance/components/Artifact/README.md +30 -0
  15. package/dist/guidance/components/Avatar/README.md +34 -0
  16. package/dist/guidance/components/Await/README.md +80 -0
  17. package/dist/guidance/components/Badge/README.md +22 -0
  18. package/dist/guidance/components/BarChart/README.md +47 -0
  19. package/dist/guidance/components/BarList/README.md +29 -0
  20. package/dist/guidance/components/Board/README.md +31 -0
  21. package/dist/guidance/components/Breadcrumb/README.md +35 -0
  22. package/dist/guidance/components/Button/README.md +17 -0
  23. package/dist/guidance/components/Card/README.md +14 -0
  24. package/dist/guidance/components/ChartBrush/README.md +41 -0
  25. package/dist/guidance/components/ChartContainer/README.md +152 -0
  26. package/dist/guidance/components/ChartLegend/README.md +35 -0
  27. package/dist/guidance/components/ChartReference/README.md +41 -0
  28. package/dist/guidance/components/ChartTooltip/README.md +38 -0
  29. package/dist/guidance/components/Checkbox/README.md +11 -0
  30. package/dist/guidance/components/Checklist/README.md +71 -0
  31. package/dist/guidance/components/CodeBlock/README.md +28 -0
  32. package/dist/guidance/components/ColorPicker/README.md +30 -0
  33. package/dist/guidance/components/Combobox/README.md +16 -0
  34. package/dist/guidance/components/CommandPalette/README.md +27 -0
  35. package/dist/guidance/components/CommentThread/README.md +30 -0
  36. package/dist/guidance/components/Confirmation/README.md +34 -0
  37. package/dist/guidance/components/ContextMenu/README.md +30 -0
  38. package/dist/guidance/components/Conversation/README.md +45 -0
  39. package/dist/guidance/components/CopyButton/README.md +23 -0
  40. package/dist/guidance/components/DataGrid/README.md +82 -0
  41. package/dist/guidance/components/DatePicker/README.md +67 -0
  42. package/dist/guidance/components/Dialog/README.md +29 -0
  43. package/dist/guidance/components/DiffView/README.md +30 -0
  44. package/dist/guidance/components/DotPlot/README.md +38 -0
  45. package/dist/guidance/components/Drilldown/README.md +15 -0
  46. package/dist/guidance/components/DropZone/README.md +21 -0
  47. package/dist/guidance/components/DropdownMenu/README.md +19 -0
  48. package/dist/guidance/components/EditRegion/README.md +104 -0
  49. package/dist/guidance/components/EmptyState/README.md +17 -0
  50. package/dist/guidance/components/FeatureGate/README.md +28 -0
  51. package/dist/guidance/components/Field/README.md +10 -0
  52. package/dist/guidance/components/FilterBar/README.md +69 -0
  53. package/dist/guidance/components/FormattedNumber/README.md +37 -0
  54. package/dist/guidance/components/Heatmap/README.md +45 -0
  55. package/dist/guidance/components/HoldButton/README.md +16 -0
  56. package/dist/guidance/components/HoverCard/README.md +18 -0
  57. package/dist/guidance/components/InlineCitation/README.md +24 -0
  58. package/dist/guidance/components/InlineEdit/README.md +133 -0
  59. package/dist/guidance/components/Input/README.md +27 -0
  60. package/dist/guidance/components/InputOTP/README.md +30 -0
  61. package/dist/guidance/components/ItemGroup/README.md +93 -0
  62. package/dist/guidance/components/Kbd/README.md +9 -0
  63. package/dist/guidance/components/LineChart/README.md +47 -0
  64. package/dist/guidance/components/Link/README.md +28 -0
  65. package/dist/guidance/components/Markdown/README.md +32 -0
  66. package/dist/guidance/components/Meter/README.md +29 -0
  67. package/dist/guidance/components/NotificationCenter/README.md +28 -0
  68. package/dist/guidance/components/NumberInput/README.md +29 -0
  69. package/dist/guidance/components/PageHeader/README.md +29 -0
  70. package/dist/guidance/components/Pagination/README.md +25 -0
  71. package/dist/guidance/components/PasswordInput/README.md +34 -0
  72. package/dist/guidance/components/PermissionMatrix/README.md +28 -0
  73. package/dist/guidance/components/Pile/README.md +12 -0
  74. package/dist/guidance/components/PlanPicker/README.md +25 -0
  75. package/dist/guidance/components/Popover/README.md +12 -0
  76. package/dist/guidance/components/Progress/README.md +10 -0
  77. package/dist/guidance/components/PromptInput/README.md +40 -0
  78. package/dist/guidance/components/PropertyList/README.md +57 -0
  79. package/dist/guidance/components/Question/README.md +27 -0
  80. package/dist/guidance/components/RadioGroup/README.md +10 -0
  81. package/dist/guidance/components/RelativeTime/README.md +26 -0
  82. package/dist/guidance/components/RemovalReceipt/README.md +34 -0
  83. package/dist/guidance/components/Resizable/README.md +36 -0
  84. package/dist/guidance/components/SaveBar/README.md +27 -0
  85. package/dist/guidance/components/SavedViews/README.md +24 -0
  86. package/dist/guidance/components/ScatterChart/README.md +41 -0
  87. package/dist/guidance/components/Schedule/README.md +76 -0
  88. package/dist/guidance/components/ScrollArea/README.md +7 -0
  89. package/dist/guidance/components/SecretField/README.md +28 -0
  90. package/dist/guidance/components/Select/README.md +27 -0
  91. package/dist/guidance/components/Separator/README.md +26 -0
  92. package/dist/guidance/components/Sheet/README.md +32 -0
  93. package/dist/guidance/components/Skeleton/README.md +14 -0
  94. package/dist/guidance/components/Slider/README.md +25 -0
  95. package/dist/guidance/components/SortableList/README.md +11 -0
  96. package/dist/guidance/components/Sparkline/README.md +29 -0
  97. package/dist/guidance/components/SplitButton/README.md +26 -0
  98. package/dist/guidance/components/Stat/README.md +31 -0
  99. package/dist/guidance/components/Stepper/README.md +29 -0
  100. package/dist/guidance/components/Suggestions/README.md +29 -0
  101. package/dist/guidance/components/Surface/README.md +20 -0
  102. package/dist/guidance/components/Switch/README.md +9 -0
  103. package/dist/guidance/components/SyncIndicator/README.md +24 -0
  104. package/dist/guidance/components/Table/README.md +76 -0
  105. package/dist/guidance/components/Tabs/README.md +18 -0
  106. package/dist/guidance/components/TagInput/README.md +66 -0
  107. package/dist/guidance/components/Terminal/README.md +27 -0
  108. package/dist/guidance/components/ThreadList/README.md +37 -0
  109. package/dist/guidance/components/TimeInput/README.md +26 -0
  110. package/dist/guidance/components/Timeline/README.md +32 -0
  111. package/dist/guidance/components/Toaster/README.md +16 -0
  112. package/dist/guidance/components/Toggle/README.md +33 -0
  113. package/dist/guidance/components/Toolbar/README.md +37 -0
  114. package/dist/guidance/components/Tooltip/README.md +12 -0
  115. package/dist/guidance/components/Tour/README.md +29 -0
  116. package/dist/guidance/components/Trace/README.md +37 -0
  117. package/dist/guidance/components/Tree/README.md +86 -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/index.cjs.map +1 -1
  137. package/dist/types/index.d.ts +1 -1
  138. package/dist/types-cjs/index.d.cts +1 -1
  139. package/package.json +1 -1
package/README.md CHANGED
@@ -117,6 +117,7 @@ npx altitude init --client codex # preview exact changes
117
117
  npx altitude init --client codex --apply # apply after reviewing the preview
118
118
  # Claude Code: --client claude. Both: --client all.
119
119
  npx altitude doctor --json
120
+ npx altitude check # Altitude's rules that are certain from your source
120
121
  ```
121
122
 
122
123
  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,25 @@
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.
@@ -0,0 +1,7 @@
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.
@@ -0,0 +1,34 @@
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.0/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.
@@ -0,0 +1,21 @@
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").
@@ -0,0 +1,29 @@
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.
@@ -0,0 +1,22 @@
1
+ # Alert
2
+
3
+ Tells the person something about the current context. An alert is **a sheet laid on the page**: it rests on the ground, you can see its paper edge and the contact shadow under it, and it never floats. When it asks the person to act it becomes a raised card.
4
+
5
+ **Page banners.** `placement="page"` lays the alert edge to edge across the top of the app or page: "Your trial ends in 3 days", "Scheduled maintenance Sunday 02:00 UTC", "We're seeing delays in email delivery". It has no side rounding and no side edges, and its text wraps on narrow screens. The elevation rule is the same at page scope: one that informs is a flat sheet, and one that asks for action is raised. `sticky` keeps it at the top while the page scrolls. It sits in a `region` named by its title, so it can be found and skipped. `dismissLabel` gives the dismiss button words ("Remind me tomorrow"), and dismissing moves focus to the page's `main`. Several page banners go in `AlertStack placement="page"`, most severe first. Put a banner above `AppShell`, or at the top of the page.
6
+
7
+ - `variant="flat"` (default): a thin sheet in the tone's `-soft` colour (neutral is `surface-raised`), with a tone-tinted hairline, a 1px paper edge and a contact shadow. It informs.
8
+ - `variant="raised"`: a card at level 2 with the sheen of something you can act on, the icon in the tone colour. Use it when the alert has an `action` or blocks progress.
9
+ - `onDismiss`: adds a dismiss button. Dismissing **lifts the sheet off the page**: it rises 8px as its shadow spreads, fades, and then the gap closes; `onDismiss` is called once it's gone.
10
+ - `settle`: an afterglow when the alert first appears or changes.
11
+
12
+ `critical` renders `role="alert"`; the others use `role="status"`.
13
+
14
+ ## AlertStack
15
+
16
+ Several alerts as a **stack of sheets**, the same language as `Pile`. The first alert lies on top; up to two more peek out beneath it, each tinted by its own tone, so you can see what kind of news is waiting without reading it. A floating "+N more" sits on the stack's edge. Opening it fans the alerts out into a column, one after another; "Show less" tucks them back. Dismiss the top alert and it lifts off while the next one comes up from under it.
17
+
18
+ **You provide:** for `Alert`, `tone`, `title`, optional body as children, optional `action` (a small Button), `variant`, `onDismiss`. For `AlertStack`, keyed `Alert` children in order of importance, and optionally `expanded`/`onExpandedChange`, `moreLabel`, `lessLabel`.
19
+
20
+ - Keep it to a title and one sentence.
21
+ - Generic action-completion messages belong in a Toast. When an action removes an item from a list (‘Brand guidelines.pdf archived’ with Undo), don't add an Alert below the list: show the result in the item's own slot with [RemovalReceipt](../RemovalReceipt/README.md), then hand Undo to a Toast. Keep a durable recovery destination such as Archived → Restore; a timed notification must not be the only recovery path. See [Actions in place](../../visual/ActionsInPlace.md).
22
+ - An Alert still suits a failure or a changed record that can't be shown beside its item (it's outside the current view). Key it by its operation ID, and let its delayed `onDismiss` clear state only when that ID is still current. Dismissing an old Alert must not hide a newer action's feedback.
@@ -0,0 +1,48 @@
1
+ # AltitudeProvider
2
+
3
+ Sets the color scheme, accent and motion preference for everything inside it, and hosts the layers that rise above the page.
4
+
5
+ Wrap the app once, near the root. It renders a themed root, a **stage** (your app, which recedes behind dialogs) and a **layer host** (where dialogs and toasts render, so they stay inside the theme but outside the receding stage).
6
+
7
+ It also sets the high-contrast rendering: with `contrast="more"` (or the system's `prefers-contrast: more`), shadows become hard ink slabs whose thickness is the height.
8
+
9
+ It runs the **approach** tracker too, which lets openings react to a pointer before it arrives: an empty field deepens to receive, and a bottom sheet's finger pull opens. Raised things (buttons, cards, piles, steps) never react before the pointer is on them and never follow it. It only follows a mouse or pen, never touch, and reduced motion turns every approach effect off. `approach={false}` turns it off for everything inside.
10
+
11
+ **Your own scheme.** `scheme` also takes a custom scheme: `{ name, base, tokens }`, your colour and shadow tokens laid over a built-in scheme (`base`, which supplies anything you leave out and the light or dark handling). The provider scopes them to itself in a stylesheet, so they nest like the built-in schemes and still give way to high contrast. If the scheme breaks the invariants in [Theming](../../visual/Theming.md) (the surface ladder, contrast on every surface, the data palette's gates), it says which in a console warning; `checkScheme(el, { include: ['core', 'data'] })` runs the same test in your own tests. A plain string names your own `[data-theme="…"]` block instead.
12
+
13
+ **You provide:** `scheme` (a built-in name, your own, a custom scheme, or nothing, to inherit `data-theme` from the page), optionally `accent` as a hex color, `motion`, `approach`, `loading="breath"` to try the experimental breathing imprints (see `Await`), and `recede={false}` if your layout can't tolerate a transform on the app root (for example a `position: fixed` header that must not scale).
14
+
15
+ - Do nest a provider to theme a region differently: a `nightfall` sidebar in a `daylight` app.
16
+ - Do pass your brand color as `accent`; legibility is derived for you, and the chart palette's first series and magnitude ramp follow it (the other series re-order so neighbours stay apart for colour-blind readers).
17
+ - Do reach for a custom scheme only when the brand needs its own grounds and ink, and keep `checkScheme` passing in your tests.
18
+ - Don't render `Dialog` or `Toaster` outside a provider unless you accept them portaling to `document.body` without the receding page.
19
+
20
+ Modal ownership is coordinated privately per document. A nested themed Dialog keeps the nearest provider's portal host and blocks background branches throughout the document. Ancestors containing that portal are never transformed or made inert as a whole. Each provider's own `recede` setting remains independent. A stationary Dialog suppresses recession and blur on participating lower modal layers until it finishes closing.
21
+
22
+ Isolation preserves application-owned inertness and restores the original inline scroll style after the last modal exits. Reopening during exit retains the same ownership; route/provider unmount releases it immediately. Applications do not need a competing inert wrapper. The legacy `useAltitude().pushLayer()` helper remains compatible as a visual recession vote with an idempotent release. It does not acquire modal isolation or focus ownership; use Dialog and modal Sheet for those behaviors. `useAltitude()` supplies the nearest portal `host`, and the ownership registry remains private.
23
+
24
+ **Density.** `density` sets how large controls are. `'auto'` (default) uses the mouse sizes and steps up under a coarse pointer, so controls are big enough to hit with a finger; `'comfortable'` uses the larger sizes everywhere; `'compact'` keeps the mouse sizes on touch. Hit areas reach 44px under a coarse pointer either way. See [Theming](../../visual/Theming.md), "Density and target size".
25
+
26
+ ## App modes
27
+
28
+ `mode` marks the whole app as being in a state people must never mistake for the normal app: Stripe-style **test mode**, a support agent **viewing as a customer**, a **read-only** workspace, a **preview** of unpublished content.
29
+
30
+ ```tsx
31
+ <AltitudeProvider mode={{ tone: 'caution', label: 'Test mode', description: 'Payments are simulated.',
32
+ action: <Button size="sm" onClick={exit}>Leave test mode</Button> }}>
33
+ ```
34
+
35
+ **Elevation: a word and a border.** The page is the ground, so height can't say "this whole ground is different". The mode is carried by the non-height cues instead (law 7 as the primary channel):
36
+ - **A flush strip** at the top, on the tone's soft colour, naming the mode, with your way out. It can't be dismissed.
37
+ - **A 2px frame around the viewport** in the mode's tone. It stays while scrolling, and above dialogs and sheets.
38
+ - **A plate in every `Dialog` and `Sheet` header** naming the mode, so a refund confirmed in test mode says so.
39
+
40
+ Nothing moves: a mode is there from load. Entering it is announced once, politely ("Test mode on"). The strip is a region named by the mode, first in the tab order. Under forced colours the frame is a 4px double `CanvasText` border. Under `contrast="more"` it's 3px. `ModeContext` gives your own floating parts the mode.
41
+
42
+ - Do use a mode for anything that changes what actions do (simulated payments, acting as someone else).
43
+ - Don't use a page banner for a mode; a banner can be dismissed and scrolls away.
44
+
45
+ ## One primary per view
46
+
47
+ Law 1 says the primary action rests at level 2, one per view. With `devChecks` (on by default when `process.env.NODE_ENV` isn't `'production'`), a `Button variant="primary"` registers with the provider while it's mounted. When more than one *visible* primary sits in the same view, one grouped warning names them and law 1. The page is one view, and each `Dialog`, `Sheet` and `Popover` is its own. Wrap independent parts (cards on a dashboard, each with its own task) in `<PrimaryScope>` to make them views of their own. The check is a warning only, it's gone from production builds, and the rule is `ALT-VISUAL-002` (`altitude explain ALT-VISUAL-002`). `PageHeader`'s `primary` slot is where a page's one primary belongs.
48
+
@@ -0,0 +1,51 @@
1
+ # AppShell and Sidebar
2
+
3
+ The frame of an app: a sidebar of destinations (grouped, collapsible), a top bar with search and the user's menu, and the page.
4
+
5
+ **Elevation: the frame lies on the ground.** Where you are is part of the record, so the sidebar, top bar and page are all `surface-ground`, divided by `line`. Don't float the sidebar as a raised card. Only the things in it that act are live:
6
+ - **A destination** lifts one level when pointed at, because it navigates.
7
+ - **The current one** sits on a raised pill (segmented `Tabs`' build) that glides from destination to destination as you move.
8
+ - **Only a count floats** (`Badge variant="count"`). A status ("Beta") would be a flush plate.
9
+ - **Collapsing to a rail** changes width on `ease-glide`, not level. In the rail, each destination is an icon with its name in a tooltip to the right.
10
+ - **The top bar** rises one level while content passes under it, as `ScrollArea`'s header does.
11
+ - **On narrow screens** (below `mobileBreakpoint`, default 640px, the compact breakpoint) the sidebar moves into a `Sheet` drawer from the left (level 4), titled "Navigation". Choosing a destination closes it.
12
+
13
+ **Parts** (shadcn/ui's names):
14
+ - `AppShell`: `sidebar`, `topBar` and the page. It includes a `SidebarProvider`, a skip link, and a `SidebarTrigger` at the start of the top bar.
15
+ - `SidebarProvider`: `collapsed`, `defaultCollapsed`, `onCollapsedChange`, `mobileBreakpoint`. ⌘B or Ctrl+B toggles. `useSidebar()` reads and changes the state.
16
+ - `Sidebar`: a `nav` (`aria-label`, default "Main") with `collapsible` (`'icon' | 'none'`). Inside it go `SidebarHeader`, `SidebarContent` (scrolls, and holds the pill), `SidebarFooter`.
17
+ - `SidebarGroup`: `label`; `collapsible` makes the label a disclosure button. Also `SidebarGroupLabel`.
18
+ - `SidebarMenu` › `SidebarMenuItem` › `SidebarMenuButton` (`href`, `current`, `icon`, `asChild` for a router's link) and `SidebarMenuBadge` (`label`, such as "4 unread").
19
+ - `SidebarTrigger`, and `SidebarInset` (the `main` landmark; it sets `--alt-page-pad` for `PageHeader`).
20
+
21
+ **Open questions, answered.** Persisting the collapsed state is up to your app (use `collapsed` and `onCollapsedChange` with a cookie or `localStorage`). The workspace switcher is a recipe, a `DropdownMenu` whose trigger is an `Avatar` and a name, not a part (see the preview).
22
+
23
+ **Accessibility.** [Landmarks](https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/): `nav` with a label, `header`, `main`. A skip link ("Skip to content") comes first. The current destination has `aria-current="page"`. **A count is part of the destination's name** ("Inbox, 4 unread"). In the rail, each destination keeps its name as `aria-label` and in a tooltip. Collapsible groups use the disclosure pattern. `SidebarTrigger` says what it will do ("Collapse sidebar", "Open navigation") and carries `aria-keyshortcuts`. Under forced colours the pill gets a 1px border. Under `contrast="more"` it gets a 2px slab and the dividers are `line-control`. Under reduced motion the pill appears in place and the rail changes width at once.
24
+
25
+ ```tsx
26
+ <AppShell
27
+ sidebar={
28
+ <Sidebar>
29
+ <SidebarHeader><WorkspaceMenu /></SidebarHeader>
30
+ <SidebarContent>
31
+ <SidebarGroup label="Workspace">
32
+ <SidebarMenu>
33
+ <SidebarMenuItem>
34
+ <SidebarMenuButton href="/inbox" current icon={<Inbox />}>Inbox</SidebarMenuButton>
35
+ <SidebarMenuBadge label="4 unread">4</SidebarMenuBadge>
36
+ </SidebarMenuItem>
37
+ </SidebarMenu>
38
+ </SidebarGroup>
39
+ </SidebarContent>
40
+ <SidebarFooter><UserMenu /></SidebarFooter>
41
+ </Sidebar>
42
+ }
43
+ topBar={<SearchEntry />}>
44
+ {page}
45
+ </AppShell>
46
+ ```
47
+
48
+ - Do give every destination an icon if the sidebar can collapse to a rail.
49
+ - Do say what a count counts (`label="4 unread"`).
50
+ - Don't raise the sidebar into a card or give it a shadow.
51
+ - Don't put actions in the sidebar's destinations; they navigate.
@@ -0,0 +1,43 @@
1
+ # AreaChart
2
+
3
+ A total over time and the parts that make it up (requests by region, storage by type), or a single volume over time. Stacked areas answer "how big, and from where"; `stack="expand"` answers "what share". It renders inside a [`ChartContainer`](../ChartContainer/README.md).
4
+
5
+ **Why it's flat.** Bands are the record. Each is a flat `data-*` fill, stacked on the ground line in config order from the bottom, and parted from the band above by a 2px line in the plot's own colour, the same gap a stacked bar's segments have. The topmost band carries a 2px top line in its colour. A single area is a flat `data-1` fill at reduced opacity under a 2px `data-1` line.
6
+
7
+ **No gradient fill.** Most libraries fade an area to transparent by default. Altitude's only face gradient is `--alt-sheen`, and it means "press it" (E4), so an area never has one.
8
+
9
+ **Stack order never changes.** Bands stack in config order, so a band never jumps to a new position when values change, and its colour stays its own. In a stack a missing value adds nothing (the bands above rest on the one below); in a single area it's a gap, as in a line.
10
+
11
+ **Named in place.** Stacked bands are labelled inside themselves in `ink`, on a halo of the plot's colour, at their thickest point, where they're at least 20px tall; thin bands rely on the legend and the readout. `labels={false}` turns this off.
12
+
13
+ **Without colour.** Under forced colours and in print, and with `patterns` on the container, each band takes its slot's hatch (the first band is solid). With `contrast="more"` the separators are 3px `ink`.
14
+
15
+ **Motion.** As `LineChart`: when the x values are the same, bands glide to their new values on `ease-glide`, after any rescale; when they differ, the old chart and the new one cross-fade. Nothing grows from the ground on first render ([Motion](../../visual/Motion.md), "Charts"). While data loads, the imprint is an area pressed into the page.
16
+
17
+ **Accessibility.** The keyboard path is the container's (#63): ←/→ along x and, in a stack, ↑/↓ through the bands at that x, from the bottom up, so ↑ goes to the band above. Each point names its band's value and its share of the total: "Oct 1, 2026, EU, 1.1K. 34% of 3.2K. 1 of 14." The table view (`views`) has a column per band and a Total.
18
+
19
+ **Not yet the record.** `status` marks partial or projected records. From the last settled point on, every band is hatched in its colour on the plot's own, with no solid fill, and the top line is dashed. A 1px `line-control` rule named "Today" (`boundary`) marks where the record ends, and the readout, point names and table say "Projected" or "So far". While patterns are on, a band with its own hatch keeps it there, faded, so bands stay told apart.
20
+
21
+ **Selecting and drilling.** `selected` and `onSelect` make the marks a selection that cross-filters the rest of a dashboard, and `drill` opens a level down inside a `Drilldown`. Hover only promises what a click does. See [ChartContainer](../ChartContainer/README.md), "Marks you can select or drill into".
22
+
23
+ **Filtering in place.** `relevance` recedes the bands to 35% around each missing record, so the stack keeps its shape; `rescale` fits the axis to the matches. See [ChartContainer](../ChartContainer/README.md), "Filtering in place".
24
+
25
+ **A sliding window.** `window` works as `LineChart`'s: the latest stretch of x, gliding one step in a `live` container, or a chosen `{ start, end }`, which a `ChartBrush` sets (`onWindowChange`).
26
+
27
+ **Props:** `data` and `x`; `window` and `onWindowChange`; `stack` (`true` or `'expand'`); `curve` (`'linear'` or `'monotone'`); `y={{ format }}`; `labels`; `tooltip`; `status` and `boundary`; and a `ChartTooltip` or `ChartLegend` as children.
28
+
29
+ **Focus.** `focus={['team']}` keeps those bands' colours and draws the rest in `data-context`, their labels in `ink-muted`. The stack's order doesn't change. See [ChartContainer](../ChartContainer/README.md), "Focus".
30
+
31
+ **You provide:** a `ChartContainer` whose `config` names each band in stacking order (the base first), the `data` in x order, and usually a `summary`.
32
+
33
+ ```tsx
34
+ <ChartContainer config={{ us: { label: 'US' }, eu: { label: 'EU' }, apac: { label: 'APAC' } }}>
35
+ <AreaChart data={requests} x="day" stack /> {/* stack="expand" for 100% */}
36
+ </ChartContainer>
37
+ ```
38
+
39
+ - Do put the largest or steadiest part at the bottom, where it stands on the ground line and reads most accurately.
40
+ - Do use `stack="expand"` only when the share is the point; the totals disappear.
41
+ - Do keep to six bands or fewer; fold the rest into one "Other" series.
42
+ - Don't overlay several unstacked areas; compare trends with a `LineChart`.
43
+ - Don't add a gradient, a glow or a shadow to make it look richer.