altitude-design 0.4.0 → 0.4.2

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 (280) hide show
  1. package/README.md +1 -0
  2. package/dist/guidance/components/AILabel/README.md +35 -0
  3. package/dist/guidance/components/Accordion/README.md +25 -0
  4. package/dist/guidance/components/AgentFleet/README.md +48 -1
  5. package/dist/guidance/components/AgentLanes/README.md +32 -0
  6. package/dist/guidance/components/AgentRequests/README.md +47 -0
  7. package/dist/guidance/components/Alert/README.md +49 -0
  8. package/dist/guidance/components/AltitudeProvider/README.md +63 -0
  9. package/dist/guidance/components/AppShell/README.md +70 -0
  10. package/dist/guidance/components/AreaChart/README.md +58 -0
  11. package/dist/guidance/components/Artifact/README.md +69 -0
  12. package/dist/guidance/components/Avatar/README.md +41 -0
  13. package/dist/guidance/components/Await/README.md +48 -0
  14. package/dist/guidance/components/Badge/README.md +44 -0
  15. package/dist/guidance/components/BarChart/README.md +62 -0
  16. package/dist/guidance/components/BarList/README.md +58 -0
  17. package/dist/guidance/components/Board/README.md +67 -0
  18. package/dist/guidance/components/Breadcrumb/README.md +31 -0
  19. package/dist/guidance/components/Button/README.md +41 -0
  20. package/dist/guidance/components/Card/README.md +38 -0
  21. package/dist/guidance/components/ChartBrush/README.md +57 -0
  22. package/dist/guidance/components/ChartContainer/README.md +77 -0
  23. package/dist/guidance/components/ChartLegend/README.md +43 -0
  24. package/dist/guidance/components/ChartReference/README.md +74 -0
  25. package/dist/guidance/components/ChartTooltip/README.md +63 -0
  26. package/dist/guidance/components/Checkbox/README.md +35 -0
  27. package/dist/guidance/components/Checklist/README.md +66 -0
  28. package/dist/guidance/components/CodeBlock/README.md +44 -0
  29. package/dist/guidance/components/ColorPicker/README.md +39 -0
  30. package/dist/guidance/components/Combobox/README.md +35 -0
  31. package/dist/guidance/components/CommandPalette/README.md +177 -0
  32. package/dist/guidance/components/CommentThread/README.md +51 -0
  33. package/dist/guidance/components/Confirmation/README.md +60 -0
  34. package/dist/guidance/components/ContextMenu/README.md +51 -0
  35. package/dist/guidance/components/Conversation/README.md +124 -0
  36. package/dist/guidance/components/CopyButton/README.md +31 -0
  37. package/dist/guidance/components/DataGrid/README.md +75 -0
  38. package/dist/guidance/components/DatePicker/README.md +49 -0
  39. package/dist/guidance/components/Dialog/README.md +175 -0
  40. package/dist/guidance/components/DiffView/README.md +77 -0
  41. package/dist/guidance/components/DotPlot/README.md +44 -0
  42. package/dist/guidance/components/Drilldown/README.md +28 -0
  43. package/dist/guidance/components/DropZone/README.md +50 -0
  44. package/dist/guidance/components/DropdownMenu/README.md +44 -0
  45. package/dist/guidance/components/EditRegion/README.md +159 -0
  46. package/dist/guidance/components/EmptyState/README.md +49 -0
  47. package/dist/guidance/components/FeatureGate/README.md +39 -0
  48. package/dist/guidance/components/Field/README.md +36 -0
  49. package/dist/guidance/components/FilterBar/README.md +63 -0
  50. package/dist/guidance/components/FormattedNumber/README.md +44 -0
  51. package/dist/guidance/components/Heatmap/README.md +71 -0
  52. package/dist/guidance/components/HoldButton/README.md +32 -0
  53. package/dist/guidance/components/HoverCard/README.md +53 -0
  54. package/dist/guidance/components/InlineCitation/README.md +32 -0
  55. package/dist/guidance/components/InlineEdit/README.md +247 -1
  56. package/dist/guidance/components/Input/README.md +36 -0
  57. package/dist/guidance/components/InputOTP/README.md +46 -0
  58. package/dist/guidance/components/ItemGroup/README.md +100 -0
  59. package/dist/guidance/components/Kbd/README.md +27 -0
  60. package/dist/guidance/components/LineChart/README.md +62 -0
  61. package/dist/guidance/components/Link/README.md +41 -0
  62. package/dist/guidance/components/Markdown/README.md +81 -0
  63. package/dist/guidance/components/Meter/README.md +52 -0
  64. package/dist/guidance/components/NotificationCenter/README.md +39 -0
  65. package/dist/guidance/components/NumberInput/README.md +37 -0
  66. package/dist/guidance/components/PageHeader/README.md +45 -0
  67. package/dist/guidance/components/Pagination/README.md +33 -0
  68. package/dist/guidance/components/PasswordInput/README.md +36 -0
  69. package/dist/guidance/components/PermissionMatrix/README.md +40 -0
  70. package/dist/guidance/components/Pile/README.md +42 -0
  71. package/dist/guidance/components/PlanPicker/README.md +37 -0
  72. package/dist/guidance/components/Popover/README.md +36 -0
  73. package/dist/guidance/components/Progress/README.md +31 -0
  74. package/dist/guidance/components/PromptInput/README.md +98 -0
  75. package/dist/guidance/components/PropertyList/README.md +80 -0
  76. package/dist/guidance/components/Question/README.md +54 -0
  77. package/dist/guidance/components/RadioGroup/README.md +37 -0
  78. package/dist/guidance/components/RelativeTime/README.md +47 -0
  79. package/dist/guidance/components/RemovalReceipt/README.md +77 -0
  80. package/dist/guidance/components/Resizable/README.md +29 -0
  81. package/dist/guidance/components/SaveBar/README.md +38 -0
  82. package/dist/guidance/components/SavedViews/README.md +58 -0
  83. package/dist/guidance/components/ScatterChart/README.md +63 -0
  84. package/dist/guidance/components/Schedule/README.md +66 -0
  85. package/dist/guidance/components/ScrollArea/README.md +28 -0
  86. package/dist/guidance/components/SecretField/README.md +46 -0
  87. package/dist/guidance/components/Select/README.md +70 -0
  88. package/dist/guidance/components/Separator/README.md +44 -0
  89. package/dist/guidance/components/Sheet/README.md +94 -0
  90. package/dist/guidance/components/Skeleton/README.md +36 -0
  91. package/dist/guidance/components/Slider/README.md +35 -0
  92. package/dist/guidance/components/SortableList/README.md +34 -0
  93. package/dist/guidance/components/Sparkline/README.md +58 -0
  94. package/dist/guidance/components/SplitButton/README.md +39 -0
  95. package/dist/guidance/components/Stat/README.md +61 -0
  96. package/dist/guidance/components/Stepper/README.md +61 -0
  97. package/dist/guidance/components/Suggestions/README.md +58 -0
  98. package/dist/guidance/components/Surface/README.md +28 -0
  99. package/dist/guidance/components/Switch/README.md +29 -0
  100. package/dist/guidance/components/SyncIndicator/README.md +54 -0
  101. package/dist/guidance/components/Table/README.md +62 -0
  102. package/dist/guidance/components/Tabs/README.md +55 -0
  103. package/dist/guidance/components/TagInput/README.md +50 -0
  104. package/dist/guidance/components/Terminal/README.md +42 -0
  105. package/dist/guidance/components/ThreadList/README.md +73 -0
  106. package/dist/guidance/components/TimeInput/README.md +37 -0
  107. package/dist/guidance/components/Timeline/README.md +55 -0
  108. package/dist/guidance/components/Toaster/README.md +38 -0
  109. package/dist/guidance/components/Toggle/README.md +52 -0
  110. package/dist/guidance/components/Toolbar/README.md +69 -0
  111. package/dist/guidance/components/Tooltip/README.md +29 -0
  112. package/dist/guidance/components/Tour/README.md +44 -0
  113. package/dist/guidance/components/Trace/README.md +92 -0
  114. package/dist/guidance/components/Tree/README.md +85 -0
  115. package/dist/guidance/index.json +117 -117
  116. package/dist/guidance/llms.txt +1 -1
  117. package/dist/guidance/manifest.json +120 -120
  118. package/dist/guidance/visual/ActionsInPlace.md +5 -5
  119. package/dist/guidance/visual/Distinctiveness.md +6 -2
  120. package/dist/guidance/visual/Research.md +14 -2
  121. package/dist/guidance/visual/Theming.md +1 -1
  122. package/dist/types/agent-fleet.d.ts +28 -0
  123. package/dist/types/ai-label.d.ts +7 -0
  124. package/dist/types/app-shell.d.ts +23 -0
  125. package/dist/types/area-chart.d.ts +8 -0
  126. package/dist/types/artifact.d.ts +14 -0
  127. package/dist/types/bar-chart.d.ts +6 -0
  128. package/dist/types/bar-list.d.ts +6 -0
  129. package/dist/types/breadcrumb.d.ts +6 -0
  130. package/dist/types/chart-brush.d.ts +6 -0
  131. package/dist/types/chart.d.ts +29 -1
  132. package/dist/types/checklist.d.ts +6 -0
  133. package/dist/types/code-block.d.ts +6 -0
  134. package/dist/types/collections.d.ts +15 -0
  135. package/dist/types/color-picker.d.ts +6 -0
  136. package/dist/types/command.d.ts +13 -0
  137. package/dist/types/comment-thread.d.ts +11 -0
  138. package/dist/types/confirmation.d.ts +8 -0
  139. package/dist/types/context-menu.d.ts +11 -0
  140. package/dist/types/controls.d.ts +174 -10
  141. package/dist/types/conversation.d.ts +21 -0
  142. package/dist/types/copy-button.d.ts +6 -0
  143. package/dist/types/core.d.ts +31 -0
  144. package/dist/types/data-grid.d.ts +25 -0
  145. package/dist/types/date-picker.d.ts +6 -0
  146. package/dist/types/diff-view.d.ts +8 -0
  147. package/dist/types/dot-plot.d.ts +6 -0
  148. package/dist/types/edit-region.d.ts +23 -0
  149. package/dist/types/empty.d.ts +9 -0
  150. package/dist/types/feature-gate.d.ts +8 -0
  151. package/dist/types/filter-bar.d.ts +8 -0
  152. package/dist/types/formatted-number.d.ts +6 -0
  153. package/dist/types/heatmap.d.ts +6 -0
  154. package/dist/types/inline-citation.d.ts +6 -0
  155. package/dist/types/inline-edit.d.ts +7 -0
  156. package/dist/types/input-otp.d.ts +11 -0
  157. package/dist/types/item-group.d.ts +29 -0
  158. package/dist/types/line-chart.d.ts +6 -0
  159. package/dist/types/link.d.ts +8 -1
  160. package/dist/types/markdown.d.ts +6 -0
  161. package/dist/types/meter.d.ts +6 -0
  162. package/dist/types/nav.d.ts +6 -0
  163. package/dist/types/notifications.d.ts +6 -0
  164. package/dist/types/number-input.d.ts +6 -0
  165. package/dist/types/overlays.d.ts +61 -0
  166. package/dist/types/page-header.d.ts +14 -0
  167. package/dist/types/pagination.d.ts +6 -0
  168. package/dist/types/password-input.d.ts +6 -0
  169. package/dist/types/permission-matrix.d.ts +7 -0
  170. package/dist/types/pile.d.ts +6 -0
  171. package/dist/types/plan-picker.d.ts +6 -0
  172. package/dist/types/prompt-input.d.ts +11 -0
  173. package/dist/types/property-list.d.ts +17 -0
  174. package/dist/types/question.d.ts +11 -0
  175. package/dist/types/relative-time.d.ts +8 -1
  176. package/dist/types/removal-receipt.d.ts +18 -1
  177. package/dist/types/save-bar.d.ts +6 -0
  178. package/dist/types/saved-views.d.ts +12 -0
  179. package/dist/types/scatter-chart.d.ts +8 -0
  180. package/dist/types/schedule.d.ts +21 -0
  181. package/dist/types/secret-field.d.ts +6 -0
  182. package/dist/types/select.d.ts +12 -0
  183. package/dist/types/separator.d.ts +8 -1
  184. package/dist/types/sheet.d.ts +19 -0
  185. package/dist/types/sparkline.d.ts +6 -0
  186. package/dist/types/split-button.d.ts +7 -0
  187. package/dist/types/stat.d.ts +7 -0
  188. package/dist/types/stepper.d.ts +12 -0
  189. package/dist/types/suggestions.d.ts +10 -0
  190. package/dist/types/sync-indicator.d.ts +8 -0
  191. package/dist/types/tag-input.d.ts +8 -0
  192. package/dist/types/terminal.d.ts +6 -0
  193. package/dist/types/thread-list.d.ts +16 -0
  194. package/dist/types/time-input.d.ts +6 -0
  195. package/dist/types/timeline.d.ts +9 -0
  196. package/dist/types/toggle.d.ts +6 -0
  197. package/dist/types/toolbar.d.ts +20 -0
  198. package/dist/types/tour.d.ts +10 -0
  199. package/dist/types/trace.d.ts +16 -0
  200. package/dist/types/tree.d.ts +21 -0
  201. package/dist/types-cjs/agent-fleet.d.cts +28 -0
  202. package/dist/types-cjs/ai-label.d.cts +7 -0
  203. package/dist/types-cjs/app-shell.d.cts +23 -0
  204. package/dist/types-cjs/area-chart.d.cts +8 -0
  205. package/dist/types-cjs/artifact.d.cts +14 -0
  206. package/dist/types-cjs/bar-chart.d.cts +6 -0
  207. package/dist/types-cjs/bar-list.d.cts +6 -0
  208. package/dist/types-cjs/breadcrumb.d.cts +6 -0
  209. package/dist/types-cjs/chart-brush.d.cts +6 -0
  210. package/dist/types-cjs/chart.d.cts +29 -1
  211. package/dist/types-cjs/checklist.d.cts +6 -0
  212. package/dist/types-cjs/code-block.d.cts +6 -0
  213. package/dist/types-cjs/collections.d.cts +15 -0
  214. package/dist/types-cjs/color-picker.d.cts +6 -0
  215. package/dist/types-cjs/command.d.cts +13 -0
  216. package/dist/types-cjs/comment-thread.d.cts +11 -0
  217. package/dist/types-cjs/confirmation.d.cts +8 -0
  218. package/dist/types-cjs/context-menu.d.cts +11 -0
  219. package/dist/types-cjs/controls.d.cts +174 -10
  220. package/dist/types-cjs/conversation.d.cts +21 -0
  221. package/dist/types-cjs/copy-button.d.cts +6 -0
  222. package/dist/types-cjs/core.d.cts +31 -0
  223. package/dist/types-cjs/data-grid.d.cts +25 -0
  224. package/dist/types-cjs/date-picker.d.cts +6 -0
  225. package/dist/types-cjs/diff-view.d.cts +8 -0
  226. package/dist/types-cjs/dot-plot.d.cts +6 -0
  227. package/dist/types-cjs/edit-region.d.cts +23 -0
  228. package/dist/types-cjs/empty.d.cts +9 -0
  229. package/dist/types-cjs/feature-gate.d.cts +8 -0
  230. package/dist/types-cjs/filter-bar.d.cts +8 -0
  231. package/dist/types-cjs/formatted-number.d.cts +6 -0
  232. package/dist/types-cjs/heatmap.d.cts +6 -0
  233. package/dist/types-cjs/inline-citation.d.cts +6 -0
  234. package/dist/types-cjs/inline-edit.d.cts +7 -0
  235. package/dist/types-cjs/input-otp.d.cts +11 -0
  236. package/dist/types-cjs/item-group.d.cts +29 -0
  237. package/dist/types-cjs/line-chart.d.cts +6 -0
  238. package/dist/types-cjs/link.d.cts +8 -1
  239. package/dist/types-cjs/markdown.d.cts +6 -0
  240. package/dist/types-cjs/meter.d.cts +6 -0
  241. package/dist/types-cjs/nav.d.cts +6 -0
  242. package/dist/types-cjs/notifications.d.cts +6 -0
  243. package/dist/types-cjs/number-input.d.cts +6 -0
  244. package/dist/types-cjs/overlays.d.cts +61 -0
  245. package/dist/types-cjs/page-header.d.cts +14 -0
  246. package/dist/types-cjs/pagination.d.cts +6 -0
  247. package/dist/types-cjs/password-input.d.cts +6 -0
  248. package/dist/types-cjs/permission-matrix.d.cts +7 -0
  249. package/dist/types-cjs/pile.d.cts +6 -0
  250. package/dist/types-cjs/plan-picker.d.cts +6 -0
  251. package/dist/types-cjs/prompt-input.d.cts +11 -0
  252. package/dist/types-cjs/property-list.d.cts +17 -0
  253. package/dist/types-cjs/question.d.cts +11 -0
  254. package/dist/types-cjs/relative-time.d.cts +8 -1
  255. package/dist/types-cjs/removal-receipt.d.cts +18 -1
  256. package/dist/types-cjs/save-bar.d.cts +6 -0
  257. package/dist/types-cjs/saved-views.d.cts +12 -0
  258. package/dist/types-cjs/scatter-chart.d.cts +8 -0
  259. package/dist/types-cjs/schedule.d.cts +21 -0
  260. package/dist/types-cjs/secret-field.d.cts +6 -0
  261. package/dist/types-cjs/select.d.cts +12 -0
  262. package/dist/types-cjs/separator.d.cts +8 -1
  263. package/dist/types-cjs/sheet.d.cts +19 -0
  264. package/dist/types-cjs/sparkline.d.cts +6 -0
  265. package/dist/types-cjs/split-button.d.cts +7 -0
  266. package/dist/types-cjs/stat.d.cts +7 -0
  267. package/dist/types-cjs/stepper.d.cts +12 -0
  268. package/dist/types-cjs/suggestions.d.cts +10 -0
  269. package/dist/types-cjs/sync-indicator.d.cts +8 -0
  270. package/dist/types-cjs/tag-input.d.cts +8 -0
  271. package/dist/types-cjs/terminal.d.cts +6 -0
  272. package/dist/types-cjs/thread-list.d.cts +16 -0
  273. package/dist/types-cjs/time-input.d.cts +6 -0
  274. package/dist/types-cjs/timeline.d.cts +9 -0
  275. package/dist/types-cjs/toggle.d.cts +6 -0
  276. package/dist/types-cjs/toolbar.d.cts +20 -0
  277. package/dist/types-cjs/tour.d.cts +10 -0
  278. package/dist/types-cjs/trace.d.cts +16 -0
  279. package/dist/types-cjs/tree.d.cts +21 -0
  280. package/package.json +1 -1
