altitude-design 0.2.0 → 0.3.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 (295) hide show
  1. package/README.md +46 -8
  2. package/dist/adapters/codemirror.cjs +88 -0
  3. package/dist/adapters/codemirror.cjs.map +7 -0
  4. package/dist/adapters/codemirror.js +67 -0
  5. package/dist/adapters/codemirror.js.map +7 -0
  6. package/dist/adapters/react-flow.css +102 -0
  7. package/dist/adapters/tiptap.css +71 -0
  8. package/dist/altitude.css +3165 -246
  9. package/dist/altitude.iife.js +33 -5
  10. package/dist/altitude.no-fonts.css +3165 -246
  11. package/dist/chart-themes.cjs +195 -0
  12. package/dist/chart-themes.cjs.map +7 -0
  13. package/dist/chart-themes.js +165 -0
  14. package/dist/chart-themes.js.map +7 -0
  15. package/dist/cli/altitude.mjs +134 -0
  16. package/dist/guidance/README.md +32 -0
  17. package/dist/guidance/agent-setup.md +22 -0
  18. package/dist/guidance/app-skeleton.md +17 -0
  19. package/dist/guidance/evidence.json +95 -0
  20. package/dist/guidance/index.json +493 -0
  21. package/dist/guidance/llms.txt +20 -0
  22. package/dist/guidance/manifest.json +92 -0
  23. package/dist/guidance/pattern.schema.json +181 -0
  24. package/dist/guidance/patterns/agent-action.json +77 -0
  25. package/dist/guidance/patterns/agent-action.md +39 -0
  26. package/dist/guidance/patterns/assistant-conversation.json +77 -0
  27. package/dist/guidance/patterns/assistant-conversation.md +50 -0
  28. package/dist/guidance/patterns/async-collection.json +71 -0
  29. package/dist/guidance/patterns/async-collection.md +29 -0
  30. package/dist/guidance/patterns/audit-log.json +75 -0
  31. package/dist/guidance/patterns/audit-log.md +27 -0
  32. package/dist/guidance/patterns/auth.json +86 -0
  33. package/dist/guidance/patterns/auth.md +29 -0
  34. package/dist/guidance/patterns/billing.json +80 -0
  35. package/dist/guidance/patterns/billing.md +29 -0
  36. package/dist/guidance/patterns/consequential-action.json +76 -0
  37. package/dist/guidance/patterns/consequential-action.md +27 -0
  38. package/dist/guidance/patterns/developer-settings.json +79 -0
  39. package/dist/guidance/patterns/developer-settings.md +29 -0
  40. package/dist/guidance/patterns/edit-record.json +74 -0
  41. package/dist/guidance/patterns/edit-record.md +51 -0
  42. package/dist/guidance/patterns/import.json +77 -0
  43. package/dist/guidance/patterns/import.md +31 -0
  44. package/dist/guidance/patterns/members.json +77 -0
  45. package/dist/guidance/patterns/members.md +27 -0
  46. package/dist/guidance/patterns/onboarding.json +78 -0
  47. package/dist/guidance/patterns/onboarding.md +31 -0
  48. package/dist/guidance/patterns/review-submit.json +72 -0
  49. package/dist/guidance/patterns/review-submit.md +27 -0
  50. package/dist/guidance/patterns/screen-layout.json +79 -0
  51. package/dist/guidance/patterns/screen-layout.md +40 -0
  52. package/dist/guidance/patterns/settings.json +78 -0
  53. package/dist/guidance/patterns/settings.md +32 -0
  54. package/dist/guidance/patterns/sync.json +77 -0
  55. package/dist/guidance/patterns/sync.md +31 -0
  56. package/dist/guidance/principles.md +23 -0
  57. package/dist/guidance/references/build/checks/agent-action-pattern.mjs +57 -0
  58. package/dist/guidance/references/build/checks/agent-setup.mjs +78 -0
  59. package/dist/guidance/references/build/checks/assistant-conversation-pattern.mjs +49 -0
  60. package/dist/guidance/references/build/checks/audit-log-pattern.mjs +63 -0
  61. package/dist/guidance/references/build/checks/auth-pattern.mjs +67 -0
  62. package/dist/guidance/references/build/checks/billing-pattern.mjs +63 -0
  63. package/dist/guidance/references/build/checks/developer-settings-pattern.mjs +76 -0
  64. package/dist/guidance/references/build/checks/guidance.mjs +71 -0
  65. package/dist/guidance/references/build/checks/import-pattern.mjs +69 -0
  66. package/dist/guidance/references/build/checks/members-pattern.mjs +71 -0
  67. package/dist/guidance/references/build/checks/onboarding-pattern.mjs +54 -0
  68. package/dist/guidance/references/build/checks/pattern-types.mjs +13 -0
  69. package/dist/guidance/references/build/checks/pattern-workflows.mjs +226 -0
  70. package/dist/guidance/references/build/checks/primary-scope.mjs +49 -0
  71. package/dist/guidance/references/build/checks/render-tells.mjs +72 -0
  72. package/dist/guidance/references/build/checks/screen-layout-pattern.mjs +111 -0
  73. package/dist/guidance/references/build/checks/semantics.mjs +72 -0
  74. package/dist/guidance/references/build/checks/settings-pattern.mjs +73 -0
  75. package/dist/guidance/references/build/checks/sync-pattern.mjs +62 -0
  76. package/dist/guidance/references/build/checks/tells-lint.mjs +164 -0
  77. package/dist/guidance/references/build/fixtures/pattern-workflows.tsx +83 -0
  78. package/dist/guidance/references/build/fixtures/patterns/agent-action.jsx +60 -0
  79. package/dist/guidance/references/build/fixtures/patterns/app-skeleton.jsx +94 -0
  80. package/dist/guidance/references/build/fixtures/patterns/assistant-conversation.jsx +79 -0
  81. package/dist/guidance/references/build/fixtures/patterns/audit-log.jsx +81 -0
  82. package/dist/guidance/references/build/fixtures/patterns/auth.jsx +150 -0
  83. package/dist/guidance/references/build/fixtures/patterns/billing.jsx +96 -0
  84. package/dist/guidance/references/build/fixtures/patterns/developer-settings.jsx +95 -0
  85. package/dist/guidance/references/build/fixtures/patterns/import.jsx +102 -0
  86. package/dist/guidance/references/build/fixtures/patterns/members.jsx +106 -0
  87. package/dist/guidance/references/build/fixtures/patterns/onboarding.jsx +70 -0
  88. package/dist/guidance/references/build/fixtures/patterns/screen-layout-decision.jsx +74 -0
  89. package/dist/guidance/references/build/fixtures/patterns/screen-layout-editor.jsx +56 -0
  90. package/dist/guidance/references/build/fixtures/patterns/screen-layout-report.jsx +42 -0
  91. package/dist/guidance/references/build/fixtures/patterns/settings.jsx +73 -0
  92. package/dist/guidance/references/build/fixtures/patterns/sync.jsx +78 -0
  93. package/dist/guidance/references/project/components/src/pattern-async-collection.tsx +57 -0
  94. package/dist/guidance/references/project/components/src/pattern-consequential-action.tsx +77 -0
  95. package/dist/guidance/references/project/components/src/pattern-edit-record.tsx +127 -0
  96. package/dist/guidance/references/project/components/src/pattern-review-submit.tsx +47 -0
  97. package/dist/guidance/references/project/components/src/settings.tsx +79 -0
  98. package/dist/guidance/references/scripts/build-framework-fixture.mjs +20 -0
  99. package/dist/guidance/rules.json +653 -0
  100. package/dist/guidance/visual/Elevation.md +194 -0
  101. package/dist/guidance/visual/Fundamentals.md +83 -0
  102. package/dist/guidance/visual/Research.md +1620 -0
  103. package/dist/index.cjs +19009 -1854
  104. package/dist/index.cjs.map +4 -4
  105. package/dist/index.js +19009 -1854
  106. package/dist/index.js.map +4 -4
  107. package/dist/tokens.css +84 -84
  108. package/dist/tokens.json +301 -128
  109. package/dist/types/adapter-codemirror.d.ts +13 -0
  110. package/dist/types/agent-fleet.d.ts +166 -0
  111. package/dist/types/ai-label.d.ts +30 -0
  112. package/dist/types/app-shell.d.ts +83 -0
  113. package/dist/types/area-chart.d.ts +80 -0
  114. package/dist/types/artifact.d.ts +65 -0
  115. package/dist/types/bar-chart.d.ts +83 -0
  116. package/dist/types/bar-list.d.ts +35 -0
  117. package/dist/types/breadcrumb.d.ts +62 -0
  118. package/dist/types/chart-brush.d.ts +35 -0
  119. package/dist/types/chart-themes.d.ts +229 -0
  120. package/dist/types/chart.d.ts +570 -0
  121. package/dist/types/checklist.d.ts +71 -0
  122. package/dist/types/code-block.d.ts +32 -0
  123. package/dist/types/collections.d.ts +116 -5
  124. package/dist/types/color-picker.d.ts +38 -0
  125. package/dist/types/command.d.ts +6 -3
  126. package/dist/types/comment-thread.d.ts +48 -0
  127. package/dist/types/confirmation.d.ts +60 -0
  128. package/dist/types/context-menu.d.ts +17 -0
  129. package/dist/types/controls.d.ts +134 -22
  130. package/dist/types/conversation.d.ts +130 -0
  131. package/dist/types/copy-button.d.ts +19 -0
  132. package/dist/types/core.d.ts +139 -10
  133. package/dist/types/data-grid.d.ts +76 -0
  134. package/dist/types/date-picker.d.ts +96 -0
  135. package/dist/types/diff-view.d.ts +54 -0
  136. package/dist/types/dot-plot.d.ts +54 -0
  137. package/dist/types/edit-region.d.ts +121 -0
  138. package/dist/types/feature-gate.d.ts +22 -0
  139. package/dist/types/filter-bar.d.ts +65 -0
  140. package/dist/types/formatted-number.d.ts +38 -0
  141. package/dist/types/heatmap.d.ts +64 -0
  142. package/dist/types/in-place.d.ts +24 -0
  143. package/dist/types/index.d.ts +159 -14
  144. package/dist/types/inline-citation.d.ts +35 -0
  145. package/dist/types/inline-edit.d.ts +71 -0
  146. package/dist/types/input-otp.d.ts +37 -0
  147. package/dist/types/item-group.d.ts +104 -0
  148. package/dist/types/line-chart.d.ts +116 -0
  149. package/dist/types/link.d.ts +11 -0
  150. package/dist/types/markdown.d.ts +88 -0
  151. package/dist/types/meter.d.ts +43 -0
  152. package/dist/types/modal-state.d.ts +62 -0
  153. package/dist/types/nav.d.ts +8 -1
  154. package/dist/types/notifications.d.ts +42 -0
  155. package/dist/types/number-input.d.ts +30 -0
  156. package/dist/types/overlays.d.ts +56 -15
  157. package/dist/types/page-header.d.ts +27 -0
  158. package/dist/types/pagination.d.ts +29 -0
  159. package/dist/types/palette.d.ts +64 -0
  160. package/dist/types/password-input.d.ts +22 -0
  161. package/dist/types/pattern-assistant-conversation.d.ts +55 -0
  162. package/dist/types/pattern-async-collection.d.ts +34 -0
  163. package/dist/types/pattern-consequential-action.d.ts +53 -0
  164. package/dist/types/pattern-edit-record.d.ts +72 -0
  165. package/dist/types/pattern-review-submit.d.ts +43 -0
  166. package/dist/types/permission-matrix.d.ts +34 -0
  167. package/dist/types/plan-picker.d.ts +40 -0
  168. package/dist/types/prompt-input.d.ts +106 -0
  169. package/dist/types/property-list.d.ts +67 -0
  170. package/dist/types/question.d.ts +64 -0
  171. package/dist/types/relative-time.d.ts +21 -0
  172. package/dist/types/removal-receipt.d.ts +1180 -0
  173. package/dist/types/resizable.d.ts +48 -0
  174. package/dist/types/roving.d.ts +44 -0
  175. package/dist/types/save-bar.d.ts +38 -0
  176. package/dist/types/saved-views.d.ts +72 -0
  177. package/dist/types/scatter-chart.d.ts +60 -0
  178. package/dist/types/schedule.d.ts +80 -0
  179. package/dist/types/secret-field.d.ts +33 -0
  180. package/dist/types/select.d.ts +44 -2
  181. package/dist/types/separator.d.ts +11 -0
  182. package/dist/types/settings.d.ts +39 -0
  183. package/dist/types/sheet.d.ts +1 -1
  184. package/dist/types/sparkline.d.ts +46 -0
  185. package/dist/types/split-button.d.ts +21 -0
  186. package/dist/types/stat.d.ts +54 -0
  187. package/dist/types/stepper.d.ts +6 -3
  188. package/dist/types/suggestions.d.ts +29 -0
  189. package/dist/types/suspense.d.ts +85 -0
  190. package/dist/types/sync-indicator.d.ts +24 -0
  191. package/dist/types/tag-input.d.ts +65 -0
  192. package/dist/types/terminal.d.ts +33 -0
  193. package/dist/types/thread-list.d.ts +58 -0
  194. package/dist/types/time-input.d.ts +33 -0
  195. package/dist/types/timeline.d.ts +38 -0
  196. package/dist/types/toggle.d.ts +46 -0
  197. package/dist/types/toolbar.d.ts +34 -0
  198. package/dist/types/tour.d.ts +23 -0
  199. package/dist/types/trace.d.ts +65 -0
  200. package/dist/types/tree.d.ts +71 -0
  201. package/dist/types-cjs/adapter-codemirror.d.cts +13 -0
  202. package/dist/types-cjs/agent-fleet.d.cts +166 -0
  203. package/dist/types-cjs/ai-label.d.cts +30 -0
  204. package/dist/types-cjs/app-shell.d.cts +83 -0
  205. package/dist/types-cjs/area-chart.d.cts +80 -0
  206. package/dist/types-cjs/artifact.d.cts +65 -0
  207. package/dist/types-cjs/bar-chart.d.cts +83 -0
  208. package/dist/types-cjs/bar-list.d.cts +35 -0
  209. package/dist/types-cjs/breadcrumb.d.cts +62 -0
  210. package/dist/types-cjs/chart-brush.d.cts +35 -0
  211. package/dist/types-cjs/chart-themes.d.cts +229 -0
  212. package/dist/types-cjs/chart.d.cts +570 -0
  213. package/dist/types-cjs/checklist.d.cts +71 -0
  214. package/dist/types-cjs/code-block.d.cts +32 -0
  215. package/dist/types-cjs/collections.d.cts +246 -0
  216. package/dist/types-cjs/color-picker.d.cts +38 -0
  217. package/dist/types-cjs/command.d.cts +66 -0
  218. package/dist/types-cjs/comment-thread.d.cts +48 -0
  219. package/dist/types-cjs/confirmation.d.cts +60 -0
  220. package/dist/types-cjs/context-menu.d.cts +17 -0
  221. package/dist/types-cjs/controls.d.cts +360 -0
  222. package/dist/types-cjs/conversation.d.cts +130 -0
  223. package/dist/types-cjs/copy-button.d.cts +19 -0
  224. package/dist/types-cjs/core.d.cts +256 -0
  225. package/dist/types-cjs/data-grid.d.cts +76 -0
  226. package/dist/types-cjs/date-picker.d.cts +96 -0
  227. package/dist/types-cjs/diff-view.d.cts +54 -0
  228. package/dist/types-cjs/dot-plot.d.cts +54 -0
  229. package/dist/types-cjs/edit-region.d.cts +121 -0
  230. package/dist/types-cjs/empty.d.cts +24 -0
  231. package/dist/types-cjs/feature-gate.d.cts +22 -0
  232. package/dist/types-cjs/filter-bar.d.cts +65 -0
  233. package/dist/types-cjs/formatted-number.d.cts +38 -0
  234. package/dist/types-cjs/heatmap.d.cts +64 -0
  235. package/dist/types-cjs/in-place.d.cts +24 -0
  236. package/dist/types-cjs/index.d.cts +154 -0
  237. package/dist/types-cjs/inline-citation.d.cts +35 -0
  238. package/dist/types-cjs/inline-edit.d.cts +71 -0
  239. package/dist/types-cjs/input-otp.d.cts +37 -0
  240. package/dist/types-cjs/item-group.d.cts +104 -0
  241. package/dist/types-cjs/line-chart.d.cts +116 -0
  242. package/dist/types-cjs/link.d.cts +11 -0
  243. package/dist/types-cjs/markdown.d.cts +88 -0
  244. package/dist/types-cjs/meter.d.cts +43 -0
  245. package/dist/types-cjs/modal-state.d.cts +62 -0
  246. package/dist/types-cjs/nav.d.cts +25 -0
  247. package/dist/types-cjs/notifications.d.cts +42 -0
  248. package/dist/types-cjs/number-input.d.cts +30 -0
  249. package/dist/types-cjs/overlays.d.cts +170 -0
  250. package/dist/types-cjs/page-header.d.cts +27 -0
  251. package/dist/types-cjs/pagination.d.cts +29 -0
  252. package/dist/types-cjs/palette.d.cts +64 -0
  253. package/dist/types-cjs/password-input.d.cts +22 -0
  254. package/dist/types-cjs/pattern-assistant-conversation.d.cts +55 -0
  255. package/dist/types-cjs/pattern-async-collection.d.cts +34 -0
  256. package/dist/types-cjs/pattern-consequential-action.d.cts +53 -0
  257. package/dist/types-cjs/pattern-edit-record.d.cts +72 -0
  258. package/dist/types-cjs/pattern-review-submit.d.cts +43 -0
  259. package/dist/types-cjs/permission-matrix.d.cts +34 -0
  260. package/dist/types-cjs/pile.d.cts +27 -0
  261. package/dist/types-cjs/plan-picker.d.cts +40 -0
  262. package/dist/types-cjs/prompt-input.d.cts +106 -0
  263. package/dist/types-cjs/property-list.d.cts +67 -0
  264. package/dist/types-cjs/question.d.cts +64 -0
  265. package/dist/types-cjs/relative-time.d.cts +21 -0
  266. package/dist/types-cjs/removal-receipt.d.cts +1180 -0
  267. package/dist/types-cjs/resizable.d.cts +48 -0
  268. package/dist/types-cjs/roving.d.cts +44 -0
  269. package/dist/types-cjs/save-bar.d.cts +38 -0
  270. package/dist/types-cjs/saved-views.d.cts +72 -0
  271. package/dist/types-cjs/scatter-chart.d.cts +60 -0
  272. package/dist/types-cjs/schedule.d.cts +80 -0
  273. package/dist/types-cjs/secret-field.d.cts +33 -0
  274. package/dist/types-cjs/select.d.cts +109 -0
  275. package/dist/types-cjs/separator.d.cts +11 -0
  276. package/dist/types-cjs/settings.d.cts +39 -0
  277. package/dist/types-cjs/sheet.d.cts +27 -0
  278. package/dist/types-cjs/sparkline.d.cts +46 -0
  279. package/dist/types-cjs/split-button.d.cts +21 -0
  280. package/dist/types-cjs/stat.d.cts +54 -0
  281. package/dist/types-cjs/stepper.d.cts +26 -0
  282. package/dist/types-cjs/suggestions.d.cts +29 -0
  283. package/dist/types-cjs/suspense.d.cts +85 -0
  284. package/dist/types-cjs/sync-indicator.d.cts +24 -0
  285. package/dist/types-cjs/tag-input.d.cts +65 -0
  286. package/dist/types-cjs/terminal.d.cts +33 -0
  287. package/dist/types-cjs/thread-list.d.cts +58 -0
  288. package/dist/types-cjs/time-input.d.cts +33 -0
  289. package/dist/types-cjs/timeline.d.cts +38 -0
  290. package/dist/types-cjs/toggle.d.cts +46 -0
  291. package/dist/types-cjs/toolbar.d.cts +34 -0
  292. package/dist/types-cjs/tour.d.cts +23 -0
  293. package/dist/types-cjs/trace.d.cts +65 -0
  294. package/dist/types-cjs/tree.d.cts +71 -0
  295. package/package.json +90 -9
