altitude-design 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/altitude.mjs +120 -20
  3. package/dist/guidance/README.md +4 -0
  4. package/dist/guidance/agent-setup.md +3 -2
  5. package/dist/guidance/components/AILabel/README.md +60 -0
  6. package/dist/guidance/components/Accordion/README.md +32 -0
  7. package/dist/guidance/components/AgentFleet/README.md +81 -0
  8. package/dist/guidance/components/AgentLanes/README.md +53 -0
  9. package/dist/guidance/components/AgentRequests/README.md +76 -0
  10. package/dist/guidance/components/Alert/README.md +71 -0
  11. package/dist/guidance/components/AltitudeProvider/README.md +111 -0
  12. package/dist/guidance/components/AppShell/README.md +121 -0
  13. package/dist/guidance/components/AreaChart/README.md +101 -0
  14. package/dist/guidance/components/Artifact/README.md +99 -0
  15. package/dist/guidance/components/Avatar/README.md +75 -0
  16. package/dist/guidance/components/Await/README.md +128 -0
  17. package/dist/guidance/components/Badge/README.md +66 -0
  18. package/dist/guidance/components/BarChart/README.md +109 -0
  19. package/dist/guidance/components/BarList/README.md +87 -0
  20. package/dist/guidance/components/Board/README.md +98 -0
  21. package/dist/guidance/components/Breadcrumb/README.md +66 -0
  22. package/dist/guidance/components/Button/README.md +58 -0
  23. package/dist/guidance/components/Card/README.md +52 -0
  24. package/dist/guidance/components/ChartBrush/README.md +98 -0
  25. package/dist/guidance/components/ChartContainer/README.md +229 -0
  26. package/dist/guidance/components/ChartLegend/README.md +78 -0
  27. package/dist/guidance/components/ChartReference/README.md +115 -0
  28. package/dist/guidance/components/ChartTooltip/README.md +101 -0
  29. package/dist/guidance/components/Checkbox/README.md +46 -0
  30. package/dist/guidance/components/Checklist/README.md +137 -0
  31. package/dist/guidance/components/CodeBlock/README.md +72 -0
  32. package/dist/guidance/components/ColorPicker/README.md +69 -0
  33. package/dist/guidance/components/Combobox/README.md +51 -0
  34. package/dist/guidance/components/CommandPalette/README.md +204 -0
  35. package/dist/guidance/components/CommentThread/README.md +81 -0
  36. package/dist/guidance/components/Confirmation/README.md +94 -0
  37. package/dist/guidance/components/ContextMenu/README.md +81 -0
  38. package/dist/guidance/components/Conversation/README.md +169 -0
  39. package/dist/guidance/components/CopyButton/README.md +54 -0
  40. package/dist/guidance/components/DataGrid/README.md +157 -0
  41. package/dist/guidance/components/DatePicker/README.md +116 -0
  42. package/dist/guidance/components/Dialog/README.md +204 -0
  43. package/dist/guidance/components/DiffView/README.md +107 -0
  44. package/dist/guidance/components/DotPlot/README.md +82 -0
  45. package/dist/guidance/components/Drilldown/README.md +43 -0
  46. package/dist/guidance/components/DropZone/README.md +71 -0
  47. package/dist/guidance/components/DropdownMenu/README.md +63 -0
  48. package/dist/guidance/components/EditRegion/README.md +263 -0
  49. package/dist/guidance/components/EmptyState/README.md +66 -0
  50. package/dist/guidance/components/FeatureGate/README.md +67 -0
  51. package/dist/guidance/components/Field/README.md +46 -0
  52. package/dist/guidance/components/FilterBar/README.md +132 -0
  53. package/dist/guidance/components/FormattedNumber/README.md +81 -0
  54. package/dist/guidance/components/Heatmap/README.md +116 -0
  55. package/dist/guidance/components/HoldButton/README.md +48 -0
  56. package/dist/guidance/components/HoverCard/README.md +71 -0
  57. package/dist/guidance/components/InlineCitation/README.md +56 -0
  58. package/dist/guidance/components/InlineEdit/README.md +379 -0
  59. package/dist/guidance/components/Input/README.md +63 -0
  60. package/dist/guidance/components/InputOTP/README.md +76 -0
  61. package/dist/guidance/components/ItemGroup/README.md +193 -0
  62. package/dist/guidance/components/Kbd/README.md +36 -0
  63. package/dist/guidance/components/LineChart/README.md +109 -0
  64. package/dist/guidance/components/Link/README.md +69 -0
  65. package/dist/guidance/components/Markdown/README.md +113 -0
  66. package/dist/guidance/components/Meter/README.md +81 -0
  67. package/dist/guidance/components/NotificationCenter/README.md +67 -0
  68. package/dist/guidance/components/NumberInput/README.md +66 -0
  69. package/dist/guidance/components/PageHeader/README.md +74 -0
  70. package/dist/guidance/components/Pagination/README.md +58 -0
  71. package/dist/guidance/components/PasswordInput/README.md +70 -0
  72. package/dist/guidance/components/PermissionMatrix/README.md +68 -0
  73. package/dist/guidance/components/Pile/README.md +54 -0
  74. package/dist/guidance/components/PlanPicker/README.md +62 -0
  75. package/dist/guidance/components/Popover/README.md +48 -0
  76. package/dist/guidance/components/Progress/README.md +41 -0
  77. package/dist/guidance/components/PromptInput/README.md +138 -0
  78. package/dist/guidance/components/PropertyList/README.md +137 -0
  79. package/dist/guidance/components/Question/README.md +81 -0
  80. package/dist/guidance/components/RadioGroup/README.md +47 -0
  81. package/dist/guidance/components/RelativeTime/README.md +73 -0
  82. package/dist/guidance/components/RemovalReceipt/README.md +111 -0
  83. package/dist/guidance/components/Resizable/README.md +65 -0
  84. package/dist/guidance/components/SaveBar/README.md +65 -0
  85. package/dist/guidance/components/SavedViews/README.md +82 -0
  86. package/dist/guidance/components/ScatterChart/README.md +104 -0
  87. package/dist/guidance/components/Schedule/README.md +142 -0
  88. package/dist/guidance/components/ScrollArea/README.md +35 -0
  89. package/dist/guidance/components/SecretField/README.md +74 -0
  90. package/dist/guidance/components/Select/README.md +97 -0
  91. package/dist/guidance/components/Separator/README.md +70 -0
  92. package/dist/guidance/components/Sheet/README.md +126 -0
  93. package/dist/guidance/components/Skeleton/README.md +50 -0
  94. package/dist/guidance/components/Slider/README.md +60 -0
  95. package/dist/guidance/components/SortableList/README.md +45 -0
  96. package/dist/guidance/components/Sparkline/README.md +87 -0
  97. package/dist/guidance/components/SplitButton/README.md +65 -0
  98. package/dist/guidance/components/Stat/README.md +92 -0
  99. package/dist/guidance/components/Stepper/README.md +90 -0
  100. package/dist/guidance/components/Suggestions/README.md +87 -0
  101. package/dist/guidance/components/Surface/README.md +48 -0
  102. package/dist/guidance/components/Switch/README.md +38 -0
  103. package/dist/guidance/components/SyncIndicator/README.md +78 -0
  104. package/dist/guidance/components/Table/README.md +138 -0
  105. package/dist/guidance/components/Tabs/README.md +73 -0
  106. package/dist/guidance/components/TagInput/README.md +116 -0
  107. package/dist/guidance/components/Terminal/README.md +69 -0
  108. package/dist/guidance/components/ThreadList/README.md +110 -0
  109. package/dist/guidance/components/TimeInput/README.md +63 -0
  110. package/dist/guidance/components/Timeline/README.md +87 -0
  111. package/dist/guidance/components/Toaster/README.md +54 -0
  112. package/dist/guidance/components/Toggle/README.md +85 -0
  113. package/dist/guidance/components/Toolbar/README.md +106 -0
  114. package/dist/guidance/components/Tooltip/README.md +41 -0
  115. package/dist/guidance/components/Tour/README.md +73 -0
  116. package/dist/guidance/components/Trace/README.md +129 -0
  117. package/dist/guidance/components/Tree/README.md +171 -0
  118. package/dist/guidance/evidence.json +18 -0
  119. package/dist/guidance/index.json +811 -7
  120. package/dist/guidance/llms.txt +153 -2
  121. package/dist/guidance/manifest.json +133 -12
  122. package/dist/guidance/patterns/screen-layout.md +1 -1
  123. package/dist/guidance/references/build/checks/agent-setup.mjs +7 -0
  124. package/dist/guidance/references/build/checks/guidance.mjs +8 -2
  125. package/dist/guidance/references/build/checks/source-check.mjs +101 -0
  126. package/dist/guidance/rules.json +155 -0
  127. package/dist/guidance/visual/ActionsInPlace.md +463 -0
  128. package/dist/guidance/visual/Charts.md +119 -0
  129. package/dist/guidance/visual/Distinctiveness.md +106 -0
  130. package/dist/guidance/visual/Elevation.md +1 -1
  131. package/dist/guidance/visual/Mobile.md +64 -0
  132. package/dist/guidance/visual/Motion.md +225 -0
  133. package/dist/guidance/visual/README.md +98 -0
  134. package/dist/guidance/visual/Research.md +1 -1
  135. package/dist/guidance/visual/Theming.md +349 -0
  136. package/dist/types/agent-fleet.d.ts +28 -0
  137. package/dist/types/ai-label.d.ts +7 -0
  138. package/dist/types/app-shell.d.ts +23 -0
  139. package/dist/types/area-chart.d.ts +8 -0
  140. package/dist/types/artifact.d.ts +14 -0
  141. package/dist/types/bar-chart.d.ts +6 -0
  142. package/dist/types/bar-list.d.ts +6 -0
  143. package/dist/types/breadcrumb.d.ts +6 -0
  144. package/dist/types/chart-brush.d.ts +6 -0
  145. package/dist/types/chart.d.ts +29 -1
  146. package/dist/types/checklist.d.ts +6 -0
  147. package/dist/types/code-block.d.ts +6 -0
  148. package/dist/types/collections.d.ts +15 -0
  149. package/dist/types/color-picker.d.ts +6 -0
  150. package/dist/types/command.d.ts +13 -0
  151. package/dist/types/comment-thread.d.ts +11 -0
  152. package/dist/types/confirmation.d.ts +8 -0
  153. package/dist/types/context-menu.d.ts +11 -0
  154. package/dist/types/controls.d.ts +174 -10
  155. package/dist/types/conversation.d.ts +21 -0
  156. package/dist/types/copy-button.d.ts +6 -0
  157. package/dist/types/core.d.ts +31 -0
  158. package/dist/types/data-grid.d.ts +25 -0
  159. package/dist/types/date-picker.d.ts +6 -0
  160. package/dist/types/diff-view.d.ts +8 -0
  161. package/dist/types/dot-plot.d.ts +6 -0
  162. package/dist/types/edit-region.d.ts +23 -0
  163. package/dist/types/empty.d.ts +9 -0
  164. package/dist/types/feature-gate.d.ts +8 -0
  165. package/dist/types/filter-bar.d.ts +8 -0
  166. package/dist/types/formatted-number.d.ts +6 -0
  167. package/dist/types/heatmap.d.ts +6 -0
  168. package/dist/types/inline-citation.d.ts +6 -0
  169. package/dist/types/inline-edit.d.ts +7 -0
  170. package/dist/types/input-otp.d.ts +11 -0
  171. package/dist/types/item-group.d.ts +29 -0
  172. package/dist/types/line-chart.d.ts +6 -0
  173. package/dist/types/link.d.ts +8 -1
  174. package/dist/types/markdown.d.ts +6 -0
  175. package/dist/types/meter.d.ts +6 -0
  176. package/dist/types/nav.d.ts +6 -0
  177. package/dist/types/notifications.d.ts +6 -0
  178. package/dist/types/number-input.d.ts +6 -0
  179. package/dist/types/overlays.d.ts +61 -0
  180. package/dist/types/page-header.d.ts +14 -0
  181. package/dist/types/pagination.d.ts +6 -0
  182. package/dist/types/password-input.d.ts +6 -0
  183. package/dist/types/permission-matrix.d.ts +7 -0
  184. package/dist/types/pile.d.ts +6 -0
  185. package/dist/types/plan-picker.d.ts +6 -0
  186. package/dist/types/prompt-input.d.ts +11 -0
  187. package/dist/types/property-list.d.ts +17 -0
  188. package/dist/types/question.d.ts +11 -0
  189. package/dist/types/relative-time.d.ts +8 -1
  190. package/dist/types/removal-receipt.d.ts +18 -1
  191. package/dist/types/save-bar.d.ts +6 -0
  192. package/dist/types/saved-views.d.ts +12 -0
  193. package/dist/types/scatter-chart.d.ts +8 -0
  194. package/dist/types/schedule.d.ts +21 -0
  195. package/dist/types/secret-field.d.ts +6 -0
  196. package/dist/types/select.d.ts +12 -0
  197. package/dist/types/separator.d.ts +8 -1
  198. package/dist/types/sheet.d.ts +19 -0
  199. package/dist/types/sparkline.d.ts +6 -0
  200. package/dist/types/split-button.d.ts +7 -0
  201. package/dist/types/stat.d.ts +7 -0
  202. package/dist/types/stepper.d.ts +12 -0
  203. package/dist/types/suggestions.d.ts +10 -0
  204. package/dist/types/sync-indicator.d.ts +8 -0
  205. package/dist/types/tag-input.d.ts +8 -0
  206. package/dist/types/terminal.d.ts +6 -0
  207. package/dist/types/thread-list.d.ts +16 -0
  208. package/dist/types/time-input.d.ts +6 -0
  209. package/dist/types/timeline.d.ts +9 -0
  210. package/dist/types/toggle.d.ts +6 -0
  211. package/dist/types/toolbar.d.ts +20 -0
  212. package/dist/types/tour.d.ts +10 -0
  213. package/dist/types/trace.d.ts +16 -0
  214. package/dist/types/tree.d.ts +21 -0
  215. package/dist/types-cjs/agent-fleet.d.cts +28 -0
  216. package/dist/types-cjs/ai-label.d.cts +7 -0
  217. package/dist/types-cjs/app-shell.d.cts +23 -0
  218. package/dist/types-cjs/area-chart.d.cts +8 -0
  219. package/dist/types-cjs/artifact.d.cts +14 -0
  220. package/dist/types-cjs/bar-chart.d.cts +6 -0
  221. package/dist/types-cjs/bar-list.d.cts +6 -0
  222. package/dist/types-cjs/breadcrumb.d.cts +6 -0
  223. package/dist/types-cjs/chart-brush.d.cts +6 -0
  224. package/dist/types-cjs/chart.d.cts +29 -1
  225. package/dist/types-cjs/checklist.d.cts +6 -0
  226. package/dist/types-cjs/code-block.d.cts +6 -0
  227. package/dist/types-cjs/collections.d.cts +15 -0
  228. package/dist/types-cjs/color-picker.d.cts +6 -0
  229. package/dist/types-cjs/command.d.cts +13 -0
  230. package/dist/types-cjs/comment-thread.d.cts +11 -0
  231. package/dist/types-cjs/confirmation.d.cts +8 -0
  232. package/dist/types-cjs/context-menu.d.cts +11 -0
  233. package/dist/types-cjs/controls.d.cts +174 -10
  234. package/dist/types-cjs/conversation.d.cts +21 -0
  235. package/dist/types-cjs/copy-button.d.cts +6 -0
  236. package/dist/types-cjs/core.d.cts +31 -0
  237. package/dist/types-cjs/data-grid.d.cts +25 -0
  238. package/dist/types-cjs/date-picker.d.cts +6 -0
  239. package/dist/types-cjs/diff-view.d.cts +8 -0
  240. package/dist/types-cjs/dot-plot.d.cts +6 -0
  241. package/dist/types-cjs/edit-region.d.cts +23 -0
  242. package/dist/types-cjs/empty.d.cts +9 -0
  243. package/dist/types-cjs/feature-gate.d.cts +8 -0
  244. package/dist/types-cjs/filter-bar.d.cts +8 -0
  245. package/dist/types-cjs/formatted-number.d.cts +6 -0
  246. package/dist/types-cjs/heatmap.d.cts +6 -0
  247. package/dist/types-cjs/inline-citation.d.cts +6 -0
  248. package/dist/types-cjs/inline-edit.d.cts +7 -0
  249. package/dist/types-cjs/input-otp.d.cts +11 -0
  250. package/dist/types-cjs/item-group.d.cts +29 -0
  251. package/dist/types-cjs/line-chart.d.cts +6 -0
  252. package/dist/types-cjs/link.d.cts +8 -1
  253. package/dist/types-cjs/markdown.d.cts +6 -0
  254. package/dist/types-cjs/meter.d.cts +6 -0
  255. package/dist/types-cjs/nav.d.cts +6 -0
  256. package/dist/types-cjs/notifications.d.cts +6 -0
  257. package/dist/types-cjs/number-input.d.cts +6 -0
  258. package/dist/types-cjs/overlays.d.cts +61 -0
  259. package/dist/types-cjs/page-header.d.cts +14 -0
  260. package/dist/types-cjs/pagination.d.cts +6 -0
  261. package/dist/types-cjs/password-input.d.cts +6 -0
  262. package/dist/types-cjs/permission-matrix.d.cts +7 -0
  263. package/dist/types-cjs/pile.d.cts +6 -0
  264. package/dist/types-cjs/plan-picker.d.cts +6 -0
  265. package/dist/types-cjs/prompt-input.d.cts +11 -0
  266. package/dist/types-cjs/property-list.d.cts +17 -0
  267. package/dist/types-cjs/question.d.cts +11 -0
  268. package/dist/types-cjs/relative-time.d.cts +8 -1
  269. package/dist/types-cjs/removal-receipt.d.cts +18 -1
  270. package/dist/types-cjs/save-bar.d.cts +6 -0
  271. package/dist/types-cjs/saved-views.d.cts +12 -0
  272. package/dist/types-cjs/scatter-chart.d.cts +8 -0
  273. package/dist/types-cjs/schedule.d.cts +21 -0
  274. package/dist/types-cjs/secret-field.d.cts +6 -0
  275. package/dist/types-cjs/select.d.cts +12 -0
  276. package/dist/types-cjs/separator.d.cts +8 -1
  277. package/dist/types-cjs/sheet.d.cts +19 -0
  278. package/dist/types-cjs/sparkline.d.cts +6 -0
  279. package/dist/types-cjs/split-button.d.cts +7 -0
  280. package/dist/types-cjs/stat.d.cts +7 -0
  281. package/dist/types-cjs/stepper.d.cts +12 -0
  282. package/dist/types-cjs/suggestions.d.cts +10 -0
  283. package/dist/types-cjs/sync-indicator.d.cts +8 -0
  284. package/dist/types-cjs/tag-input.d.cts +8 -0
  285. package/dist/types-cjs/terminal.d.cts +6 -0
  286. package/dist/types-cjs/thread-list.d.cts +16 -0
  287. package/dist/types-cjs/time-input.d.cts +6 -0
  288. package/dist/types-cjs/timeline.d.cts +9 -0
  289. package/dist/types-cjs/toggle.d.cts +6 -0
  290. package/dist/types-cjs/toolbar.d.cts +20 -0
  291. package/dist/types-cjs/tour.d.cts +10 -0
  292. package/dist/types-cjs/trace.d.cts +16 -0
  293. package/dist/types-cjs/tree.d.cts +21 -0
  294. package/package.json +1 -1