@@ -35,3 +35,47 @@ formatNumber(4213, { format: 'compact' }) // "4.2K"
35
35
  - Do use `formatNumber` for table columns and chart formatters, so a number reads the same everywhere.
36
36
  - Don't animate digits or count up to a new value.
37
37
  - Don't use it for a headline figure; that's a `Stat`.
38
+
39
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
40
+ ## Example
41
+
42
+ `FormattedNumber` as first used in its preview:
43
+
44
+ ```tsx
45
+ import { FormattedNumber } from 'altitude-design';
46
+
47
+ <FormattedNumber value={4900} format="currency" currency="EUR" locale="en-IE" />
48
+ ```
49
+
50
+ The full preview, as rendered in Storybook and checked in CI (`fn`, `cap` are the preview page's own layout classes, not Altitude's):
51
+
52
+ ```tsx
53
+ import * as React from 'react';
54
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle, FormattedNumber } from 'altitude-design';
55
+
56
+ const usage = [4_212_000_000, 4_512_000_000, 5_030_000_000];
57
+ export default function App() {
58
+ const [i, setI] = React.useState(0);
59
+ React.useEffect(() => { const t = setInterval(() => setI((n) => (n + 1) % usage.length), 2600); return () => clearInterval(t); }, []);
60
+ return (
61
+ <div className="fn">
62
+ <dl>
63
+ <dt>Plan</dt><dd><FormattedNumber value={4900} format="currency" currency="EUR" locale="en-IE" /> a month</dd>
64
+ <dt>Storage</dt><dd><FormattedNumber value={4_512_000_000} format="bytes" /></dd>
65
+ <dt>Latency</dt><dd><FormattedNumber value={128} format="duration" /> · <FormattedNumber value={95_000} format="duration" /></dd>
66
+ <dt>Error rate</dt><dd><FormattedNumber value={0.0123} format="percent" maximumFractionDigits={1} /></dd>
67
+ <dt>Requests</dt><dd><FormattedNumber value={4213} format="compact" /></dd>
68
+ <dt>Seats</dt><dd><FormattedNumber value={1250} /></dd>
69
+ </dl>
70
+ <Card>
71
+ <CardHeader><CardTitle>Usage</CardTitle><CardDescription>Refreshes as files are uploaded</CardDescription></CardHeader>
72
+ <CardContent>
73
+ <p>You've used <FormattedNumber value={usage[i]} format="bytes" /> of <FormattedNumber value={10_000_000_000} format="bytes" />.</p>
74
+ <p className="cap">A changed value cross-fades and settles with a tint. It never counts up. Point at "4.2K" or a byte size for its exact value.</p>
75
+ </CardContent>
76
+ </Card>
77
+ </div>
78
+ );
79
+ }
80
+ ```
81
+ <!-- /example -->
@@ -43,3 +43,74 @@ A value per cell across two dimensions: load by hour and weekday, error rate by
43
43
  - Do use `scale="diverging"` only when the midpoint means something (no change, a target).