@@ -0,0 +1,79 @@
1
+ // Assistant conversation pattern (project/framework/patterns/assistant-conversation.md): send, stream, stop, retry and
2
+ // recover without losing the person's words. useAssistantTurn holds the turn: a double Enter sends once, Stop keeps the
3
+ // partial answer, an answer settles only when the stream says so, a failure keeps the message and says what happened
4
+ // in words (offline, rate limited, context full), and an unfinished answer after a reload is "Interrupted". A Meter in
5
+ // the toolbar shows how much of the context window is used.
6
+ // The lines between the preview markers are its "AssistantConversationPattern" preview (scripts/pattern-previews.mjs
7
+ // copies them); build/checks/pattern-types.mjs type-checks this file against the public declarations.
8
+ import * as React from 'react';
9
+ import * as A from 'altitude-design';
10
+ // preview:start
11
+ const answer = 'The billing migration is two days behind: the invoice backfill is still running. The EU data residency review needs a decision from legal by Friday.';
12
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
13
+ /** @param {'offline' | 'rate-limited' | 'context-full' | 'none'} failure */
14
+ function scripted(failure) {
15
+ /** @param {string} _input @param {AbortSignal} signal */
16
+ return async function* (_input, signal) {
17
+ await wait(350);
18
+ if (failure === 'offline') throw new A.AssistantError('offline');
19
+ if (failure === 'rate-limited') throw new A.AssistantError('rate-limited', undefined, Date.now() + 25 * 60000);
20
+ if (failure === 'context-full') throw new A.AssistantError('context-full');
21
+ for (let n = 0; n < answer.length; n += 6) { if (signal.aborted) return; await wait(45); yield answer.slice(n, n + 6); }
22
+ };
23
+ }
24
+ /** @param {{ restored?: { input: string, text: string, complete: boolean } }} props */
25
+ function Thread({ restored }) {
26
+ const [failure, setFailure] = React.useState(/** @type {'offline' | 'rate-limited' | 'context-full' | 'none'} */ ('none'));
27
+ const [draft, setDraft] = React.useState('');
28
+ const [used, setUsed] = React.useState(64);
29
+ const turn = A.useAssistantTurn({ stream: scripted(failure), restored });
30
+ // A failed send keeps the message: it goes back in the composer.
31
+ React.useEffect(() => { if (turn.state === 'failed' && turn.input) setDraft(turn.input); }, [turn.state]); // eslint-disable-line react-hooks/exhaustive-deps
32
+ React.useEffect(() => { if (turn.state === 'complete') setUsed((u) => Math.min(99, u + 9)); }, [turn.state]);
33
+ return (
34
+ <div className="col ac">
35
+ <A.ToggleGroup type="single" size="sm" aria-label="Make the next send fail" value={failure} onValueChange={(v) => setFailure(/** @type {any} */ (v) || 'none')}>
36
+ <A.ToggleGroupItem value="none">Works</A.ToggleGroupItem>
37
+ <A.ToggleGroupItem value="offline">Offline</A.ToggleGroupItem>
38
+ <A.ToggleGroupItem value="rate-limited">Rate limited</A.ToggleGroupItem>
39
+ <A.ToggleGroupItem value="context-full">Context full</A.ToggleGroupItem>
40
+ </A.ToggleGroup>
41
+ <A.Conversation aria-label="Assistant">
42
+ <A.ConversationContent>
43
+ {turn.input !== undefined && <A.Message from="user"><A.MessageContent>{turn.input}</A.MessageContent></A.Message>}
44
+ {(turn.text || turn.state === 'streaming' || turn.state === 'interrupted') && (
45
+ <A.Message from="assistant" status={turn.messageStatus} onRetry={turn.state === 'stopped' || turn.state === 'interrupted' ? turn.retry : undefined}>
46
+ <A.MessageContent><A.Markdown streaming={turn.state === 'streaming'}>{turn.text}</A.Markdown></A.MessageContent>
47
+ {turn.state === 'interrupted' && <p className="ac-note">Interrupted: the page reloaded before this answer finished.</p>}
48
+ </A.Message>
49
+ )}
50
+ </A.ConversationContent>
51
+ </A.Conversation>
52
+ {turn.state === 'failed' && (
53
+ <A.Alert tone={turn.error?.kind === 'context-full' ? 'caution' : 'critical'} title={turn.errorText}
54
+ action={turn.error?.kind === 'context-full' ? <A.Button size="sm" variant="secondary">Start a new conversation</A.Button> : <A.Button size="sm" variant="secondary" onClick={() => { setFailure('none'); setDraft(''); }}>Try again</A.Button>} />
55
+ )}
56
+ <A.PromptInput status={turn.promptStatus === 'error' ? 'ready' : turn.promptStatus} value={draft} onValueChange={setDraft} onStop={turn.stop}
57
+ onSubmit={({ text }) => { turn.submit(text); }}>
58
+ <A.PromptInputTextarea placeholder="Ask about the release" />
59
+ <A.PromptInputToolbar>
60
+ <A.Meter className="ac-context" label="Context" value={used} max={100} high={80} critical={95} format={(v) => `${v}% of context used`} />
61
+ <A.PromptInputSubmit />
62
+ </A.PromptInputToolbar>
63
+ </A.PromptInput>
64
+ </div>
65
+ );
66
+ }
67
+ function App() {
68
+ const [reloaded, setReloaded] = React.useState(false);
69
+ return (
70
+ <div className="col">
71
+ <A.Button size="sm" variant="ghost" onClick={() => setReloaded((r) => !r)}>{reloaded ? 'Start fresh' : 'Reload mid-answer'}</A.Button>
72
+ {reloaded
73
+ ? <Thread key="restored" restored={{ input: 'What are the open risks?', text: 'The billing migration is two days behind: the invoice', complete: false }} />
74
+ : <Thread key="fresh" />}
75
+ </div>
76
+ );
77
+ }
78
+ // preview:end
79
+ export default App;
@@ -0,0 +1,81 @@
1
+ // Audit log pattern (project/framework/patterns/audit-log.md): the workspace audit log is a DataTable of flush entries,
2
+ // each one sentence, filtered with a FilterBar and paged by the server; an entry expands to show what changed. A record's
3
+ // own activity is a Timeline. Exporting is a job: Progress while it runs, then a toast with the download, and the export
4
+ // is itself logged.
5
+ // The lines between the preview markers are its "AuditLogPattern" preview (scripts/pattern-previews.mjs copies them);
6
+ // build/checks/pattern-types.mjs type-checks this file against the public declarations and fails if the two differ.
7
+ import * as React from 'react';
8
+ import * as A from 'altitude-design';
9
+ // preview:start
10
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
11
+ const ACTORS = [{ value: 'Maya Chen', label: 'Maya Chen' }, { value: 'Theo Marsh', label: 'Theo Marsh' }, { value: 'Ines Duarte', label: 'Ines Duarte' }];
12
+ const ACTIONS = [{ value: 'role', label: 'Changed a role' }, { value: 'invite', label: 'Invited someone' }, { value: 'key', label: 'Rotated an API key' }, { value: 'export', label: 'Exported data' }];
13
+ /** @typedef {{ id: string, actor: string, action: string, resource: string, sentence: string, at: string, ip: string, before: Record<string, string>, after: Record<string, string> }} AuditEvent */
14
+ const SEED = Array.from({ length: 22 }, /** @returns {AuditEvent} */ (_, i) => {
15
+ const actor = ACTORS[i % 3].value, kind = ACTIONS[i % 3].value, day = String(28 - Math.floor(i / 3)).padStart(2, '0');
16
+ const at = `2026-10-${day}T${String(17 - (i % 3) * 3).padStart(2, '0')}:${String((i * 7) % 60).padStart(2, '0')}:00Z`;
17
+ if (kind === 'role') return { id: 'e' + i, actor, action: kind, resource: 'Ines Duarte', sentence: `${actor} changed Ines Duarte’s role from Member to Admin`, at, ip: '81.2.69.' + (10 + i), before: { role: 'Member' }, after: { role: 'Admin' } };
18
+ if (kind === 'invite') return { id: 'e' + i, actor, action: kind, resource: `guest${i}@acme.dev`, sentence: `${actor} invited guest${i}@acme.dev as a Viewer`, at, ip: '81.2.69.' + (10 + i), before: { invited: 'No' }, after: { invited: 'Yes, as Viewer' } };
19
+ return { id: 'e' + i, actor, action: kind, resource: 'Production key', sentence: `${actor} rotated the Production API key`, at, ip: '81.2.69.' + (10 + i), before: { key: 'ak_live_EXAMPLE…4f2a' }, after: { key: 'ak_live_EXAMPLE…9c1d' } };
20
+ });
21
+ /** @type {A.FilterField[]} */
22
+ const FIELDS = [{ key: 'actor', label: 'Actor', type: 'enum', options: ACTORS }, { key: 'action', label: 'Action', type: 'enum', options: ACTIONS }, { key: 'at', label: 'Date', type: 'date' }];
23
+ const test = (row, f) => {
24
+ if (f.value === undefined || f.value === '') return true;
25
+ const v = f.field === 'at' ? row.at.slice(0, 10) : row[f.field];
26
+ if (f.operator === 'is') return v === f.value;
27
+ if (f.operator === 'is not') return v !== f.value;
28
+ if (f.operator === 'before') return v < f.value;
29
+ if (f.operator === 'after') return v > f.value;
30
+ if (f.operator === 'between' && typeof f.value === 'object') return v >= f.value.start && v <= f.value.end;
31
+ return true;
32
+ };
33
+ function App() {
34
+ const [log, setLog] = React.useState(SEED);
35
+ const [filters, setFilters] = React.useState(/** @type {A.Filter[]} */ ([]));
36
+ const [page, setPage] = React.useState(1);
37
+ const [loading, setLoading] = React.useState(false);
38
+ const [exporting, setExporting] = React.useState(/** @type {number | null} */ (null));
39
+ const matching = log.filter((r) => filters.every((f) => test(r, f)));
40
+ const pageCount = Math.max(1, Math.ceil(matching.length / 5));
41
+ const rows = matching.slice((page - 1) * 5, page * 5);
42
+ const turn = async (p) => { setLoading(true); await wait(400); setPage(p); setLoading(false); };
43
+ const runExport = async () => {
44
+ for (const pct of [0, 20, 40, 60, 80, 100]) { setExporting(pct); await wait(250); }
45
+ setExporting(null);
46
+ setLog((l) => [{ id: 'x' + l.length, actor: 'Maya Chen', action: 'export', resource: 'Audit log', sentence: `Maya Chen exported ${matching.length} audit log events as CSV`, at: '2026-10-29T09:00:00Z', ip: '81.2.69.4', before: {}, after: { file: 'audit-log.csv' } }, ...l]);
47
+ A.toast({ title: 'Your export is ready', description: `${matching.length} events, audit-log.csv`, tone: 'positive', duration: 0, action: { label: 'Download', onClick: () => {} } });
48
+ };
49
+ return (
50
+ <main className="al-page">
51
+ <h2 className="al-h">Audit log</h2>
52
+ <p className="al-note">Every change in the Acme workspace, kept for 90 days on the Pro plan. Older events are deleted.</p>
53
+ <div className="al-tools">
54
+ <A.FilterBar aria-label="Filter events" fields={FIELDS} value={filters} onValueChange={(f) => { setFilters(f); setPage(1); }} resultCount={matching.length} />
55
+ <A.Button onClick={runExport} disabled={exporting !== null}>Export CSV</A.Button>
56
+ </div>
57
+ {exporting !== null && <div className="al-export"><A.Progress value={exporting} aria-label="Preparing export" /><span role="status">Preparing export, {exporting}%</span></div>}
58
+ <A.DataTable aria-label="Audit log events" noun="events" rows={rows} getRowId={(r) => r.id} getRowLabel={(r) => r.sentence}
59
+ empty="No events match these filters. Remove a filter to see more."
60
+ columns={[
61
+ { key: 'sentence', header: 'Event', width: '46%' },
62
+ { key: 'actor', header: 'Actor' },
63
+ { key: 'at', header: 'Time', cell: (r) => <A.RelativeTime date={r.at} locale="en-IE" /> },
64
+ { key: 'ip', header: 'IP address' },
65
+ ]}
66
+ renderExpanded={(r) => <A.DiffView mode="properties" before={r.before} after={r.after} caption={`${r.resource}: what changed`} />}
67
+ pagination={{ page, pageCount, total: matching.length, onPageChange: turn, loading, noun: 'events' }} />
68
+ <section className="al-record" aria-labelledby="al-activity-h">
69
+ <h3 id="al-activity-h" className="al-sub">Activity on Q4 plan</h3>
70
+ <A.Timeline aria-label="Activity on Q4 plan" groupBy="none" locale="en-IE">
71
+ <A.TimelineItem key="a1" time="2026-10-28T15:10:00Z">Theo Marsh changed the due date to 15 December</A.TimelineItem>
72
+ <A.TimelineItem key="a2" time="2026-10-27T11:02:00Z" detail={<A.DiffView mode="properties" before={{ owner: 'Maya Chen' }} after={{ owner: 'Ines Duarte' }} />}>Maya Chen made Ines Duarte the owner</A.TimelineItem>
73
+ <A.TimelineItem key="a3" time="2026-10-26T09:45:00Z" href="#/docs/q4">Ines Duarte linked the Q4 pricing doc</A.TimelineItem>
74
+ </A.Timeline>
75
+ </section>
76
+ <A.Toaster />
77
+ </main>
78
+ );
79
+ }
80
+ // preview:end
81
+ export default App;
@@ -0,0 +1,150 @@
1
+ // Auth pattern (project/framework/patterns/auth.md): sign in on a raised Card (level 1, never a Dialog), with SSO, a
2
+ // password, or an emailed link; sign up and verify the email; two-factor with a one-time code or a recovery code; reset. The copy never reveals
3
+ // whether an account exists, rate limits are said with the wait, nothing is submitted for the person, and the email
4
+ // is kept across steps. Demo: any email; the password is "altitude"; the code is 123456.
5
+ // The lines between the preview markers are its "AuthPattern" preview (scripts/pattern-previews.mjs copies them);
6
+ // build/checks/pattern-types.mjs type-checks this file against the public declarations and fails if the two differ.
7
+ import * as React from 'react';
8
+ import * as A from 'altitude-design';
9
+ // preview:start
10
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
11
+ const isEmail = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
12
+ const RULES = [{ label: 'At least 12 characters', test: (v) => v.length >= 12 }, { label: 'Not just letters', test: (v) => /[^a-z]/i.test(v) }];
13
+ function App() {
14
+ const [step, setStep] = React.useState('sign-in');
15
+ const [email, setEmail] = React.useState('');
16
+ const [password, setPassword] = React.useState('');
17
+ const [name, setName] = React.useState('');
18
+ const [code, setCode] = React.useState('');
19
+ const [recovery, setRecovery] = React.useState('');
20
+ const [trust, setTrust] = React.useState(false);
21
+ const [busy, setBusy] = React.useState('');
22
+ const [errors, setErrors] = React.useState(/** @type {{ id: string, label: string, message: string }[]} */ ([]));
23
+ const [tries, setTries] = React.useState(0);
24
+ const [submits, setSubmits] = React.useState(0);
25
+ const [resendIn, setResendIn] = React.useState(0);
26
+ const heading = React.useRef(/** @type {HTMLHeadingElement | null} */ (null));
27
+ React.useEffect(() => { if (resendIn <= 0) return; const t = setTimeout(() => setResendIn((s) => s - 1), 1000); return () => clearTimeout(t); }, [resendIn]);
28
+ const go = (next) => { setStep(next); setErrors([]); setBusy(''); requestAnimationFrame(() => heading.current?.focus()); };
29
+ const fail = (list) => { setErrors(list); setSubmits((n) => n + 1); };
30
+ const signIn = async (e) => {
31
+ e.preventDefault();
32
+ const list = [];
33
+ if (!isEmail(email)) list.push({ id: 'au-email', label: 'Email', message: 'Enter your email address, like name@example.com.' });
34
+ if (!password) list.push({ id: 'au-password', label: 'Password', message: 'Enter your password.' });
35
+ if (list.length) return fail(list);
36
+ if (tries >= 3) return fail([{ id: 'au-password', label: 'Password', message: 'Too many attempts. Wait 5 minutes, then try again, or reset your password.' }]);
37
+ setBusy('password'); await wait(800); setBusy('');
38
+ if (password !== 'altitude') { setTries((n) => n + 1); setPassword(''); return fail([{ id: 'au-password', label: 'Password', message: 'That email and password don’t match. Try again or reset your password.' }]); }
39
+ go('two-factor');
40
+ };
41
+ const sendLink = async (next) => {
42
+ if (!isEmail(email)) return fail([{ id: 'au-email', label: 'Email', message: 'Enter your email address, like name@example.com.' }]);
43
+ setBusy('link'); await wait(700); setResendIn(30); go(next);
44
+ };
45
+ const signUp = async (e) => {
46
+ e.preventDefault();
47
+ const list = [];
48
+ if (!name.trim()) list.push({ id: 'au-name', label: 'Name', message: 'Enter your name.' });
49
+ if (!isEmail(email)) list.push({ id: 'au-email', label: 'Email', message: 'Enter your email address, like name@example.com.' });
50
+ if (!RULES.every((r) => r.test(password))) list.push({ id: 'au-password', label: 'Password', message: 'Choose a password that meets both requirements.' });
51
+ if (list.length) return fail(list);
52
+ setBusy('password'); await wait(800); setCode(''); setResendIn(30); go('verify');
53
+ };
54
+ const verify = async (value) => {
55
+ setBusy('code'); await wait(700);
56
+ if (value !== '123456') { setBusy(''); setCode(''); return fail([{ id: 'au-code', label: 'Code', message: 'That code didn’t work. Check your authenticator app and enter the newest code.' }]); }
57
+ go('done');
58
+ };
59
+ const field = (id) => errors.find((x) => x.id === id)?.message;
60
+ const resend = <p className="au-note">{resendIn > 0 ? `You can resend the email in ${resendIn} ${resendIn === 1 ? 'second' : 'seconds'}.` : <A.Button variant="ghost" size="sm" onClick={() => sendLink(step)}>Resend the email</A.Button>}</p>;
61
+ return (
62
+ <main className="au-page">
63
+ <A.Card className="au-card">
64
+ {step === 'sign-in' && <>
65
+ <h2 ref={heading} tabIndex={-1} className="au-h">Sign in to Acme</h2>
66
+ <A.Button className="au-wide" onClick={() => go('sso')}>Continue with single sign-on (SSO)</A.Button>
67
+ <A.Separator label="or" />
68
+ <form className="au-form" noValidate onSubmit={signIn} aria-labelledby="au-pw-h">
69
+ <span id="au-pw-h" hidden>Sign in with a password</span>
70
+ <A.ErrorSummary errors={errors} focusKey={submits} title="We couldn’t sign you in" />
71
+ <A.Field label="Email" error={field('au-email')}><A.Input id="au-email" type="email" autoComplete="username" value={email} onChange={(e) => setEmail(e.target.value)} /></A.Field>
72
+ <A.Field label="Password" error={field('au-password')}><A.PasswordInput id="au-password" autoComplete="current-password" value={password} onChange={(e) => setPassword(e.target.value)} /></A.Field>
73
+ <A.Button type="submit" variant="primary" className="au-wide" loading={busy === 'password'}>{busy === 'password' ? 'Signing in' : 'Sign in'}</A.Button>
74
+ </form>
75
+ <div className="au-links"><A.Button variant="ghost" size="sm" loading={busy === 'link'} onClick={() => sendLink('link-sent')}>Email me a sign-in link</A.Button><A.Button variant="ghost" size="sm" onClick={() => go('reset')}>Forgot your password?</A.Button></div>
76
+ <p className="au-note">New to Acme? <A.Button variant="ghost" size="sm" onClick={() => { setPassword(''); go('sign-up'); }}>Create an account</A.Button></p>
77
+ </>}
78
+ {step === 'sign-up' && <>
79
+ <h2 ref={heading} tabIndex={-1} className="au-h">Create your Acme account</h2>
80
+ <form className="au-form" noValidate onSubmit={signUp} aria-label="Create an account">
81
+ <A.ErrorSummary errors={errors} focusKey={submits} />
82
+ <A.Field label="Name" error={field('au-name')}><A.Input id="au-name" autoComplete="name" value={name} onChange={(e) => setName(e.target.value)} /></A.Field>
83
+ <A.Field label="Email" error={field('au-email')}><A.Input id="au-email" type="email" autoComplete="email" value={email} onChange={(e) => setEmail(e.target.value)} /></A.Field>
84
+ <A.Field label="Password" error={field('au-password')}><A.PasswordInput id="au-password" autoComplete="new-password" requirements={RULES} value={password} onChange={(e) => setPassword(e.target.value)} /></A.Field>
85
+ <A.Button type="submit" variant="primary" className="au-wide" loading={busy === 'password'}>{busy === 'password' ? 'Creating account' : 'Create account'}</A.Button>
86
+ </form>
87
+ <A.Button variant="ghost" size="sm" onClick={() => go('sign-in')}>I already have an account</A.Button>
88
+ </>}
89
+ {step === 'verify' && <>
90
+ <h2 ref={heading} tabIndex={-1} className="au-h">Verify your email</h2>
91
+ <p>We sent a 6-digit code to <strong>{email}</strong>. It works for 10 minutes.</p>
92
+ <A.ErrorSummary errors={errors} focusKey={submits} title="We couldn’t verify that code" />
93
+ <A.Field label="Verification code" error={field('au-code')}><A.InputOTP id="au-code" maxLength={6} value={code} onChange={setCode} verifying={busy === 'code'} autoComplete="one-time-code" /></A.Field>
94
+ <A.Button variant="primary" className="au-wide" loading={busy === 'code'} disabled={code.length < 6} onClick={() => verify(code)}>{busy === 'code' ? 'Verifying' : 'Verify email'}</A.Button>
95
+ {resend}
96
+ <A.Button variant="ghost" size="sm" onClick={() => go('sign-up')}>Change email address</A.Button>
97
+ </>}
98
+ {step === 'link-sent' && <>
99
+ <h2 ref={heading} tabIndex={-1} className="au-h">Check your email</h2>
100
+ <p>If <strong>{email}</strong> has an Acme account, we’ve sent a sign-in link to it. The link works once, for 15 minutes.</p>
101
+ {resend}
102
+ <A.Button variant="ghost" size="sm" onClick={() => go('sign-in')}>Use a different email address</A.Button>
103
+ </>}
104
+ {step === 'sso' && <>
105
+ <h2 ref={heading} tabIndex={-1} className="au-h">Sign in with SSO</h2>
106
+ <A.Field label="Work email" hint="We’ll send you to your company’s sign-in page."><A.Input type="email" autoComplete="username" value={email} onChange={(e) => setEmail(e.target.value)} /></A.Field>
107
+ <A.Button variant="primary" className="au-wide" onClick={() => go('sign-in')}>Continue</A.Button>
108
+ <A.Button variant="ghost" size="sm" onClick={() => go('sign-in')}>Back to sign in</A.Button>
109
+ </>}
110
+ {step === 'two-factor' && <>
111
+ <h2 ref={heading} tabIndex={-1} className="au-h">Enter your code</h2>
112
+ <p>Open your authenticator app and enter the 6-digit code for Acme.</p>
113
+ <A.ErrorSummary errors={errors} focusKey={submits} title="We couldn’t verify that code" />
114
+ <A.Field label="Authentication code" error={field('au-code')}>
115
+ <A.InputOTP id="au-code" maxLength={6} value={code} onChange={setCode} verifying={busy === 'code'} autoComplete="one-time-code" />
116
+ </A.Field>
117
+ <A.Checkbox label="Trust this device for 30 days" checked={trust} onCheckedChange={setTrust} />
118
+ <A.Button variant="primary" className="au-wide" loading={busy === 'code'} disabled={code.length < 6} onClick={() => verify(code)}>{busy === 'code' ? 'Verifying' : 'Verify'}</A.Button>
119
+ <A.Button variant="ghost" size="sm" onClick={() => go('recovery')}>Use a recovery code instead</A.Button>
120
+ </>}
121
+ {step === 'recovery' && <>
122
+ <h2 ref={heading} tabIndex={-1} className="au-h">Use a recovery code</h2>
123
+ <A.Field label="Recovery code" hint="One of the codes you saved when you set up two-factor. Each works once."><A.Input autoComplete="off" value={recovery} onChange={(e) => setRecovery(e.target.value)} /></A.Field>
124
+ <A.Button variant="primary" className="au-wide" onClick={() => go('done')}>Verify</A.Button>
125
+ <A.Button variant="ghost" size="sm" onClick={() => go('two-factor')}>Use your authenticator app</A.Button>
126
+ </>}
127
+ {step === 'reset' && <>
128
+ <h2 ref={heading} tabIndex={-1} className="au-h">Reset your password</h2>
129
+ <A.ErrorSummary errors={errors} focusKey={submits} />
130
+ <A.Field label="Email" error={field('au-email')}><A.Input id="au-email" type="email" autoComplete="username" value={email} onChange={(e) => setEmail(e.target.value)} /></A.Field>
131
+ <A.Button variant="primary" className="au-wide" loading={busy === 'link'} onClick={() => sendLink('reset-sent')}>Send reset link</A.Button>
132
+ <A.Button variant="ghost" size="sm" onClick={() => go('sign-in')}>Back to sign in</A.Button>
133
+ </>}
134
+ {step === 'reset-sent' && <>
135
+ <h2 ref={heading} tabIndex={-1} className="au-h">Check your email</h2>
136
+ <p>If <strong>{email}</strong> has an Acme account, we’ve sent a link to reset its password. The link works once, for 1 hour.</p>
137
+ {resend}
138
+ <A.Button variant="ghost" size="sm" onClick={() => go('sign-in')}>Back to sign in</A.Button>
139
+ </>}
140
+ {step === 'done' && <>
141
+ <h2 ref={heading} tabIndex={-1} className="au-h">You’re signed in</h2>
142
+ <p>{trust ? 'This device won’t ask for a code for 30 days.' : 'We’ll ask for a code next time you sign in on this device.'}</p>
143
+ <A.Button variant="ghost" size="sm" onClick={() => { setPassword(''); setCode(''); setTries(0); go('sign-in'); }}>Sign out</A.Button>
144
+ </>}
145
+ </A.Card>
146
+ </main>
147
+ );
148
+ }
149
+ // preview:end
150
+ export default App;
@@ -0,0 +1,96 @@
1
+ // Billing pattern (project/framework/patterns/billing.md): usage against plan limits as readings (Meters, never
2
+ // Progress), history with the rest of the cycle as Projected, an estimate that says how it's computed, a plan change
3
+ // reviewed with its proration before it's confirmed (a downgrade lists what will be lost), a locked feature, a failed
4
+ // payment as a page banner, and invoices.
5
+ // The lines between the preview markers are its "BillingPattern" preview (scripts/pattern-previews.mjs copies them);
6
+ // build/checks/pattern-types.mjs type-checks this file against the public declarations and fails if the two differ.
7
+ import * as React from 'react';
8
+ import * as A from 'altitude-design';
9
+ // preview:start
10
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
11
+ const PLANS = [
12
+ { id: 'free', name: 'Free', price: 0, currency: 'EUR', unit: 'per seat', features: ['3 projects', 'Community support'] },
13
+ { id: 'pro', name: 'Pro', price: { month: 2400, year: 2000 }, currency: 'EUR', unit: 'per seat', features: ['Unlimited projects', 'SAML SSO', 'Audit log'] },
14
+ { id: 'business', name: 'Business', price: { month: 4800, year: 4000 }, currency: 'EUR', unit: 'per seat', features: ['Everything in Pro', 'Custom roles', '99.9% uptime SLA'] },
15
+ ];
16
+ const LOSSES = { free: ['SAML SSO, which 8 members sign in with', 'The audit log and its 90 days of history', '9 of your 12 projects become read-only'], pro: ['Custom roles: 2 members on the “Finance” role move to Member'] };
17
+ const eur = (minor) => A.formatNumber(minor, { format: 'currency', currency: 'EUR', locale: 'en-IE' });
18
+ const usage = [
19
+ { week: '2026-10-05', calls: 210000 }, { week: '2026-10-12', calls: 260000 }, { week: '2026-10-19', calls: 310000 },
20
+ { week: '2026-10-26', calls: 290000 }, { week: '2026-11-02', calls: 300000 }, { week: '2026-11-09', calls: 320000 },
21
+ ];
22
+ function PlanChange({ current, period, onPeriod, onChanged, onAgain }) {
23
+ const rank = (id) => PLANS.findIndex((p) => p.id === id);
24
+ const flow = A.useReviewSubmit({
25
+ initialValue: { plan: current }, validate: (d) => (d.plan === current ? { plan: 'Choose a plan you’re not on.' } : {}),
26
+ submit: async (d) => { await wait(900); return { ok: true, receipt: { plan: d.plan } }; },
27
+ messages: { submitting: 'Changing plan…', complete: 'Plan changed', failed: 'We couldn’t change your plan. Nothing was charged. Try again.' },
28
+ });
29
+ React.useEffect(() => { if (flow.phase === 'review' || flow.phase === 'complete') document.getElementById('bl-plan-h')?.focus(); if (flow.phase === 'complete' && flow.receipt) onChanged(flow.receipt.plan); }, [flow.phase]);
30
+ const next = PLANS.find((p) => p.id === flow.draft.plan) || PLANS[0], down = rank(flow.draft.plan) < rank(current);
31
+ const per = (p) => (typeof p.price === 'number' ? p.price : p.price[period]) * 10;
32
+ const today = Math.max(0, Math.round((per(next) - per(PLANS[rank(current)])) * 0.35));
33
+ return (
34
+ <section className="bl-section" aria-labelledby="bl-plan-h">
35
+ <h3 id="bl-plan-h" tabIndex={-1} className="bl-sub">{flow.phase === 'complete' ? `You’re on ${next.name}` : flow.phase === 'editing' ? 'Change plan' : down ? `Downgrade to ${next.name}?` : `Upgrade to ${next.name}?`}</h3>
36
+ {flow.phase === 'editing' && <>
37
+ <A.PlanPicker plans={PLANS} value={flow.draft.plan} onValueChange={(v) => flow.setField('plan', v)} current={current} recommended="business" period={period} onPeriodChange={onPeriod} locale="en-IE" aria-label="Plans" />
38
+ {flow.errors.plan && <p className="bl-note" role="alert">{flow.errors.plan}</p>}
39
+ <div><A.Button variant="primary" onClick={flow.review}>Review change</A.Button></div>
40
+ </>}
41
+ {(flow.phase === 'review' || flow.phase === 'submitting') && <>
42
+ <p className="bl-proration">{down ? `Your plan changes to ${next.name} at the end of this cycle, on 30 November. Then you’ll pay ${eur(per(next))}/${period}.` : `You’ll pay ${eur(today)} today for the rest of this cycle, then ${eur(per(next))}/${period} for 10 seats.`}</p>
43
+ {down && <div className="bl-losses"><strong>You’ll lose</strong><ul>{(LOSSES[next.id] || []).map((l) => <li key={l}>{l}</li>)}</ul></div>}
44
+ <div className="bl-actions"><A.Button disabled={flow.phase === 'submitting'} onClick={flow.correct}>Choose another plan</A.Button>
45
+ <A.Button variant={down ? 'danger' : 'primary'} loading={flow.phase === 'submitting'} onClick={() => void flow.submit()}>{flow.phase === 'submitting' ? 'Changing plan' : down ? `Downgrade to ${next.name}` : `Upgrade to ${next.name}`}</A.Button></div>
46
+ </>}
47
+ {flow.phase === 'complete' && <p className="bl-note">{down ? `${next.name} starts on 30 November. Until then everything on your current plan keeps working.` : `${next.name} features are on now. A receipt is on its way to finance@acme.dev.`}</p>}
48
+ {flow.phase === 'complete' && <div><A.Button variant="ghost" onClick={onAgain}>Change plan again</A.Button></div>}
49
+ <A.WorkflowStatus>{flow.status}</A.WorkflowStatus>
50
+ </section>
51
+ );
52
+ }
53
+ function App() {
54
+ const [current, setCurrent] = React.useState('pro');
55
+ const [period, setPeriod] = React.useState('month');
56
+ const [attempt, setAttempt] = React.useState(0);
57
+ const [failed, setFailed] = React.useState(false);
58
+ return (
59
+ <main className="bl-page">
60
+ {failed && <A.Alert placement="page" tone="critical" variant="raised" title="Your last payment failed"
61
+ action={<A.Button size="sm" onClick={() => setFailed(false)}>Update payment method</A.Button>}>We couldn’t charge the card ending 4242 on 1 November. Update it by 8 November to keep your workspace active.</A.Alert>}
62
+ <h2 className="bl-h">Billing</h2>
63
+ <A.Switch label="Simulate a failed payment" checked={failed} onCheckedChange={setFailed} />
64
+ <section className="bl-section" aria-labelledby="bl-usage-h">
65
+ <h3 id="bl-usage-h" className="bl-sub">Usage this cycle</h3>
66
+ <div className="bl-meters">
67
+ <A.Meter label="Seats" value={8} max={10} high={8} format={(v) => String(v)} />
68
+ <A.Meter label="Storage" value={62} max={100} high={80} format={(v) => `${v} GB`} />
69
+ <A.Meter label="API calls" value={1.2} max={1} high={0.8} format={(v) => `${v}M`} />
70
+ </div>
71
+ <A.Stat label="Estimated next bill" value={28800} format={eur} help="10 seats × €24, plus €48 for 0.2M API calls over the 1M included. Updated hourly; the final amount is on your invoice." />
72
+ <A.ChartContainer aria-labelledby="bl-calls-h" config={{ calls: { label: 'API calls', format: (v) => A.formatNumber(v, { format: 'compact', locale: 'en-IE' }) } }} height={180}>
73
+ <A.BarChart data={usage} x="week" status={(d) => (String(d.week) > '2026-10-30' ? 'projected' : undefined)} />
74
+ </A.ChartContainer>
75
+ <p id="bl-calls-h" className="bl-note">API calls per week. The last two weeks of the cycle are projected.</p>
76
+ </section>
77
+ <PlanChange key={attempt} current={current} period={period} onPeriod={setPeriod} onChanged={setCurrent} onAgain={() => setAttempt((n) => n + 1)} />
78
+ <section className="bl-section" aria-labelledby="bl-roles-h">
79
+ <h3 id="bl-roles-h" className="bl-sub">Custom roles</h3>
80
+ <A.FeatureGate requires="Business" description="Create roles with exactly the permissions a team needs." onUpgrade={() => document.getElementById('bl-plan-h')?.focus()}>
81
+ <A.Button>Create a role</A.Button>
82
+ </A.FeatureGate>
83
+ </section>
84
+ <section className="bl-section" aria-labelledby="bl-inv-h">
85
+ <h3 id="bl-inv-h" className="bl-sub">Invoices</h3>
86
+ <A.Table aria-labelledby="bl-inv-h">
87
+ <A.TableHeader><A.TableRow><A.TableHead>Date</A.TableHead><A.TableHead>Invoice</A.TableHead><A.TableHead align="end">Amount</A.TableHead><A.TableHead>Status</A.TableHead></A.TableRow></A.TableHeader>
88
+ <A.TableBody>{[{ d: '1 Oct 2026', n: 'INV-0042', a: 26400, s: 'Paid' }, { d: '1 Sep 2026', n: 'INV-0041', a: 24000, s: 'Paid' }, { d: '1 Aug 2026', n: 'INV-0040', a: 24000, s: 'Refunded' }].map(({ d, n, a, s }) =>
89
+ <A.TableRow key={n}><A.TableCell>{d}</A.TableCell><A.TableCell><A.Link href={'#/invoices/' + n}>{n}</A.Link></A.TableCell><A.TableCell align="end"><A.FormattedNumber value={a} format="currency" currency="EUR" locale="en-IE" /></A.TableCell><A.TableCell><A.Badge tone={s === 'Paid' ? 'positive' : 'neutral'}>{s}</A.Badge></A.TableCell></A.TableRow>)}</A.TableBody>
90
+ </A.Table>
91
+ </section>
92
+ </main>
93
+ );
94
+ }
95
+ // preview:end
96
+ export default App;
@@ -0,0 +1,95 @@
1
+ // Developer settings pattern (project/framework/patterns/developer-settings.md): API keys created in a dialog that can't
2
+ // close until the secret is saved, listed masked with when each was last used, rolled with an overlap said in words and
3
+ // revoked with what breaks; webhooks whose deliveries are a Timeline, a retry hovering as a pending write and a test
4
+ // event's payload shown in a CodeBlock; integrations as interactive cards, the OAuth handoff pending, disconnect named.
5
+ // The lines between the preview markers are its "DeveloperSettingsPattern" preview (scripts/pattern-previews.mjs copies
6
+ // them); build/checks/pattern-types.mjs type-checks this file against the public declarations and fails if they differ.
7
+ import * as React from 'react';
8
+ import * as A from 'altitude-design';
9
+ // preview:start
10
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
11
+ const PAYLOAD = JSON.stringify({ id: 'evt_EXAMPLE_1042', type: 'invoice.paid', created: '2026-10-29T09:12:00Z', data: { invoice: 'INV-0042', amount: 26400, currency: 'eur' } }, null, 2);
12
+ function App() {
13
+ const [keys, setKeys] = React.useState([{ id: 'k1', name: 'Production', tail: '4f2a', used: 'Used 3 minutes ago by 3 services' }, { id: 'k2', name: 'CI pipeline', tail: '8d0e', used: 'Used yesterday' }]);
14
+ const [creating, setCreating] = React.useState(/** @type {null | { name: string, secret?: string, saved?: boolean }} */ (null));
15
+ const [confirm, setConfirm] = React.useState(/** @type {null | { kind: 'roll' | 'revoke' | 'disconnect', id: string, busy?: boolean }} */ (null));
16
+ const [status, setStatus] = React.useState('');
17
+ const [deliveries, setDeliveries] = React.useState(/** @type {{ id: string, event: string, at: string, result: 'Delivered' | 'Failed', status?: 'pending' | 'committed' | 'failed', attempt: number }[]} */ ([
18
+ { id: 'd1', event: 'invoice.paid', at: '2026-10-29T09:12:00Z', result: 'Delivered', attempt: 1 },
19
+ { id: 'd2', event: 'customer.updated', at: '2026-10-29T08:40:00Z', result: 'Failed', status: 'failed', attempt: 3 },
20
+ ]));
21
+ const [test, setTest] = React.useState(false);
22
+ const [apps, setApps] = React.useState([{ id: 'chat', name: 'Team chat', about: 'Post alerts to a channel.', state: 'connected' }, { id: 'cal', name: 'Calendar sync', about: 'Add due dates to calendars.', state: 'idle' }, { id: 'store', name: 'Cloud storage', about: 'Attach files from your drive.', state: 'idle' }]);
23
+ const heading = React.useRef(/** @type {HTMLHeadingElement | null} */ (null));
24
+ const key = confirm && keys.find((k) => k.id === confirm.id);
25
+ const app = confirm && apps.find((a) => a.id === confirm.id);
26
+ const create = async () => { if (!creating) return; await wait(500); setCreating({ ...creating, secret: 'ak_live_EXAMPLE_7Hq2Xv9LmR3p' }); };
27
+ const finishCreate = () => { if (!creating) return; setKeys((l) => [...l, { id: 'k' + (l.length + 1), name: creating.name || 'Untitled key', tail: '3p0K', used: 'Never used' }]); setStatus(`Key “${creating.name || 'Untitled key'}” created.`); setCreating(null); };
28
+ const retry = async (id) => {
29
+ setDeliveries((l) => l.map((d) => (d.id === id ? { ...d, status: 'pending', attempt: d.attempt + 1 } : d)));
30
+ await wait(1000);
31
+ setDeliveries((l) => l.map((d) => (d.id === id ? { ...d, status: 'committed', result: 'Delivered' } : d)));
32
+ };
33
+ const act = async () => {
34
+ if (!confirm) return;
35
+ setConfirm({ ...confirm, busy: true }); await wait(700);
36
+ if (confirm.kind === 'revoke') { setKeys((l) => l.filter((k) => k.id !== confirm.id)); setStatus(`Key “${key?.name}” revoked. Requests that use it now fail.`); }
37
+ if (confirm.kind === 'roll') { setKeys((l) => l.map((k) => (k.id === confirm.id ? { ...k, tail: 'Zx81', used: 'New key. The old one works until tomorrow, 09:12' } : k))); setStatus(`Key “${key?.name}” rolled. The old key keeps working for 24 hours.`); }
38
+ if (confirm.kind === 'disconnect') { setApps((l) => l.map((a) => (a.id === confirm.id ? { ...a, state: 'idle' } : a))); setStatus(`${app?.name} disconnected.`); }
39
+ setConfirm(null); requestAnimationFrame(() => heading.current?.focus());
40
+ };
41
+ const connect = async (id) => {
42
+ setApps((l) => l.map((a) => (a.id === id ? { ...a, state: 'pending' } : a)));
43
+ await wait(1200);
44
+ setApps((l) => l.map((a) => (a.id === id ? { ...a, state: 'connected' } : a)));
45
+ };
46
+ return (
47
+ <main className="dv-page">
48
+ <h2 ref={heading} tabIndex={-1} className="dv-h">Developers</h2>
49
+ <section className="dv-section" aria-labelledby="dv-keys-h">
50
+ <div className="dv-row"><h3 id="dv-keys-h" className="dv-sub">API keys</h3><A.Button size="sm" onClick={() => setCreating({ name: '' })}>Create key</A.Button></div>
51
+ <A.Table aria-labelledby="dv-keys-h">
52
+ <A.TableHeader><A.TableRow><A.TableHead>Name</A.TableHead><A.TableHead>Key</A.TableHead><A.TableHead>Last used</A.TableHead><A.TableHead align="end"><span className="dv-vh">Actions</span></A.TableHead></A.TableRow></A.TableHeader>
53
+ <A.TableBody>{keys.map((k) => <A.TableRow key={k.id}><A.TableCell>{k.name}</A.TableCell><A.TableCell><code className="dv-mask">ak_live_••••{k.tail}</code></A.TableCell><A.TableCell className="dv-muted">{k.used}</A.TableCell>
54
+ <A.TableCell align="end"><div className="dv-actions"><A.Button size="sm" variant="ghost" aria-label={`Roll ${k.name} key`} onClick={() => setConfirm({ kind: 'roll', id: k.id })}>Roll</A.Button><A.Button size="sm" variant="ghost" aria-label={`Revoke ${k.name} key`} onClick={() => setConfirm({ kind: 'revoke', id: k.id })}>Revoke</A.Button></div></A.TableCell></A.TableRow>)}</A.TableBody>
55
+ </A.Table>
56
+ </section>
57
+ <section className="dv-section" aria-labelledby="dv-hooks-h">
58
+ <h3 id="dv-hooks-h" className="dv-sub">Webhooks</h3>
59
+ <A.Card className="dv-hook">
60
+ <div className="dv-row"><code>https://api.acme.dev/hooks/altitude</code><A.Badge tone="positive">Active</A.Badge></div>
61
+ <div className="dv-events">{['invoice.paid', 'customer.updated', 'member.removed'].map((e) => <A.Checkbox key={e} label={e} defaultChecked={e !== 'member.removed'} />)}</div>
62
+ <A.Timeline aria-label="Recent deliveries" groupBy="none" locale="en-IE">
63
+ {deliveries.map((d) => <A.TimelineItem key={d.id} time={d.at} status={d.status} onRetry={d.result === 'Failed' ? () => retry(d.id) : undefined}>
64
+ <span className="dv-delivery"><code>{d.event}</code> <A.Badge tone={d.status === 'pending' ? 'neutral' : d.result === 'Delivered' ? 'positive' : 'critical'}>{d.status === 'pending' ? `Retrying (attempt ${d.attempt} of 5)` : d.result}</A.Badge></span>
65
+ </A.TimelineItem>)}
66
+ </A.Timeline>
67
+ <div><A.Button size="sm" onClick={() => setTest(true)}>Send test event</A.Button></div>
68
+ {test && <A.CodeBlock title="Test event sent: invoice.paid" language="json" code={PAYLOAD} />}
69
+ </A.Card>
70
+ </section>
71
+ <section className="dv-section" aria-labelledby="dv-apps-h" data-tells-ignore="LA4 installed integrations are peers; none should lead">
72
+ <h3 id="dv-apps-h" className="dv-sub">Integrations</h3>
73
+ <div className="dv-apps">{apps.map((a) => <A.Card key={a.id} className="dv-app" status={a.state === 'pending' ? 'pending' : undefined}>
74
+ <div className="dv-row"><strong>{a.name}</strong>{a.state === 'connected' && <A.Badge tone="positive">Connected</A.Badge>}</div>
75
+ <p className="dv-muted">{a.about}</p>
76
+ {a.state === 'connected' ? <A.Button size="sm" variant="ghost" onClick={() => setConfirm({ kind: 'disconnect', id: a.id })}>Disconnect</A.Button>
77
+ : <A.Button size="sm" loading={a.state === 'pending'} onClick={() => connect(a.id)}>{a.state === 'pending' ? 'Waiting for authorization' : `Connect ${a.name}`}</A.Button>}
78
+ </A.Card>)}</div>
79
+ </section>
80
+ <p className="dv-muted" role="status">{status}</p>
81
+ <A.Dialog open={!!creating} dismissible={!creating?.secret || !!creating?.saved} onOpenChange={(o) => { if (!o) setCreating(null); }} title={creating?.secret ? 'Save your new key' : 'Create an API key'}
82
+ footer={creating?.secret ? <A.Button variant="primary" disabled={!creating.saved} onClick={finishCreate}>Done</A.Button> : <><A.Button onClick={() => setCreating(null)}>Cancel</A.Button><A.Button variant="primary" onClick={create}>Create key</A.Button></>}>
83
+ {creating?.secret ? <A.SecretField label="Secret key" value={creating.secret} once warning="You won’t see this key again. Store it somewhere safe, like your secrets manager." onAcknowledge={() => setCreating({ ...creating, saved: true })} />
84
+ : <A.Field label="Key name" hint="Say where it’s used, like “Billing service”."><A.Input value={creating?.name || ''} onChange={(e) => creating && setCreating({ ...creating, name: e.target.value })} /></A.Field>}
85
+ </A.Dialog>
86
+ <A.Dialog open={!!confirm} background="stationary" size="sm" dismissible={!confirm?.busy} onOpenChange={(o) => { if (!o) setConfirm(null); }}
87
+ title={confirm?.kind === 'roll' ? `Roll the ${key?.name} key?` : confirm?.kind === 'revoke' ? `Revoke the ${key?.name} key?` : `Disconnect ${app?.name}?`}
88
+ description={confirm?.kind === 'roll' ? 'You’ll get a new key now. The old key keeps working for 24 hours, so you can update your services first.'
89
+ : confirm?.kind === 'revoke' ? `Requests that use it fail immediately. ${key?.used}.` : `${app?.name} stops receiving updates, and anyone using it loses access to Acme data through it.`}
90
+ footer={<><A.Button disabled={confirm?.busy} onClick={() => setConfirm(null)}>Cancel</A.Button><A.Button variant={confirm?.kind === 'roll' ? 'primary' : 'danger'} loading={confirm?.busy} onClick={act}>{confirm?.kind === 'roll' ? 'Roll key' : confirm?.kind === 'revoke' ? 'Revoke key' : 'Disconnect'}</A.Button></>} />
91
+ </main>
92
+ );
93
+ }
94
+ // preview:end
95
+ export default App;