@@ -0,0 +1,263 @@
1
+ # EditRegion
2
+
3
+ Edit several fields of one record where the record is shown, and save them together as one write. Use it when two or more related values change together (a contact's name, email and role; a task's title, assignee and due date): several `InlineEdit`s would save one at a time and could leave the record half-changed, and a `Dialog` would cover the neighbouring records people look at to judge what a reasonable value is.
4
+
5
+ ```tsx
6
+ <EditRegion label="Contact" value={contact} onValueChange={setContact}
7
+ validate={(draft) => draft.email.includes('@') ? undefined : { email: 'Enter an email address.' }}
8
+ onSave={async (draft, { previousValue, changed }) => {
9
+ const result = await api.patchContact(contact.id, pick(draft, changed), contact.revision);
10
+ return result.ok ? { ok: true, value: result.contact } : { ok: false, error: result.message, fieldErrors: result.fieldErrors };
11
+ }}>
12
+ <EditRegionView>
13
+ <strong>{contact.name}</strong> <span>{contact.email}</span>
14
+ </EditRegionView>
15
+ <EditRegionForm>
16
+ {(field) => <>
17
+ <Field label="Name" required error={field.error('name')}><Input {...field('name')} /></Field>
18
+ <Field label="Email" required error={field.error('email')}><Input type="email" {...field('email')} /></Field>
19
+ <Field label="Role"><Select {...field.value('role')} items={roles} /></Field>
20
+ <Switch {...field.checked('notify')} label="Email them when this project changes" />
21
+ </>}
22
+ </EditRegionForm>
23
+ </EditRegion>
24
+ ```
25
+
26
+ ## Elevation
27
+
28
+ - **Read mode** is the record on the ground (level 0): the values as text and a visible ghost “Edit <label>” button.
29
+ - **Editing** lifts the region into a static raised card (level 1, `surface-raised`, `elev-1`, a `line-control` border), as an `Accordion` lid lifts over its compartment. The card's padding and border are there in read mode too, transparent, under a matching negative margin, so lifting changes colour and shadow but never moves the record. Leave room for that margin either side: `space-3` plus 1px, or 22px with `layout="in-place"`. It doesn't lift on hover: it's being worked in, not offered. The fields inside are openings: empty wells, filled flush, invalid trenches, as `Input`, `Select` and `Field` draw them.
30
+ - **Saving** uses `Surface status="pending"`: after `dur-grace` it hovers at level 2, so a fast save shows nothing. **A known failure** drops to the failed trench with a critical `Alert` inside the editor; the draft stays. **Accepted** values settle back to the ground with one afterglow for the region, however many fields changed.
31
+ - **An outside change** while editing keeps the draft, blocks Save and shows a caution `Alert` “This record changed” with “Use current values”.
32
+
33
+ The words carry every state without height: the form's name, the button labels, `aria-invalid` and error text on fields, `aria-busy` on the form while saving, and the polite status (“Checking”, “Saving changes”, “Saved” or the error), which is announced, never shown, and clears itself after `dur-linger`. The afterglow fills the padded card, so it never sits against the text. Under forced colours the editor is a raised Surface with a border; under `contrast="more"` it gets level 1's ink slab.
34
+
35
+ ## Props
36
+
37
+ | Prop | |
38
+ |---|---|
39
+ | `label` | names the region: the form is “Edit <label>” |
40
+ | `value`, `onValueChange` | the committed record, and a synchronous setter for the accepted canonical record (no network write) |
41
+ | `onSave(draft, { previousValue, changed })` | the sole write. Return `{ ok: true, value }` with the canonical record, or `{ ok: false, error, fieldErrors? }` for a known failure. A throw or malformed result shows a generic retry message |
42
+ | `validate(draft)` | on Save: an error per field, or `undefined`; sync or a promise |
43
+ | `open`, `defaultOpen`, `onOpenChange` | control the session. Closing a controlled region doesn't cancel a dispatched save |
44
+ | `disabled`, `initialFocus`, `focusFallback`, `isEqual` | as `InlineEdit`; `initialFocus` names a field key; `isEqual` compares one field's values (default `Object.is`) |
45
+ | `editLabel`, `saveLabel`, `cancelLabel` | default “Edit <label>”, “Save changes”, “Cancel” |
46
+ | `settleDuration` | how long the afterglow takes to fade after a save, in ms. Default 5000 (shorter than `dur-settle`, because the person is watching the change) |
47
+ | `layout` | `'form'` (default): editing opens your form. `'in-place'`: each value's well opens where its text is (below) |
48
+
49
+ Parts: `EditRegionView` (read mode; `trigger={false}` to place `EditRegionTrigger` yourself), `EditRegionValue` (one value in read mode, named by its field; `reserve` for labelled rows), `EditRegionForm` (edit mode; children may be a function of the draft binder; `actions={false}` to place `EditRegionActions` yourself), `EditRegionTrigger`, `EditRegionActions`.
50
+
51
+ The binder: `field(key)` for text controls (`value` + `onChange`, `readOnly` while locked), `field.value(key)` for `Select`, `Combobox` and `Slider` (`value` + `onValueChange`), `field.checked(key)` for `Switch` and `Checkbox`, `field.error(key)` for the field's `Field`, and `field.draft`. Each binding carries `data-field`, which `initialFocus` and error focus use.
52
+
53
+ ## In place: `layout="in-place"`
54
+
55
+ With `layout="in-place"` the fields open where the values are, as a single `InlineEdit` does, instead of a form replacing the record.
56
+
57
+ ```tsx
58
+ <EditRegion layout="in-place" label={person.name} value={person} onValueChange={…} onSave={…}>
59
+ <EditRegionView>
60
+ <span className="name"><EditRegionValue field="name">{person.name}</EditRegionValue></span>
61
+ <span className="meta"><EditRegionValue field="email">{person.email}</EditRegionValue> · <EditRegionValue field="role">{roleName}</EditRegionValue></span>
62
+ </EditRegionView>
63
+ <EditRegionForm>
64
+ {(field) => <>
65
+ <div className="name"><Field label="Name" error={field.error('name')}><Input {...field('name')} /></Field></div>
66
+ <div className="meta-row">
67
+ <Field label="Email" error={field.error('email')}><Input {...field('email')} /></Field>
68
+ <Field label="Role"><Select {...field.value('role')} items={roles} /></Field>
69
+ </div>
70
+ </>}
71
+ </EditRegionForm>
72
+ </EditRegion>
73
+ ```
74
+
75
+ - **Mark each read value** with `EditRegionValue field="…"`, using the same key you bind. A field whose text opens where its value was simply appears; one that opens elsewhere (a control wider or taller than its text pushes the next one along) fades in there. Nothing slides.
76
+ - **Lay the fields out where the values were**, in the same wrappers, so each control takes the value's font. Controls reach 13px past their column on each side, the inset their text sits at, so the text lines up and the well reaches into the gutter: leave 26px more between columns than you want to see. Give rows fixed columns (a grid) rather than columns sized by what's typed, so nothing reflows while the editor opens. The form adds no spacing of its own.
77
+ - **A `Field`'s label is announced, not shown**: the record around the field already names it (a row's label, the value's place). Its error, `aria-invalid` and description still work.
78
+ - **Labelled rows**: `EditRegionValue reserve` gives a read value its control's height and inset now. In a list of label–value rows, Edit all then moves nothing, and only the actions row is added at the end.
79
+
80
+ ## Behaviour
81
+
82
+ - Edit opens the form and focuses the first field (or `initialFocus`). Tab moves through the fields and out to the page; blur and outside clicks do nothing to the session (no save on blur, as `InlineEdit`).
83
+ - Enter in a single-line field saves; Ctrl/Cmd+Enter in a textarea saves. Nothing acts during IME composition. Escape cancels, except while a popup inside (a Select's list) is open: that Escape closes the popup, and the next one cancels. Escape never cancels a dispatched save; it announces “Saving changes. Wait for the result.”
84
+ - Saving an unchanged draft closes without a write. `changed` lists only the fields that differ, so the application can send a patch.
85
+ - Validation errors mark each field and focus the first invalid one; typing in a field clears its error. A save's `fieldErrors` are shown the same way, beside the `error` Alert.
86
+ - One write per record may be in flight: Save, Enter and repeated presses dispatch once. While saving, text fields are read-only and choice controls disabled.
87
+ - An observed `value` change during a save latches a conflict: the result is never published, and the message says the save may have completed. The application must reconcile its server state.
88
+ - Completion returns focus to the Edit button only if focus was still in the editor. Unmounting invalidates pending work and suppresses publication.
89
+ - Inside another `<form>`, the editor is a `role="group"` instead of a form, so forms don't nest.
90
+
91
+ ## You provide
92
+
93
+ The record, the write (with a server-side revision or equivalent concurrency guard), validation policy, and reconciliation of interrupted writes, exactly as for `InlineEdit`. EditRegion doesn't autosave, keep history, or roll back a dispatched write.
94
+
95
+ ## Do and don't
96
+
97
+ - **Do** keep the record's neighbours visible: that's why it edits in place.
98
+ - **Do** use `InlineEdit` for a single value; EditRegion is for values that must change together.
99
+ - **Don't** put more than about five fields in one region. A long form belongs in a `Sheet` beside the page.
100
+ - **Don't** update `value` optimistically from `onSave`: an observed change is treated as someone else's write.
101
+
102
+ ## Demo and verification
103
+
104
+ The `EditRegion` section of `build/previews.txt` lists three people with access; each row is an in-place EditRegion over a fictional people service with revisions. Below it, trip details are label–value rows with reserved values. A collapsed harness picks a save fixture (`success`, `slow`, `fail-once`, `conflict-during-save`) and applies an outside update. `node checks/edit-region.mjs` (from `build/`, after rendering previews; `ALT_FIXTURE_ROOT` selects React 19 fixtures) covers editing in place (fields hold their place on every frame while the editor grows, the record below is pushed through the glide, lifting never moves the record, each field opens at its value's x, reserved values open exactly where they were, the padded afterglow, the announcement that clears itself), opening and focus, unchanged saves, one write per save, `changed`, validation and error focus, Escape with an open Select, IME Enter, failure and retry, pending lock, both conflict paths, focus return, and a 320/960px layout in both schemes under reduced motion, forced colours and `contrast="more"`.
105
+
106
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
107
+ ## Example
108
+
109
+ `EditRegion` as first used in its preview:
110
+
111
+ ```tsx
112
+ import { EditRegion, EditRegionForm, EditRegionValue, EditRegionView, Field, Input, Select, Switch } from 'altitude-design';
113
+
114
+ <EditRegion layout="in-place" label={person.name} editLabel={`Edit ${person.name}`} value={erFields(person)} validate={validate}
115
+ onValueChange={canonical => setPeople(people => people.map(p => p.id === person.id ? { ...server.current.get(person.id), ...canonical } : p))}
116
+ onSave={save(person)}>
117
+ <EditRegionView>
118
+ <span className="er-name"><EditRegionValue field="name">{person.name}</EditRegionValue></span>
119
+ <span className="er-meta"><EditRegionValue field="email">{person.email}</EditRegionValue> · <EditRegionValue field="role">{ER_ROLES.find(r => r.value === person.role).label}</EditRegionValue>{person.notify ? <> · <EditRegionValue field="notify">Gets updates</EditRegionValue></> : ''}</span>
120
+ </EditRegionView>
121
+ <EditRegionForm>
122
+ {field => <>
123
+ <div className="er-name-edit"><Field label="Name" required error={field.error('name')}><Input {...field('name')} autoComplete="off" /></Field></div>
124
+ <div className="er-meta-edit">
125
+ <Field label="Email" required error={field.error('email')}><Input {...field('email')} type="email" autoComplete="off" /></Field>
126
+ <Field label="Role"><Select {...field.value('role')} items={ER_ROLES} /></Field>
127
+ <Switch {...field.checked('notify')} label="Gets updates" />
128
+ </div>
129
+ </>}
130
+ </EditRegionForm>
131
+ </EditRegion>
132
+ ```
133
+
134
+ The full preview, as rendered in Storybook and checked in CI (`er-list`, `er-name`, `er-meta`, `er-name-edit`, `er-meta-edit`, `er-harness`, `er-harness-controls`, `er-log`, `er-sheet`, `er-sheet-head`, `er-sheet-grid`, `er-demo` are the preview page's own layout classes, not Altitude's):
135
+
136
+ ```tsx
137
+ import * as React from 'react';
138
+ import { Button, EditRegion, EditRegionForm, EditRegionTrigger, EditRegionValue, EditRegionView, Field, Input, Select, Switch } from 'altitude-design';
139
+
140
+ const ER_ROLES = [{ value: 'owner', label: 'Owner' }, { value: 'editor', label: 'Editor' }, { value: 'viewer', label: 'Viewer' }];
141
+ const ER_PEOPLE = Object.freeze([
142
+ Object.freeze({ id: 'p-mara', name: 'Mara Quist', email: 'mara@northwind.example', role: 'owner', notify: true, revision: 1 }),
143
+ Object.freeze({ id: 'p-ines', name: 'Inés Ortega', email: 'ines@northwind.example', role: 'editor', notify: false, revision: 1 }),
144
+ Object.freeze({ id: 'p-tobi', name: 'Tobi Adeyemi', email: 'tobi@northwind.example', role: 'viewer', notify: true, revision: 1 }),
145
+ ]);
146
+ const ER_FIXTURES = ['success', 'slow', 'fail-once', 'conflict-during-save'];
147
+ const erWait = ms => new Promise(resolve => setTimeout(resolve, ms));
148
+ const erFields = ({ revision, id, ...fields }) => fields;
149
+
150
+ // A fictional people service with revisions: a stale write is rejected, an accepted one returns the canonical record.
151
+ function EditRegionExample({ fixture }) {
152
+ const server = React.useRef(new Map(ER_PEOPLE.map(p => [p.id, { ...p }])));
153
+ const [people, setPeople] = React.useState(() => ER_PEOPLE.map(p => ({ ...p })));
154
+ const [log, setLog] = React.useState([]);
155
+ const failed = React.useRef(false);
156
+ const trace = event => setLog(log => [...log, event]);
157
+ const reconcile = id => setPeople(people => people.map(p => p.id === id ? { ...server.current.get(id) } : p));
158
+ const externalUpdate = () => {
159
+ const current = server.current.get('p-mara');
160
+ server.current.set('p-mara', { ...current, email: 'm.quist@northwind.example', revision: current.revision + 1 });
161
+ trace({ event: 'external-update', id: 'p-mara', revision: current.revision + 1 }); reconcile('p-mara');
162
+ };
163
+ const save = person => async (draft, { changed }) => {
164
+ trace({ event: 'requested', id: person.id, expectedRevision: person.revision, changed });
165
+ await erWait(fixture === 'slow' ? 1400 : 250);
166
+ if (fixture === 'fail-once' && !failed.current) { failed.current = true; trace({ event: 'rejected', id: person.id }); return { ok: false, error: 'We couldn’t reach the people service. Your changes are still here. Try again.' }; }
167
+ if (fixture === 'conflict-during-save') { externalUpdate(); await erWait(300); }
168
+ const current = server.current.get(person.id);
169
+ if (current.revision !== person.revision) { trace({ event: 'rejected-stale', id: person.id }); return { ok: false, error: 'Someone else changed this person. Review the current details.' }; }
170
+ if ([...server.current.values()].some(p => p.id !== person.id && p.email === draft.email.trim())) return { ok: false, error: 'Check the highlighted field.', fieldErrors: { email: 'Someone else already uses this email.' } };
171
+ const next = { ...current, ...draft, name: draft.name.trim(), email: draft.email.trim(), revision: current.revision + 1 };
172
+ server.current.set(person.id, next);
173
+ trace({ event: 'committed', id: person.id, revision: next.revision, changed });
174
+ return { ok: true, value: erFields(next) };
175
+ };
176
+ const validate = draft => {
177
+ const errors = {};
178
+ if (!draft.name.trim()) errors.name = 'Enter a name.';
179
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(draft.email.trim())) errors.email = 'Enter an email address, like name@example.com.';
180
+ return Object.keys(errors).length ? errors : undefined;
181
+ };
182
+ return <>
183
+ <section aria-labelledby="er-title">
184
+ <h2 id="er-title">People with access</h2>
185
+ <ul className="er-list">
186
+ {people.map(person => <li key={person.id}>
187
+ <EditRegion layout="in-place" label={person.name} editLabel={`Edit ${person.name}`} value={erFields(person)} validate={validate}
188
+ onValueChange={canonical => setPeople(people => people.map(p => p.id === person.id ? { ...server.current.get(person.id), ...canonical } : p))}
189
+ onSave={save(person)}>
190
+ <EditRegionView>
191
+ <span className="er-name"><EditRegionValue field="name">{person.name}</EditRegionValue></span>
192
+ <span className="er-meta"><EditRegionValue field="email">{person.email}</EditRegionValue> · <EditRegionValue field="role">{ER_ROLES.find(r => r.value === person.role).label}</EditRegionValue>{person.notify ? <> · <EditRegionValue field="notify">Gets updates</EditRegionValue></> : ''}</span>
193
+ </EditRegionView>
194
+ <EditRegionForm>
195
+ {field => <>
196
+ <div className="er-name-edit"><Field label="Name" required error={field.error('name')}><Input {...field('name')} autoComplete="off" /></Field></div>
197
+ <div className="er-meta-edit">
198
+ <Field label="Email" required error={field.error('email')}><Input {...field('email')} type="email" autoComplete="off" /></Field>
199
+ <Field label="Role"><Select {...field.value('role')} items={ER_ROLES} /></Field>
200
+ <Switch {...field.checked('notify')} label="Gets updates" />
201
+ </div>
202
+ </>}
203
+ </EditRegionForm>
204
+ </EditRegion>
205
+ </li>)}
206
+ </ul>
207
+ </section>
208
+ <TripSheet />
209
+ <details className="er-harness">
210
+ <summary>Fixture controls</summary>
211
+ <p>Documentation harness only: change Mara's record on the “server” while it's being edited.</p>
212
+ <div className="er-harness-controls"><Button onClick={externalUpdate}>Apply external update to Mara</Button></div>
213
+ <output className="er-log" data-er-log="" aria-label="Fixture operation log">{JSON.stringify(log, null, 2)}</output>
214
+ </details>
215
+ </>;
216
+ }
217
+
218
+ // Labelled rows whose values already have their controls' height (EditRegionValue reserve): Edit all moves nothing.
219
+ const ER_SEATS = [{ value: 'aisle', label: 'Aisle' }, { value: 'window', label: 'Window' }, { value: 'any', label: 'No preference' }];
220
+ function TripSheet() {
221
+ const [trip, setTrip] = React.useState({ traveller: 'Mara Quist', email: 'mara@northwind.example', phone: '', seat: 'aisle' });
222
+ const validate = draft => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(draft.email.trim()) ? undefined : { email: 'Enter an email address, like name@example.com.' };
223
+ const rows = [['traveller', 'Traveller'], ['email', 'Email'], ['phone', 'Phone'], ['seat', 'Seat']];
224
+ return <section className="er-sheet" aria-labelledby="er-sheet-title">
225
+ <EditRegion layout="in-place" label="Trip details" editLabel="Edit all trip details" value={trip} validate={validate} onValueChange={setTrip}
226
+ onSave={async draft => { await erWait(250); return { ok: true, value: { ...draft, traveller: draft.traveller.trim(), email: draft.email.trim(), phone: draft.phone.trim() } }; }}>
227
+ <EditRegionView trigger={false}>
228
+ <div className="er-sheet-head"><h2 id="er-sheet-title">Trip details</h2><EditRegionTrigger>Edit all</EditRegionTrigger></div>
229
+ <dl className="er-sheet-grid">{rows.map(([key, label]) => <React.Fragment key={key}><dt>{label}</dt><dd>
230
+ <EditRegionValue field={key} reserve>{key === 'seat' ? ER_SEATS.find(s => s.value === trip.seat).label : trip[key] || <span className="er-meta">Not set</span>}</EditRegionValue>
231
+ </dd></React.Fragment>)}</dl>
232
+ </EditRegionView>
233
+ <EditRegionForm>
234
+ {field => <>
235
+ <div className="er-sheet-head"><h2>Trip details</h2></div>
236
+ <dl className="er-sheet-grid">
237
+ <dt>Traveller</dt><dd><Field label="Traveller" required error={field.error('traveller')}><Input {...field('traveller')} autoComplete="off" /></Field></dd>
238
+ <dt>Email</dt><dd><Field label="Email" required error={field.error('email')}><Input {...field('email')} type="email" autoComplete="off" /></Field></dd>
239
+ <dt>Phone</dt><dd><Field label="Phone"><Input {...field('phone')} type="tel" placeholder="Add a phone number" autoComplete="off" /></Field></dd>
240
+ <dt>Seat</dt><dd><Field label="Seat"><Select {...field.value('seat')} items={ER_SEATS} /></Field></dd>
241
+ </dl>
242
+ </>}
243
+ </EditRegionForm>
244
+ </EditRegion>
245
+ </section>;
246
+ }
247
+
248
+ export default function App() {
249
+ const [fixture, setFixture] = React.useState('success');
250
+ const [mounted, setMounted] = React.useState({ fixture: 'success', epoch: 0 });
251
+ return <div className="er-demo">
252
+ <EditRegionExample key={mounted.epoch} fixture={mounted.fixture} />
253
+ <details className="er-harness">
254
+ <summary>Choose the next fixture</summary>
255
+ <div className="er-harness-controls">
256
+ <label>Save fixture<select value={fixture} onChange={event => setFixture(event.target.value)}>{ER_FIXTURES.map(name => <option key={name}>{name}</option>)}</select></label>
257
+ <Button onClick={() => setMounted({ fixture, epoch: mounted.epoch + 1 })}>Reset example</Button>
258
+ </div>
259
+ </details>
260
+ </div>;
261
+ }
262
+ ```
263
+ <!-- /example -->
@@ -0,0 +1,66 @@
1
+ # EmptyState
2
+
3
+ What a list, board, search or page shows when it has nothing to show: a statement of what's empty, why, and one obvious next step.
4
+
5
+ **Why it's sunk.** An empty region isn't a problem (that's `Alert`) and it isn't a settled container (that's `Card`): it's a space waiting to receive, where the record hasn't landed yet. So it's an opening, a well cut into the page (`surface-well`, `well`) with a dashed `line-control` rim, the same below-ground signal as an empty field or an armed `DropZone`: sunken things want content. Its actions are ordinary raised buttons standing in it, the primary one at level 2. Pointing at or tabbing to the primary action deepens the well to a trench and darkens its rim to `ink-muted`, showing where the record it creates will land. When it appears it sinks in from flush (`dur-travel-1`, `ease-sink`), the inverse of new content arriving lifted; it never pulses or loops, because an empty state must never beg.
6
+
7
+ **Without height.** The rim is dashed while the region is empty, and the title says so in words; the icon is in `currentColor`. Under forced colours the rim is a dashed `CanvasText` border, like every sunk surface; with `contrast="more"` it turns solid.
8
+
9
+ **Accessibility.** It's a `section` named by its title (`aria-labelledby`, an `h3` by default; set `headingLevel` to fit the page's outline) and described by its description. Actions are real buttons or links in the normal tab order: nothing is focused for you and nothing traps focus. When an empty follows something the person did (a filter or a search that matched nothing), pass `announce` and it's said politely once it appears: `true` says the title, a string says that.
10
+
11
+ **You provide:** `title` (what's empty, as a statement), usually a `description` (why, or what will appear here), an `icon` (a Lucide line icon), and `action` (one primary `Button` whose label starts with a verb). Optionally `secondary` (a ghost `Button` or a link), `children` (extra content above the actions), `size="sm"` for a widget or a list, and `announce`.
12
+
13
+ - Do name what's empty and what to do: "No gift ideas yet" and **Add idea**, not "Nothing here" and **OK**.
14
+ - Do say which kind of empty it is in words: nothing yet, nothing matches, or you can't see it. Colour and height don't tell them apart.
15
+ - Do give it one primary action at most; a second one is `secondary`, quieter.
16
+ - Don't use it for an error that stopped content loading: that's a problem to fix, an `Alert`.
17
+ - Don't raise it into a card, or make it the only thing on a page that's still loading: show a `Skeleton` until you know it's empty.
18
+
19
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
20
+ ## Example
21
+
22
+ `EmptyState` as first used in its preview:
23
+
24
+ ```tsx
25
+ import { Button, EmptyState } from 'altitude-design';
26
+
27
+ <EmptyState icon={<Gift />} title="No gift ideas yet" headingLevel={2}
28
+ description="Ideas you save stay private to you until you share them."
29
+ action={<Button variant="primary">Add idea</Button>}
30
+ secondary={<Button variant="ghost">Import a list</Button>} />
31
+ ```
32
+
33
+ The full preview, as rendered in Storybook and checked in CI (`two`, `lab`, `side`, `cap` are the preview page's own layout classes, not Altitude's):
34
+
35
+ ```tsx
36
+ import * as React from 'react';
37
+ import { Button, EmptyState } from 'altitude-design';
38
+
39
+ const Gift = () => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="8" width="18" height="4" rx="1" /><path d="M12 8v13M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7M7.5 8a2.5 2.5 0 0 1 0-5C10 3 12 8 12 8s2-5 4.5-5a2.5 2.5 0 0 1 0 5" /></svg>;
40
+ const Search = () => <svg 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>;
41
+ const Bell = () => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10.3 21a1.94 1.94 0 0 0 3.4 0" /></svg>;
42
+ export default function App() {
43
+ const [q, setQ] = React.useState('lamp');
44
+ return (
45
+ <div className="two">
46
+ <div><div className="lab">md · a page with nothing yet</div>
47
+ <EmptyState icon={<Gift />} title="No gift ideas yet" headingLevel={2}
48
+ description="Ideas you save stay private to you until you share them."
49
+ action={<Button variant="primary">Add idea</Button>}
50
+ secondary={<Button variant="ghost">Import a list</Button>} />
51
+ </div>
52
+ <div className="side">
53
+ <div><div className="lab">sm · a widget</div>
54
+ <EmptyState size="sm" icon={<Bell />} title="You're all caught up" description="New mentions and replies appear here." />
55
+ </div>
56
+ <div><div className="lab">sm · after a search</div>
57
+ {q ? <EmptyState size="sm" icon={<Search />} title={`No ideas match \u201C${q}\u201D`} announce="No matching ideas"
58
+ description="Try another word, or clear the search." action={<Button size="sm" onClick={() => setQ('')}>Clear search</Button>} />
59
+ : <p className="cap">Search cleared.</p>}
60
+ </div>
61
+ </div>
62
+ </div>
63
+ );
64
+ }
65
+ ```
66
+ <!-- /example -->
@@ -0,0 +1,67 @@
1
+ # FeatureGate
2
+
3
+ A feature that needs another plan, shown to people on a plan that doesn't include it: "Export to CSV · Business", "SAML SSO · Enterprise", a menu item with a lock.
4
+
5
+ **Elevation: locked, not disabled.** Disabled means "nothing happens": flush, `ink-faint`, skipped by the keyboard and often by screen readers. Then the people who rely on those never learn the feature exists, and `ink-faint` fails contrast. **A locked control does something**: it explains the plan and offers the upgrade. So it stays live:
6
+ - it keeps the wrapped control's own build and normal ink;
7
+ - **it lifts when pointed at** (hover is a promise);
8
+ - it carries a lock and a flush `Badge` plate naming the plan.
9
+
10
+ Pressing it opens a `Popover` (level 3): "Available on Business", what the plan adds, and "Upgrade to Business" as the popover's one primary action, with "Compare plans" if you give `compareHref`. When this person can't upgrade (`canUpgrade={false}`, not an admin), the button becomes "Ask an admin to upgrade to Business."
11
+
12
+ **Accessibility.** The control stays a button with `aria-haspopup="dialog"`. **It's never `aria-disabled`.** Its name includes the requirement ("Export to CSV, requires Business plan"). It's reachable and activatable by keyboard, Escape closes the explanation, and focus returns to the control. The wrapped control's `onClick` and `disabled` are dropped: what it does while locked is explain.
13
+
14
+ **Props:** `requires`; `description`; `onUpgrade`; `compareHref`; `canUpgrade` (default true); `children` (one control).
15
+
16
+ **You provide:** the control, the plan it needs, what the plan adds, and where upgrading goes.
17
+
18
+ ```tsx
19
+ <FeatureGate requires="Business" onUpgrade={() => openBilling('business')}
20
+ description="Export any view to CSV and schedule exports by email.">
21
+ <Button variant="secondary">Export to CSV</Button>
22
+ </FeatureGate>
23
+ ```
24
+
25
+ - Do keep locked features visible, so people know they exist.
26
+ - Do say what the plan adds, not just its name.
27
+ - Don't use `disabled` for a feature that needs another plan.
28
+ - Don't hide the upgrade from people who can't buy it; tell them who can.
29
+
30
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
31
+ ## Example
32
+
33
+ `FeatureGate` as first used in its preview:
34
+
35
+ ```tsx
36
+ import { Button, FeatureGate } from 'altitude-design';
37
+
38
+ <FeatureGate requires="Business" description="Export any view to CSV, and schedule exports by email." onUpgrade={() => setSaid('Opening plans…')} compareHref="#/billing/plans">
39
+ <Button variant="secondary">Export to CSV</Button>
40
+ </FeatureGate>
41
+ ```
42
+
43
+ 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):
44
+
45
+ ```tsx
46
+ import * as React from 'react';
47
+ import { Button, FeatureGate } from 'altitude-design';
48
+
49
+ export default function App() {
50
+ const [said, setSaid] = React.useState('');
51
+ return (
52
+ <div className="col">
53
+ <div className="row">
54
+ <Button variant="secondary">Export view</Button>
55
+ <FeatureGate requires="Business" description="Export any view to CSV, and schedule exports by email." onUpgrade={() => setSaid('Opening plans…')} compareHref="#/billing/plans">
56
+ <Button variant="secondary">Export to CSV</Button>
57
+ </FeatureGate>
58
+ <FeatureGate requires="Enterprise" description="Sign in with your identity provider (Okta, Azure AD, Google)." canUpgrade={false}>
59
+ <Button variant="ghost">SAML SSO</Button>
60
+ </FeatureGate>
61
+ </div>
62
+ <p className="cap" role="status">{said || 'Locked features keep their ink, lift when pointed at and are reachable by keyboard; pressing one explains the plan. Without permission to upgrade, it says to ask an admin.'}</p>
63
+ </div>
64
+ );
65
+ }
66
+ ```
67
+ <!-- /example -->
@@ -0,0 +1,46 @@
1
+ # Field
2
+
3
+ Pairs one control with its label, an optional hint and an error message that emerges beneath it.
4
+
5
+ `Field` wires the `id`, `aria-describedby`, `required` and `invalid` props onto its single child, so the label, hint and error are announced with the control. Existing child description IDs are preserved and deduplicated with the rendered hint and error IDs. Both hint and error stay available when an error is shown; the control sinks to its invalid state, and the message emerges from under the field (`alt-emerge`, `dur-travel-2`) in `critical` with an icon.
6
+
7
+ **You provide:** `label`, optionally `hint`, `error` (a string or node; falsy when valid) and `required`, and exactly one control as the child. A child’s `required` prop is preserved unless Field explicitly supplies one.
8
+
9
+ - Do write the error as the fix: "Use at least 5 characters", not "Invalid input".
10
+ - Don't show an error before the person has finished with the field; validate on blur or submit.
11
+
12
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
13
+ ## Example
14
+
15
+ `Field` as first used in its preview:
16
+
17
+ ```tsx
18
+ import { Field, Input } from 'altitude-design';
19
+
20
+ <Field label="Workspace name" hint="Shown to everyone you invite.">
21
+ <Input defaultValue="Northwind Studio" />
22
+ </Field>
23
+ ```
24
+
25
+ The full preview, as rendered in Storybook and checked in CI (`grid` is the preview page's own layout class, not Altitude's):
26
+
27
+ ```tsx
28
+ import * as React from 'react';
29
+ import { Field, Input } from 'altitude-design';
30
+
31
+ export default function App() {
32
+ const [v, setV] = React.useState('acme');
33
+ const err = v.length < 5 ? 'Use at least 5 characters.' : null;
34
+ return (
35
+ <div className="grid">
36
+ <Field label="Workspace name" hint="Shown to everyone you invite.">
37
+ <Input defaultValue="Northwind Studio" />
38
+ </Field>
39
+ <Field label="URL slug" required error={err}>
40
+ <Input value={v} onChange={(e) => setV(e.target.value)} />
41
+ </Field>
42
+ </div>
43
+ );
44
+ }
45
+ ```
46
+ <!-- /example -->
@@ -0,0 +1,132 @@
1
+ # FilterBar
2
+
3
+ Build and change a query as a row of conditions, each edited in place: “Status is In review”, “Assignee is Mara Quist”, “Updated between 1 Oct and 20 Oct”. Click a condition's field, operator or value to change just that part; add one with Add filter; remove one with its ×. Pair it with `DataTable`'s `relevance` so the results filter in place: matches rise, misses sink, and nothing reshuffles.
4
+
5
+ ```tsx
6
+ const fields: FilterField[] = [
7
+ { key: 'status', label: 'Status', type: 'enum', options: statuses },
8
+ { key: 'assignee', label: 'Assignee', type: 'enum', options: people, searchable: true },
9
+ { key: 'updated', label: 'Updated', type: 'date' }, // before, after, between
10
+ { key: 'title', label: 'Title', type: 'text' }, // contains, does not contain
11
+ ];
12
+
13
+ <FilterBar fields={fields} value={filters} onValueChange={setFilters} match={match} onMatchChange={setMatch} resultCount={visible.length} />
14
+ <DataTable rows={rows} relevance={(r) => filters.length ? (matches(r, filters) ? 'match' : 'miss') : undefined} … />
15
+ ```
16
+
17
+ The value is local query state, `{ id, field, operator, value }[]`, so there's no pending state: the application decides what each condition means (`matches` above). Dates are ISO `'YYYY-MM-DD'` strings; `between` takes `{ start, end }`.
18
+
19
+ ## Elevation
20
+
21
+ - **The bar and its conditions** are part of what you're looking at, so they're on the ground: each condition is a plate stamped flush like a Badge, split into segments (field · condition · value · ×).
22
+ - **A segment** opens a picker, so it lifts one level when pointed at or open (hover is a promise). Pickers float at level 3, as `Select`'s list does.
23
+ - **A condition waiting for its value** keeps that segment as an opening: a small dashed well reading “Choose a value”. **Add filter** is an opening too, a dashed well that deepens to a trench under the pointer.
24
+ - **A changed condition** is re-stamped (down 3px, settling), as a Badge whose label changes.
25
+ - **Adding** a condition: the field list sinks, then the new condition's value picker rises. The two never move at once.
26
+
27
+ Without height: the bar is a toolbar named “Filters” (or your `aria-label`); each condition is a group named by its sentence; each segment says what it is and what it does (“Value: In review. Change value”); the remove button names the condition; changes announce the new sentence and, with `resultCount`, the count (“Status is Done. 12 results”). Forced colours: conditions 1px, openings dashed, pickers 2px. `contrast="more"`: a hovered segment gets a 2px slab.
28
+
29
+ ## Keyboard
30
+
31
+ The bar is one Tab stop (the toolbar pattern): ←/→ move between segments (mirrored in RTL), Home/End go to the ends. Enter, Space or ↓ opens a segment's picker; in a list, ↑/↓, Home/End and Enter choose; Escape closes it and leaves focus on the segment. Delete or Backspace on any segment removes its whole condition; focus moves to the next condition, else the previous, else Add filter. Text and number pickers apply on Enter; a date range applies once it has both ends.
32
+
33
+ ## Props
34
+
35
+ | Prop | |
36
+ |---|---|
37
+ | `fields` | `{ key, label, type, options?, searchable?, operators? }`; operators default by type |
38
+ | `value`, `onValueChange` | the conditions |
39
+ | `match`, `onMatchChange` | `'all'` or `'any'`, shown as a segment once there are two conditions |
40
+ | `resultCount` | announced after each change |
41
+ | `createId`, `aria-label`, `className` | |
42
+
43
+ `filterValueText(field, value)` gives a value as words, for your own summaries.
44
+
45
+ ## You provide
46
+
47
+ What each condition means for your rows, and the results. Saved views and nested groups (A and (B or C)) aren't covered.
48
+
49
+ ## Do and don't
50
+
51
+ - **Do** pair it with `relevance` so the result set changes in place.
52
+ - **Do** keep fields to the handful people actually filter by; a long field list gets a search field automatically past eight.
53
+ - **Don't** use it for a one-off search box: that's an `Input` in the table's toolbar.
54
+
55
+ ## Demo and verification
56
+
57
+ The `FilterBar` section of `build/previews.txt` filters an issue table in place. `node checks/filter-bar.mjs` (from `build/`; `ALT_FIXTURE_ROOT` selects React 19 fixtures) covers the one Tab stop and arrow keys, opening and closing pickers, changing a value and an operator, the announcement with its count, re-stamping, Add filter with its field-then-value sequence and the empty value opening, text and date-range values, Match all/any, removal with focus repair, the paired table's relevance, and 320/960px layouts in both schemes under reduced motion, forced colours and `contrast="more"`.
58
+
59
+ ## Nested groups
60
+
61
+ Automation rules, segments and advanced search need nested logic: "Plan is Pro **and** any of (Country is Germany, Seats > 50)". Pass a `FilterTree` and `nesting={2}`:
62
+
63
+ ```tsx
64
+ <FilterBar fields={fields} nesting={2} value={tree} onValueChange={setTree} />
65
+ // tree: { match: 'all', items: [Filter | { id, match: 'any', items: Filter[] }] }
66
+ ```
67
+
68
+ A group is drawn as **a flush bracket on the ground**: a 1px `line-control` rule around its own conditions, with its own all/any switch (shown even with one condition) and its own Add filter. Groups are part of the record of the query, not live surfaces. Depth is capped at one level of groups so it never becomes a code editor, and groups follow the top level's conditions. "+ Add group" starts an "any of" group, and removing a group's last condition removes the group. `filterSentence(fields, tree)` reads the whole expression in words, and it's announced as it changes. `value: Filter[]` keeps working as before.
69
+
70
+ <!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
71
+ ## Example
72
+
73
+ `FilterBar` as first used in its preview:
74
+
75
+ ```tsx
76
+ import { FilterBar } from 'altitude-design';
77
+
78
+ <FilterBar aria-label="Filter issues" fields={FB_FIELDS} value={filters} onValueChange={setFilters} match={match} onMatchChange={setMatch} resultCount={count} />
79
+ ```
80
+
81
+ The full preview, as rendered in Storybook and checked in CI (`fb-demo` is the preview page's own layout class, not Altitude's):
82
+
83
+ ```tsx
84
+ import * as React from 'react';
85
+ import { DataTable, FilterBar } from 'altitude-design';
86
+
87
+ const FB_STATUSES = [{ value: 'todo', label: 'To do' }, { value: 'progress', label: 'In progress' }, { value: 'review', label: 'In review' }, { value: 'done', label: 'Done' }];
88
+ const FB_PEOPLE = [{ value: 'mara', label: 'Mara Quist' }, { value: 'ines', label: 'Inés Ortega' }, { value: 'tobi', label: 'Tobi Adeyemi' }, { value: 'wen', label: 'Li Wen' }];
89
+ const FB_FIELDS = [
90
+ { key: 'status', label: 'Status', type: 'enum', options: FB_STATUSES },
91
+ { key: 'assignee', label: 'Assignee', type: 'enum', options: FB_PEOPLE, searchable: true },
92
+ { key: 'updated', label: 'Updated', type: 'date' },
93
+ { key: 'title', label: 'Title', type: 'text' },
94
+ { key: 'points', label: 'Points', type: 'number' },
95
+ ];
96
+ const FB_ROWS = [
97
+ { id: 'a', title: 'Search results page', status: 'review', assignee: 'mara', updated: '2026-10-21', points: 5 },
98
+ { id: 'b', title: 'Billing address form', status: 'progress', assignee: 'ines', updated: '2026-10-12', points: 3 },
99
+ { id: 'c', title: 'Search filters', status: 'todo', assignee: 'tobi', updated: '2026-09-30', points: 8 },
100
+ { id: 'd', title: 'Invoice emails', status: 'done', assignee: 'wen', updated: '2026-10-02', points: 2 },
101
+ { id: 'e', title: 'Onboarding checklist', status: 'review', assignee: 'ines', updated: '2026-10-19', points: 3 },
102
+ ];
103
+ const fbTest = (row, f) => {
104
+ if (f.value === undefined || f.value === '') return true;
105
+ const v = row[f.field];
106
+ switch (f.operator) {
107
+ case 'is': return v === f.value; case 'is not': return v !== f.value;
108
+ case 'contains': return v.toLowerCase().includes(String(f.value).toLowerCase()); case 'does not contain': return !v.toLowerCase().includes(String(f.value).toLowerCase());
109
+ case '=': return v === f.value; case '>': return v > f.value; case '<': return v < f.value;
110
+ case 'before': return v < f.value; case 'after': return v > f.value; case 'between': return v >= f.value.start && v <= f.value.end;
111
+ }
112
+ return true;
113
+ };
114
+ export default function App() {
115
+ const [filters, setFilters] = React.useState([{ id: 'f1', field: 'status', operator: 'is', value: 'review' }]);
116
+ const [match, setMatch] = React.useState('all');
117
+ const matches = row => match === 'all' ? filters.every(f => fbTest(row, f)) : filters.some(f => fbTest(row, f));
118
+ const count = FB_ROWS.filter(matches).length;
119
+ return <div className="fb-demo">
120
+ <FilterBar aria-label="Filter issues" fields={FB_FIELDS} value={filters} onValueChange={setFilters} match={match} onMatchChange={setMatch} resultCount={count} />
121
+ <DataTable aria-label="Issues" rows={FB_ROWS} getRowId={r => r.id} relevance={r => filters.length ? (matches(r) ? 'match' : 'miss') : undefined}
122
+ columns={[
123
+ { key: 'title', header: 'Title' },
124
+ { key: 'status', header: 'Status', cell: r => FB_STATUSES.find(s => s.value === r.status).label },
125
+ { key: 'assignee', header: 'Assignee', cell: r => FB_PEOPLE.find(p => p.value === r.assignee).label },
126
+ { key: 'updated', header: 'Updated' },
127
+ { key: 'points', header: 'Points', align: 'end' },
128
+ ]} />
129
+ </div>;
130
+ }
131
+ ```
132
+ <!-- /example -->