44
44
  - Don't use a heatmap to compare a handful of values; that's `BarChart`.
45
45
  - Don't encode a second measure in cell size or a glow; it's one value per cell.
46
+
47
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
48
+ ## Example
49
+
50
+ `Heatmap` as first used in its preview:
51
+
52
+ ```tsx
53
+ import { Heatmap } from 'altitude-design';
54
+
55
+ <Heatmap variant="calendar" data={commits} date="day" value="count" bins={[0, 1, 4, 10]} />
56
+ ```
57
+
58
+ The full preview, as rendered in Storybook and checked in CI (`hm`, `wide` are the preview page's own layout classes, not Altitude's):
59
+
60
+ ```tsx
61
+ import * as React from 'react';
62
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle, ChartContainer, Heatmap } from 'altitude-design';
63
+
64
+ const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
65
+ // Requests per hour: a working-day hump, quieter weekends, and a Tuesday-afternoon peak.
66
+ const load = days.flatMap((weekday, w) => Array.from({ length: 24 }, (_, hour) => {
67
+ const day = Math.max(0, Math.sin(((hour - 6) / 15) * Math.PI));
68
+ const base = (w < 5 ? 300 : 110) * day + 20 + ((hour * 7 + w * 13) % 17);
69
+ return { weekday, hour, requests: Math.round(base + (w === 1 && hour >= 13 && hour <= 15 ? 160 : 0)) };
70
+ }));
71
+ // Commits per day for a year, with quiet weekends, a holiday gap and a few busy weeks.
72
+ const start = Date.UTC(2025, 9, 27);
73
+ const commits = Array.from({ length: 365 }, (_, i) => {
74
+ const t = start + i * 864e5, wd = new Date(t).getUTCDay(), r = (i * 37 + 11) % 23;
75
+ const busy = (i > 120 && i < 140) || (i > 300 && i < 312);
76
+ const n = wd === 0 || wd === 6 ? (r < 18 ? 0 : r - 17) : i > 56 && i < 68 ? 0 : busy ? 6 + (r % 9) : r < 5 ? 0 : Math.round(r / 3);
77
+ return { day: new Date(t).toISOString().slice(0, 10), count: n };
78
+ });
79
+ const services = ['Checkout', 'Search', 'Accounts', 'Media', 'Payments'];
80
+ const regions = ['US', 'EU', 'APAC', 'LATAM'];
81
+ // Error rate against last week, in points: below zero is better.
82
+ const change = services.flatMap((service, s) => regions.map((region, r) => ({ service, region, delta: s === 3 && r === 3 ? null : Math.round((Math.sin(s * 2.1 + r * 1.3) * 1.6 + (s === 4 && r === 1 ? 1.8 : 0)) * 10) / 10 })));
83
+ const pts = (v) => (v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(v).toFixed(1) + ' pts';
84
+ export default function App() {
85
+ const [cell, setCell] = React.useState(null);
86
+ return (
87
+ <div className="hm">
88
+ <Card className="wide">
89
+ <CardHeader><CardTitle id="h1">Commits</CardTitle><CardDescription>A year, a cell per day: zero is the lightest step, and each step says its range</CardDescription></CardHeader>
90
+ <CardContent>
91
+ <ChartContainer aria-labelledby="h1" config={{ count: { label: 'Commits' } }}>
92
+ <Heatmap variant="calendar" data={commits} date="day" value="count" bins={[0, 1, 4, 10]} />
93
+ </ChartContainer>
94
+ </CardContent>
95
+ </Card>
96
+ <Card>
97
+ <CardHeader><CardTitle id="h2">Requests by hour</CardTitle><CardDescription>Load by weekday and hour, with the table view</CardDescription></CardHeader>
98
+ <CardContent>
99
+ <ChartContainer aria-labelledby="h2" views config={{ requests: { label: 'Requests' } }} xLabel="Weekday">
100
+ <Heatmap data={load} x="hour" y="weekday" value="requests" xFormat={(h) => String(h).padStart(2, '0')} />
101
+ </ChartContainer>
102
+ </CardContent>
103
+ </Card>
104
+ <Card>
105
+ <CardHeader><CardTitle id="h3">Error rate against last week</CardTitle><CardDescription>{cell ? `${cell.service} in ${cell.region}: select it again to clear` : 'scale="diverging": better below zero. Select a cell'}</CardDescription></CardHeader>
106
+ <CardContent>
107
+ <ChartContainer aria-labelledby="h3" config={{ delta: { label: 'Change', format: pts } }}>
108
+ <Heatmap data={change} x="region" y="service" value="delta" scale="diverging" selected={cell} onSelect={setCell} />
109
+ </ChartContainer>
110
+ </CardContent>
111
+ </Card>
112
+ </div>
113
+ );
114
+ }
115
+ ```
116
+ <!-- /example -->
@@ -14,3 +14,35 @@ This is a design friction: a small, deliberate obstacle in front of an action th
14
14
 
15
15
  - Use it for destructive or irreversible actions where the person already knows what will happen.
16
16
  - Don't use it when the consequence needs explaining; that's a `Dialog`.
17
+
18
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
19
+ ## Example
20
+
21
+ `HoldButton` as first used in its preview:
22
+
23
+ ```tsx
24
+ import { HoldButton } from 'altitude-design';
25
+
26
+ <HoldButton onConfirm={() => {}} holdingLabel="Keep holding…" doneLabel="Project deleted">Hold to delete</HoldButton>
27
+ ```
28
+
29
+ The full preview, as rendered in Storybook and checked in CI (`col`, `row`, `cap` are the preview page's own layout classes, not Altitude's):
30
+
31
+ ```tsx
32
+ import * as React from 'react';
33
+ import { HoldButton } from 'altitude-design';
34
+
35
+ export default function App() {
36
+ const [n, setN] = React.useState(0);
37
+ return (
38
+ <div className="col">
39
+ <div className="row">
40
+ <HoldButton onConfirm={() => {}} holdingLabel="Keep holding…" doneLabel="Project deleted">Hold to delete</HoldButton>
41
+ <HoldButton variant="primary" duration={700} repeatable onConfirm={() => setN(n + 1)} doneLabel="Published">Hold to publish</HoldButton>
42
+ </div>
43
+ <p className="cap">Press and hold (or hold Space). The button sinks below the ground as the fuse burns; let go early and it springs back. Once done it lies flat and can't be pressed again, unless it's <code>repeatable</code> (published {n}×).</p>
44
+ </div>
45
+ );
46
+ }
47
+ ```
48
+ <!-- /example -->
@@ -16,3 +16,56 @@ A richer preview of what a link or a face points to: who someone is, what a proj
16
16
 
17
17
  - Do keep it to a glance: if people need to act on it, it's a Popover (click to open) or a page.
18
18
  - Don't put the only copy of anything in it, and don't put a form in it.
19
+
20
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
21
+ ## Example
22
+
23
+ `HoverCard` as first used in its preview:
24
+
25
+ ```tsx
26
+ import { HoverCard } from 'altitude-design';
27
+
28
+ <HoverCard defaultOpen={true} trigger={<a href="#mara">Mara Quist</a>}>
29
+ <Card p={people.mara} />
30
+ </HoverCard>
31
+ ```
32
+
33
+ The full preview, as rendered in Storybook and checked in CI (`who`, `bio`, `stats`, `col`, `para`, `cap` are the preview page's own layout classes, not Altitude's):
34
+
35
+ ```tsx
36
+ import { Avatar, HoverCard } from 'altitude-design';
37
+
38
+ const people = {
39
+ mara: { name: "Mara Quist", role: "Platform \xB7 Lisbon", bio: "Owns deploys and the edge runtime. Usually reviews within the hour.", s: [42, 6, 3] },
40
+ jon: { name: "Jon Reyes", role: "Docs \xB7 Lima", bio: "Writes the release notes and keeps the changelog honest.", s: [18, 2, 5] }
41
+ };
42
+ const Card = ({ p }) => <>
43
+ <div className="who">
44
+ <Avatar name={p.name} size={40} status="active" />
45
+ <div><b>{p.name}</b><span>{p.role}</span></div>
46
+ </div>
47
+ <p className="bio">{p.bio}</p>
48
+ <div className="stats">
49
+ <span><b>{p.s[0]}</b>{" merged"}</span>
50
+ <span><b>{p.s[1]}</b>{" open"}</span>
51
+ <span><b>{p.s[2]}</b>{" projects"}</span>
52
+ </div>
53
+ </>;
54
+ export default function App() {
55
+ return <div className="col" style={{ gap: 18 }}>
56
+ <p className="para">
57
+ {"Merged by "}
58
+ <HoverCard defaultOpen={true} trigger={<a href="#mara">Mara Quist</a>}>
59
+ <Card p={people.mara} />
60
+ </HoverCard>
61
+ {" two hours ago, after review from "}
62
+ <HoverCard trigger={<a href="#jon">Jon Reyes</a>}><Card p={people.jon} /></HoverCard>
63
+ .
64
+ </p>
65
+ <p className="cap" style={{ marginTop: 160 }}>
66
+ Rest the pointer on a name; once one card has shown, the other opens at once. Focus a link with Tab to open it from the keyboard; Escape closes it.
67
+ </p>
68
+ </div>;
69
+ }
70
+ ```
71
+ <!-- /example -->
@@ -22,3 +22,35 @@ Numbered source markers in a grounded answer ("…renews monthly [2]"), with a p
22
22
  - Do include the excerpt the answer relied on.
23
23
  - Do mark sources you couldn't verify.
24
24
  - Don't render citations as bare numbers or superscripts with no name.
25
+
26
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
27
+ ## Example
28
+
29
+ `InlineCitation` as first used in its preview:
30
+
31
+ ```tsx
32
+ import { InlineCitation } from 'altitude-design';
33
+
34
+ <InlineCitation index={1} source={sources[0]} />
35
+ ```
36
+
37
+ The full preview, as rendered in Storybook and checked in CI (`ic` is the preview page's own layout class, not Altitude's):
38
+
39
+ ```tsx
40
+ import { InlineCitation, Sources } from 'altitude-design';
41
+
42
+ const sources = [
43
+ { title: 'Billing FAQ', url: 'https://help.example.com/billing/faq', excerpt: 'Annual plans can be refunded in full within 30 days of purchase.' },
44
+ { title: 'Plans and pricing', url: 'https://example.com/pricing', excerpt: 'Monthly plans renew on the same day each month.' },
45
+ { title: 'Forum post: “Refunds after 30 days?”', url: 'https://community.example.com/t/4821', unverified: true },
46
+ ];
47
+ export default function App() {
48
+ return (
49
+ <div className="ic">
50
+ <p>Annual plans can be refunded in full within 30 days<InlineCitation index={1} source={sources[0]} />. Monthly plans renew on the same day each month<InlineCitation index={2} source={sources[1]} />, and some customers report partial refunds after that<InlineCitation index={3} source={sources[2]} />.</p>
51
+ <Sources items={sources} />
52
+ </div>
53
+ );
54
+ }
55
+ ```
56
+ <!-- /example -->
@@ -130,4 +130,250 @@ Optional labels are `editLabel` (default `Edit ${label}`), `saveLabel` (“Save
130
130
 
131
131
  The canonical `InlineEdit` section in `build/previews.txt` renders a task title (in its heading font), a multiline description, the task's status (Select, on choose, with a rejected “Blocked”), assignee (Combobox, on choose) and estimate (a numeric Input), an independent filename table cell with this component, and “Ways to open”: a pencil and two fields on hover, with seams. A collapsed documentation harness selects deterministic fixtures before mounting, resets records and counters, delays validation, applies external updates, removes the filename consumer during work and opens a dialog comparison using the same validation and persistence adapter. Fixture controls and operation logs stay outside the product scene.
132
132
 
133
- Run `node checks/inline-edit.mjs` from `build/` after generating preview fixtures. `ALT_FIXTURE_ROOT=test/react19` selects the React 19 source fixtures. The checks cover editing in place (the value's text doesn't move, the control takes its font, the pencil and the field move nothing on hover, the caret lands where clicked, the seam, the announcement that clears itself), multiline (newline on Enter, Ctrl/Cmd+Enter, IME, Escape, caret at the end, growth to `maxRows`, line breaks in read mode), custom editors (focus, Escape layering, on-choose saves and rejections, unchanged choices, numeric validation), lifecycle races, retained input, uncertainty reconciliation, conflict latching, keyboard/IME, focus, disabled behavior and the four-scheme responsive/accessibility matrix. Automated checks do not replace the human screen-reader, touch, magnification or formative comparisons recorded in [Actions in place validation](https://github.com/richlaconte/altitude/blob/v0.4.0/project/ActionsInPlaceValidation.md).
133
+ Run `node checks/inline-edit.mjs` from `build/` after generating preview fixtures. `ALT_FIXTURE_ROOT=test/react19` selects the React 19 source fixtures. The checks cover editing in place (the value's text doesn't move, the control takes its font, the pencil and the field move nothing on hover, the caret lands where clicked, the seam, the announcement that clears itself), multiline (newline on Enter, Ctrl/Cmd+Enter, IME, Escape, caret at the end, growth to `maxRows`, line breaks in read mode), custom editors (focus, Escape layering, on-choose saves and rejections, unchanged choices, numeric validation), lifecycle races, retained input, uncertainty reconciliation, conflict latching, keyboard/IME, focus, disabled behavior and the four-scheme responsive/accessibility matrix. Automated checks do not replace the human screen-reader, touch, magnification or formative comparisons recorded in [Actions in place validation](https://github.com/richlaconte/altitude/blob/v0.4.2/project/ActionsInPlaceValidation.md).
134
+
135
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
136
+ ## Example
137
+
138
+ `InlineEdit` as first used in its preview:
139
+
140
+ ```tsx
141
+ import { InlineEdit } from 'altitude-design';
142
+
143
+ <InlineEdit label="Description" value={task.description} onValueChange={publish('description')} onSave={save('description')}
144
+ multiline rows={3} maxRows={8} maxLength={500} />
145
+ ```
146
+
147
+ The full preview, as rendered in Storybook and checked in CI (`inline-demo-description`, `inline-demo-prop-label`, `inline-demo-props`, `inline-demo-prop`, `inline-demo-scene`, `inline-demo-task`, `inline-demo-meta`, `inline-demo-harness`, `inline-demo-controls`, `inline-demo-log`, `inline-demo` are the preview page's own layout classes, not Altitude's):
148
+
149
+ ```tsx
150
+ import * as React from 'react';
151
+ import { Badge, Button, Combobox, Dialog, Field, InlineEdit, Input, Select, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'altitude-design';
152
+
153
+ const INLINE_RECORDS = Object.freeze([
154
+ Object.freeze({ id: 'task-launch', name: 'Prepare launch checklist', revision: 1 }),
155
+ Object.freeze({ id: 'file-brand', name: 'Brand guidelines.pdf', revision: 1 }),
156
+ Object.freeze({ id: 'file-brand-draft', name: 'Brand guidelines draft.pdf', revision: 1 }),
157
+ Object.freeze({ id: 'file-launch', name: 'Launch checklist.md', revision: 1 }),
158
+ ]);
159
+ const INLINE_FIXTURES = ['success-100', 'success-1200', 'fail-once', 'conflict', 'interrupted-after-save'];
160
+ const inlineWait = ms => new Promise(resolve => setTimeout(resolve, ms));
161
+ const INLINE_STATUSES = [{ value: 'todo', label: 'To do' }, { value: 'progress', label: 'In progress' }, { value: 'review', label: 'In review' }, { value: 'blocked', label: 'Blocked' }];
162
+ const INLINE_TONES = { todo: 'neutral', progress: 'accent', review: 'caution', blocked: 'critical' };
163
+ const INLINE_PEOPLE = [{ value: 'mara', label: 'Mara Quist' }, { value: 'ines', label: 'Inés Ortega' }, { value: 'tobi', label: 'Tobi Adeyemi' }, { value: 'wen', label: 'Li Wen' }];
164
+ const inlineLabel = (list, value) => list.find(option => option.value === value)?.label ?? 'Not set';
165
+
166
+ // Task properties: the same session with a different editor. Status and assignee save on choose; the estimate waits for Save.
167
+ function InlineTaskProperties() {
168
+ const [task, setTask] = React.useState({ status: 'progress', assignee: 'mara', estimate: 3, description: 'Collect sign-off from design, support and legal.\nShare the final list in #launch by Thursday.' });
169
+ const publish = key => value => setTask(task => ({ ...task, [key]: value }));
170
+ const save = key => async draft => {
171
+ await inlineWait(300);
172
+ if (key === 'status' && draft === 'blocked') return { ok: false, error: 'Only the project lead can block a task. Choose another status.' };
173
+ return { ok: true, value: draft };
174
+ };
175
+ return <>
176
+ <div className="inline-demo-description"><span className="inline-demo-prop-label" aria-hidden="true">Description</span>
177
+ <InlineEdit label="Description" value={task.description} onValueChange={publish('description')} onSave={save('description')}
178
+ multiline rows={3} maxRows={8} maxLength={500} /></div>
179
+ <div className="inline-demo-props">
180
+ <div className="inline-demo-prop"><span className="inline-demo-prop-label" aria-hidden="true">Status</span>
181
+ <InlineEdit label="Status" value={task.status} onValueChange={publish('status')} onSave={save('status')} commit="on-choose"
182
+ renderValue={status => <Badge tone={INLINE_TONES[status]}>{inlineLabel(INLINE_STATUSES, status)}</Badge>}
183
+ editor={control => <Select {...control} items={INLINE_STATUSES} size="sm" />} /></div>
184
+ <div className="inline-demo-prop"><span className="inline-demo-prop-label" aria-hidden="true">Assignee</span>
185
+ <InlineEdit label="Assignee" value={task.assignee} onValueChange={publish('assignee')} onSave={save('assignee')} commit="on-choose"
186
+ renderValue={person => inlineLabel(INLINE_PEOPLE, person)}
187
+ editor={control => <Combobox {...control} items={INLINE_PEOPLE} size="sm" placeholder="Type a name" />} /></div>
188
+ <div className="inline-demo-prop"><span className="inline-demo-prop-label" aria-hidden="true">Estimate</span>
189
+ <InlineEdit label="Estimate" value={task.estimate} onValueChange={publish('estimate')} onSave={save('estimate')} required
190
+ validate={points => Number.isInteger(points) && points >= 0 && points <= 40 ? undefined : 'Use a whole number from 0 to 40.'}
191
+ renderValue={points => `${points} point${points === 1 ? '' : 's'}`}
192
+ editor={({ value, onValueChange, ...control }) => <Input {...control} type="number" inputMode="numeric" min={0} max={40} size="sm"
193
+ value={value ?? ''} onChange={event => onValueChange(event.target.value === '' ? null : Number(event.target.value))} />} /></div>
194
+ </div>
195
+ </>;
196
+ }
197
+
198
+ function InlineExample({ fixture, slowValidation, epoch }) {
199
+ const server = React.useRef(null);
200
+ if (!server.current) server.current = new Map(INLINE_RECORDS.map(record => [record.id, { ...record }]));
201
+ const [records, setRecords] = React.useState(() => INLINE_RECORDS.map(record => ({ ...record })));
202
+ const [log, setLog] = React.useState([]);
203
+ const [visible, setVisible] = React.useState(true);
204
+ const [outsideCount, setOutsideCount] = React.useState(0);
205
+ const [baseline, setBaseline] = React.useState(false);
206
+ const sequence = React.useRef(0), failUsed = React.useRef(false), accepted = React.useRef(new Map());
207
+ const alive = React.useRef(true);
208
+ React.useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []);
209
+ const record = id => records.find(record => record.id === id);
210
+ const trace = event => { if (alive.current) setLog(log => [...log, event]); };
211
+ const reconcile = id => { if (alive.current) setRecords(records => records.map(record => record.id === id ? { ...server.current.get(id) } : record)); };
212
+ const publish = id => canonicalValue => {
213
+ trace({ event: 'published', id, value: canonicalValue });
214
+ setRecords(records => records.map(record => record.id === id ? { ...server.current.get(id), name: canonicalValue } : record));
215
+ };
216
+ const validate = id => async draft => {
217
+ if (slowValidation) await inlineWait(1200);
218
+ if (!draft.trim()) return 'Enter a name.';
219
+ if ([...server.current.values()].some(record => record.id !== id && record.name === draft.trim())) return 'A file already has this name. Choose another.';
220
+ };
221
+ const save = id => async (draft, { previousValue }) => {
222
+ const expectedRevision = record(id).revision;
223
+ const operationId = `rename-${epoch}-${++sequence.current}`;
224
+ trace({ event: 'requested', id, expectedRevision, operationId, draft });
225
+ const commit = () => {
226
+ if (accepted.current.has(operationId)) return accepted.current.get(operationId);
227
+ const current = server.current.get(id);
228
+ if (current.revision !== expectedRevision || current.name !== previousValue) {
229
+ reconcile(id); trace({ event: 'rejected-stale', id, operationId });
230
+ return { ok: false, error: 'The saved name changed. Review the current name.' };
231
+ }
232
+ const next = { ...current, name: draft.trim(), revision: current.revision + 1 };
233
+ server.current.set(id, next);
234
+ const result = { ok: true, value: next.name };
235
+ accepted.current.set(operationId, result);
236
+ trace({ event: 'committed', id, revision: next.revision, operationId, value: next.name });
237
+ return result;
238
+ };
239
+ if (fixture === 'interrupted-after-save') {
240
+ await inlineWait(100); const result = commit();
241
+ await inlineWait(1100);
242
+ // Simulate a lost acknowledgement, then query/retry the same operation ID.
243
+ // A known committed result is returned as success; no second mutation occurs.
244
+ trace({ event: 'reconciled', id, operationId, revision: server.current.get(id).revision });
245
+ return accepted.current.get(operationId) || result;
246
+ }
247
+ if (fixture === 'conflict') {
248
+ await inlineWait(100);
249
+ const current = server.current.get(id);
250
+ server.current.set(id, { ...current, name: id === 'task-launch' ? 'Prepare updated launch checklist' : 'Brand guidelines revised.pdf', revision: current.revision + 1 });
251
+ reconcile(id); trace({ event: 'external-update', id, revision: current.revision + 1 });
252
+ await inlineWait(1100); return commit();
253
+ }
254
+ if (fixture === 'fail-once' && !failUsed.current) {
255
+ failUsed.current = true; await inlineWait(1200);
256
+ trace({ event: 'rejected', id, operationId });
257
+ return { ok: false, error: 'We couldn’t save this name. Your text is still here. Try again.' };
258
+ }
259
+ await inlineWait(fixture === 'success-1200' ? 1200 : 100);
260
+ return commit();
261
+ };
262
+ const file = record('file-brand');
263
+ const externalUpdate = () => {
264
+ const current = server.current.get('file-brand');
265
+ server.current.set('file-brand', { ...current, name: 'Brand guidelines updated.pdf', revision: current.revision + 1 });
266
+ trace({ event: 'external-update', id: 'file-brand', revision: current.revision + 1 }); reconcile('file-brand');
267
+ };
268
+ return <>
269
+ <div className="inline-demo-scene" data-inline-scene="">
270
+ <section className="inline-demo-task" aria-labelledby="inline-task-title">
271
+ <h2 id="inline-task-title">Launch preparation</h2>
272
+ <InlineEdit label="Task title" editLabel="Edit task title" value={record('task-launch').name} onValueChange={publish('task-launch')} onSave={save('task-launch')} validate={validate('task-launch')} required maxLength={100} />
273
+ <p className="inline-demo-meta">Due Friday · Product team</p>
274
+ <InlineTaskProperties />
275
+ </section>
276
+ <section aria-labelledby="inline-files-title">
277
+ <h2 id="inline-files-title">Files</h2>
278
+ <Table>
279
+ <TableHeader><TableRow><TableHead>File name</TableHead><TableHead>Size</TableHead></TableRow></TableHeader>
280
+ <TableBody>
281
+ {visible && <TableRow><TableCell>
282
+ <InlineEdit label="File name" editLabel="Rename file" value={file.name} onValueChange={publish('file-brand')} onSave={save('file-brand')} validate={validate('file-brand')} required maxLength={100} />
283
+ </TableCell><TableCell>2.4 MB</TableCell></TableRow>}
284
+ <TableRow><TableCell>{record('file-brand-draft').name}<p className="inline-demo-meta">Draft · Updated yesterday</p></TableCell><TableCell>1.8 MB</TableCell></TableRow>
285
+ <TableRow><TableCell>{record('file-launch').name}</TableCell><TableCell>8 KB</TableCell></TableRow>
286
+ </TableBody>
287
+ </Table>
288
+ <Button variant="ghost" onClick={() => setOutsideCount(count => count + 1)}>Show file details</Button>
289
+ {outsideCount > 0 && <p className="inline-demo-meta">Three fictional files · Details opened {outsideCount} time{outsideCount === 1 ? '' : 's'}.</p>}
290
+ </section>
291
+ <InlineWaysToOpen />
292
+ </div>
293
+ <details className="inline-demo-harness">
294
+ <summary>Comparison and fixture controls</summary>
295
+ <p>These controls belong to the documentation harness. The dialog comparison uses the same filename, validation and save adapter. Reset before each trial.</p>
296
+ <div className="inline-demo-controls">
297
+ <Button onClick={externalUpdate}>Apply external filename update</Button>
298
+ <Button onClick={() => { if (!visible) reconcile('file-brand'); setVisible(visible => !visible); }}>{visible ? 'Remove filename editor' : 'Show filename editor'}</Button>
299
+ <Button onClick={() => setBaseline(true)}>Open dialog rename baseline</Button>
300
+ </div>
301
+ <output className="inline-demo-log" data-inline-log="" aria-label="Fixture operation log">{JSON.stringify(log, null, 2)}</output>
302
+ </details>
303
+ <InlineDialogBaseline open={baseline} onOpenChange={setBaseline} value={file.name} validate={validate('file-brand')} onSave={save('file-brand')} onValueChange={publish('file-brand')} />
304
+ </>;
305
+ }
306
+
307
+ function InlineDialogBaseline({ open, onOpenChange, value, validate, onSave, onValueChange }) {
308
+ const [draft, setDraft] = React.useState(value), [error, setError] = React.useState(''), [busy, setBusy] = React.useState('');
309
+ const baseline = React.useRef(value), generation = React.useRef(0), guard = React.useRef(false), latest = React.useRef(value);
310
+ latest.current = value;
311
+ React.useEffect(() => {
312
+ generation.current++; guard.current = false; setBusy(''); setError('');
313
+ if (open) { baseline.current = value; setDraft(value); }
314
+ return () => { generation.current++; };
315
+ }, [open]);
316
+ const submit = async () => {
317
+ if (guard.current) return;
318
+ if (latest.current !== baseline.current) { setError('The saved value changed. Close and reopen to review it.'); return; }
319
+ if (draft === baseline.current) { onOpenChange(false); return; }
320
+ guard.current = true; const token = generation.current; setError(''); setBusy('Checking value');
321
+ const error = draft.length > 100 ? 'Use 100 characters or fewer.' : await validate(draft);
322
+ if (token !== generation.current) return;
323
+ if (error) { guard.current = false; setBusy(''); setError(error); return; }
324
+ setBusy('Saving changes');
325
+ const result = await onSave(draft, { previousValue: baseline.current });
326
+ if (token !== generation.current) return;
327
+ guard.current = false; setBusy('');
328
+ if (latest.current !== baseline.current) { setError('The value changed while saving. Your save may have completed. Check the current record.'); return; }
329
+ if (!result.ok) { setError(result.error); return; }
330
+ onValueChange(result.value); onOpenChange(false);
331
+ };
332
+ return <Dialog open={open} onOpenChange={onOpenChange} title="Rename file" description="Change the name of this file." dismissible={busy !== 'Saving changes'} footer={<>
333
+ <Button variant="ghost" disabled={busy === 'Saving changes'} onClick={() => onOpenChange(false)}>Cancel</Button>
334
+ <Button variant="primary" loading={busy === 'Saving changes'} disabled={busy === 'Checking value'} onClick={submit}>Save changes</Button>
335
+ </>}>
336
+ <Field label="File name" error={error} required><Input value={draft} maxLength={100} readOnly={!!busy} onChange={event => { setDraft(event.target.value); setError(''); }} onKeyDown={event => {
337
+ if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return;
338
+ if (event.key === 'Enter') { event.preventDefault(); submit(); }
339
+ }} /></Field>
340
+ <p role="status">{busy}</p>
341
+ </Dialog>;
342
+ }
343
+
344
+ // The value as its own button: a pencil on hover, or the field itself on hover. The seam says so at rest.
345
+ function InlineWaysToOpen() {
346
+ const [values, setValues] = React.useState({ project: 'Northwind launch', meeting: 'Quarterly planning review', owner: '' });
347
+ const set = key => next => setValues(all => ({ ...all, [key]: next }));
348
+ const save = draft => ({ ok: true, value: draft.trim() });
349
+ return <section aria-labelledby="inline-ways-title">
350
+ <h2 id="inline-ways-title">Ways to open</h2>
351
+ <div className="inline-demo-props">
352
+ <div className="inline-demo-prop"><span className="inline-demo-prop-label">Pencil on hover, with a seam</span>
353
+ <InlineEdit label="Project name" trigger="pencil" hint="seam" value={values.project} onValueChange={set('project')} onSave={save} required /></div>
354
+ <div className="inline-demo-prop"><span className="inline-demo-prop-label">Field on hover, with a seam</span>
355
+ <InlineEdit label="Meeting title" trigger="field" hint="seam" value={values.meeting} onValueChange={set('meeting')} onSave={save} required /></div>
356
+ <div className="inline-demo-prop"><span className="inline-demo-prop-label">Field on hover, empty</span>
357
+ <InlineEdit label="Owner" trigger="field" hint="seam" emptyLabel="Add an owner" value={values.owner} onValueChange={set('owner')} onSave={save} /></div>
358
+ </div>
359
+ </section>;
360
+ }
361
+
362
+ export default function App() {
363
+ const [fixture, setFixture] = React.useState('success-100'), [slow, setSlow] = React.useState(false);
364
+ const [mounted, setMounted] = React.useState({ fixture: 'success-100', slow: false, epoch: 0 });
365
+ return <div className="inline-demo">
366
+ <InlineExample key={mounted.epoch} fixture={mounted.fixture} slowValidation={mounted.slow} epoch={mounted.epoch} />
367
+ <details className="inline-demo-harness">
368
+ <summary>Choose the next fixture</summary>
369
+ <p>Select before mounting. Reset restores the same fictional records, revisions and request counters.</p>
370
+ <div className="inline-demo-controls">
371
+ <label>Rename fixture<select value={fixture} onChange={event => setFixture(event.target.value)}>{INLINE_FIXTURES.map(name => <option key={name}>{name}</option>)}</select></label>
372
+ <label><input type="checkbox" checked={slow} onChange={event => setSlow(event.target.checked)} />Delay validation by 1,200 ms</label>
373
+ <Button onClick={() => setMounted({ fixture, slow, epoch: mounted.epoch + 1 })}>Reset example</Button>
374
+ </div>
375
+ </details>
376
+ </div>;
377
+ }
378
+ ```
379
+ <!-- /example -->
@@ -25,3 +25,39 @@ The shadow list is always four layers (wall, occlusion, lip, halo; the halo is u
25
25
  - Don't add an accent focus ring or glow to fields, or a rim on click. Keyboard focus on buttons and choice controls is the `accent-ink` ring; on a field the lit floor is the focus, plus the dark rim when you arrived by keyboard.
26
26
  - Don't put a suggestion in the value: pass it as `suggestion`, so people can tell what they wrote from what was proposed.
27
27
  - Don't place inputs on `surface-well` backgrounds: a well inside a well disappears. Put forms on ground or raised surfaces.
28
+
29
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
30
+ ## Example
31
+
32
+ `Input` as first used in its preview:
33
+
34
+ ```tsx
35
+ import { Input } from 'altitude-design';
36
+
37
+ <Input type="email" placeholder="name@company.com" value={email} onChange={(e) => setEmail(e.target.value)} />
38
+ ```
39
+
40
+ The full preview, as rendered in Storybook and checked in CI (`col`, `grid`, `cap` are the preview page's own layout classes, not Altitude's):
41
+
42
+ ```tsx
43
+ import * as React from 'react';
44
+ import { Field, Input, Kbd, Textarea } from 'altitude-design';
45
+
46
+ export default function App() {
47
+ const [note, setNote] = React.useState('Ship the cold-start fix behind a flag first.');
48
+ const [email, setEmail] = React.useState('');
49
+ return (
50
+ <div className="col">
51
+ <div className="grid">
52
+ <Field label="Email address"><Input type="email" placeholder="name@company.com" value={email} onChange={(e) => setEmail(e.target.value)} /></Field>
53
+ <Field label="Billing email"><Input type="email" defaultValue="mara@northwind.dev" /></Field>
54
+ <Field label="Search projects"><Input type="search" placeholder="Project name" leading={<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>} trailing={<Kbd>/</Kbd>} /></Field>
55
+ <Field label="Invite by email" error="Enter an email address, like name@company.com."><Input type="email" defaultValue="not-an-email" /></Field>
56
+ <div style={{ gridColumn: '1 / -1' }}><Field label="Note"><Textarea value={note} onChange={(e) => setNote(e.target.value)} maxLength={80} placeholder="Leave a note for your team…" /></Field></div>
57
+ </div>
58
+ <p className="cap">An empty field shows its inside wall below the top edge, and deepens as your pointer comes near. Type and the floor rises flush with the page. Invalid drops it to trench depth.</p>
59
+ </div>
60
+ );
61
+ }
62
+ ```
63
+ <!-- /example -->
@@ -28,3 +28,49 @@ A one-time code: two-factor sign-in, email verification, device approval. Four t
28
28
  - Do say where the code went and how to get a new one.
29
29
  - Don't split the code into several inputs; autofill and paste break.
30
30
  - Don't clear the code on a rejection without saying why; leave it so it can be corrected.
31
+
32
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
33
+ ## Example
34
+
35
+ `InputOTP` as first used in its preview:
36
+
37
+ ```tsx
38
+ import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from 'altitude-design';
39
+
40
+ <InputOTP maxLength={6} value={code} verifying={state === 'verifying'}
41
+ onChange={(c) => { setCode(c); if (state === 'wrong') setState('idle'); }} onComplete={verify}>
42
+ <InputOTPGroup>{[0, 1, 2].map((i) => <InputOTPSlot key={i} index={i} />)}</InputOTPGroup>
43
+ <InputOTPSeparator />
44
+ <InputOTPGroup>{[3, 4, 5].map((i) => <InputOTPSlot key={i} index={i} />)}</InputOTPGroup>
45
+ </InputOTP>
46
+ ```
47
+
48
+ The full preview, as rendered in Storybook and checked in CI (`col` is the preview page's own layout class, not Altitude's):
49
+
50
+ ```tsx
51
+ import * as React from 'react';
52
+ import { Field, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from 'altitude-design';
53
+
54
+ export default function App() {
55
+ const [code, setCode] = React.useState('');
56
+ const [state, setState] = React.useState('idle');
57
+ const verify = (c) => { setState('verifying'); setTimeout(() => setState(c === '123456' ? 'ok' : 'wrong'), 900); };
58
+ const hint = state === 'verifying' ? 'Checking code…' : state === 'ok' ? 'Verified.' : 'We sent it to j•••@acme.com. Try 123456, or any other code to see a rejection.';
59
+ return (
60
+ <div className="col">
61
+ <Field label="Verification code" hint={hint} error={state === 'wrong' ? 'That code didn\'t work. Check the latest email and try again.' : undefined}>
62
+ <InputOTP maxLength={6} value={code} verifying={state === 'verifying'}
63
+ onChange={(c) => { setCode(c); if (state === 'wrong') setState('idle'); }} onComplete={verify}>
64
+ <InputOTPGroup>{[0, 1, 2].map((i) => <InputOTPSlot key={i} index={i} />)}</InputOTPGroup>
65
+ <InputOTPSeparator />
66
+ <InputOTPGroup>{[3, 4, 5].map((i) => <InputOTPSlot key={i} index={i} />)}</InputOTPGroup>
67
+ </InputOTP>
68
+ </Field>
69
+ <Field label="Backup code" hint="8 letters or digits.">
70
+ <InputOTP maxLength={8} pattern="alphanumeric" defaultValue="K7Q2" />
71
+ </Field>
72
+ </div>
73
+ );
74
+ }
75
+ ```
76
+ <!-- /example -->