@nadicodeai/ui 12.1.102 → 12.1.103

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 (562) hide show
  1. package/dist/components/accordion.d.ts +38 -12
  2. package/dist/components/accordion.d.ts.map +1 -1
  3. package/dist/components/accordion.js +36 -15
  4. package/dist/components/account-menu.js +2 -2
  5. package/dist/components/action-menu.d.ts +20 -19
  6. package/dist/components/action-menu.d.ts.map +1 -1
  7. package/dist/components/action-menu.js +22 -15
  8. package/dist/components/action-status.d.ts +14 -10
  9. package/dist/components/action-status.d.ts.map +1 -1
  10. package/dist/components/action-status.js +14 -10
  11. package/dist/components/activity-calendar.d.ts +3 -1
  12. package/dist/components/activity-calendar.d.ts.map +1 -1
  13. package/dist/components/activity-calendar.js +16 -2
  14. package/dist/components/activity-feed.d.ts +2 -2
  15. package/dist/components/activity-feed.d.ts.map +1 -1
  16. package/dist/components/activity-feed.js +14 -5
  17. package/dist/components/agent-card.d.ts +11 -5
  18. package/dist/components/agent-card.d.ts.map +1 -1
  19. package/dist/components/agent-card.js +13 -7
  20. package/dist/components/agent-list-row.js +1 -1
  21. package/dist/components/agent-orb-appearance.d.ts +7 -1
  22. package/dist/components/agent-orb-appearance.d.ts.map +1 -1
  23. package/dist/components/agent-orb-appearance.js +26 -0
  24. package/dist/components/agent-profile.d.ts +8 -7
  25. package/dist/components/agent-profile.d.ts.map +1 -1
  26. package/dist/components/agent-profile.js +6 -6
  27. package/dist/components/alert-dialog.d.ts +25 -16
  28. package/dist/components/alert-dialog.d.ts.map +1 -1
  29. package/dist/components/alert-dialog.js +30 -18
  30. package/dist/components/alert.d.ts +1 -1
  31. package/dist/components/avatar.d.ts +83 -17
  32. package/dist/components/avatar.d.ts.map +1 -1
  33. package/dist/components/avatar.js +89 -23
  34. package/dist/components/badge.d.ts +59 -8
  35. package/dist/components/badge.d.ts.map +1 -1
  36. package/dist/components/badge.js +57 -14
  37. package/dist/components/breadcrumb.d.ts +18 -7
  38. package/dist/components/breadcrumb.d.ts.map +1 -1
  39. package/dist/components/breadcrumb.js +37 -13
  40. package/dist/components/button-group.d.ts +12 -15
  41. package/dist/components/button-group.d.ts.map +1 -1
  42. package/dist/components/button-group.js +13 -17
  43. package/dist/components/button.d.ts +126 -23
  44. package/dist/components/button.d.ts.map +1 -1
  45. package/dist/components/button.js +164 -39
  46. package/dist/components/calendar.d.ts +26 -15
  47. package/dist/components/calendar.d.ts.map +1 -1
  48. package/dist/components/calendar.js +60 -46
  49. package/dist/components/card-row.d.ts +13 -5
  50. package/dist/components/card-row.d.ts.map +1 -1
  51. package/dist/components/card-row.js +24 -10
  52. package/dist/components/card.d.ts +6 -12
  53. package/dist/components/card.d.ts.map +1 -1
  54. package/dist/components/card.js +11 -16
  55. package/dist/components/carousel.d.ts +3 -3
  56. package/dist/components/carousel.d.ts.map +1 -1
  57. package/dist/components/carousel.js +7 -7
  58. package/dist/components/catalog-drawer.d.ts +7 -7
  59. package/dist/components/catalog-drawer.d.ts.map +1 -1
  60. package/dist/components/catalog-drawer.js +6 -6
  61. package/dist/components/chart-area-stacked.d.ts +1 -1
  62. package/dist/components/chart-area-stacked.d.ts.map +1 -1
  63. package/dist/components/chart-area-stacked.js +4 -4
  64. package/dist/components/chart-axis.d.ts +14 -1
  65. package/dist/components/chart-axis.d.ts.map +1 -1
  66. package/dist/components/chart-axis.js +11 -1
  67. package/dist/components/chart-bar-horizontal.d.ts +2 -2
  68. package/dist/components/chart-bar-horizontal.d.ts.map +1 -1
  69. package/dist/components/chart-bar-horizontal.js +14 -5
  70. package/dist/components/chart-bar-multiple.d.ts +1 -1
  71. package/dist/components/chart-bar-multiple.d.ts.map +1 -1
  72. package/dist/components/chart-bar-multiple.js +3 -3
  73. package/dist/components/chart-bar-stacked.d.ts +1 -1
  74. package/dist/components/chart-bar-stacked.d.ts.map +1 -1
  75. package/dist/components/chart-bar-stacked.js +3 -3
  76. package/dist/components/chart-card.d.ts +17 -6
  77. package/dist/components/chart-card.d.ts.map +1 -1
  78. package/dist/components/chart-card.js +11 -4
  79. package/dist/components/chart-date-range-control.d.ts +18 -9
  80. package/dist/components/chart-date-range-control.d.ts.map +1 -1
  81. package/dist/components/chart-date-range-control.js +43 -47
  82. package/dist/components/chart-sparkline.d.ts +4 -3
  83. package/dist/components/chart-sparkline.d.ts.map +1 -1
  84. package/dist/components/chart-sparkline.js +6 -5
  85. package/dist/components/check-counts.d.ts +44 -0
  86. package/dist/components/check-counts.d.ts.map +1 -0
  87. package/dist/components/check-counts.js +51 -0
  88. package/dist/components/checkbox.d.ts +39 -11
  89. package/dist/components/checkbox.d.ts.map +1 -1
  90. package/dist/components/checkbox.js +38 -11
  91. package/dist/components/choicebox.d.ts +96 -0
  92. package/dist/components/choicebox.d.ts.map +1 -0
  93. package/dist/components/choicebox.js +113 -0
  94. package/dist/components/clearable-input.d.ts +57 -0
  95. package/dist/components/clearable-input.d.ts.map +1 -0
  96. package/dist/components/clearable-input.js +58 -0
  97. package/dist/components/code-block.d.ts +77 -17
  98. package/dist/components/code-block.d.ts.map +1 -1
  99. package/dist/components/code-block.js +85 -15
  100. package/dist/components/collapsible.d.ts +27 -10
  101. package/dist/components/collapsible.d.ts.map +1 -1
  102. package/dist/components/collapsible.js +24 -9
  103. package/dist/components/collection-filters.d.ts +3 -1
  104. package/dist/components/collection-filters.d.ts.map +1 -1
  105. package/dist/components/collection-filters.js +7 -9
  106. package/dist/components/collection-page.d.ts +31 -10
  107. package/dist/components/collection-page.d.ts.map +1 -1
  108. package/dist/components/collection-page.js +14 -13
  109. package/dist/components/collection-search.d.ts +32 -10
  110. package/dist/components/collection-search.d.ts.map +1 -1
  111. package/dist/components/collection-search.js +24 -13
  112. package/dist/components/collection-sort.d.ts.map +1 -1
  113. package/dist/components/collection-sort.js +2 -2
  114. package/dist/components/collection-toolbar.d.ts +5 -2
  115. package/dist/components/collection-toolbar.d.ts.map +1 -1
  116. package/dist/components/collection-toolbar.js +7 -4
  117. package/dist/components/combobox.d.ts +97 -22
  118. package/dist/components/combobox.d.ts.map +1 -1
  119. package/dist/components/combobox.js +80 -28
  120. package/dist/components/command.d.ts +34 -21
  121. package/dist/components/command.d.ts.map +1 -1
  122. package/dist/components/command.js +33 -25
  123. package/dist/components/confirm-dialog.d.ts +23 -6
  124. package/dist/components/confirm-dialog.d.ts.map +1 -1
  125. package/dist/components/confirm-dialog.js +32 -13
  126. package/dist/components/context-menu.d.ts +14 -10
  127. package/dist/components/context-menu.d.ts.map +1 -1
  128. package/dist/components/context-menu.js +17 -12
  129. package/dist/components/copy-button.d.ts +5 -2
  130. package/dist/components/copy-button.d.ts.map +1 -1
  131. package/dist/components/copy-button.js +16 -8
  132. package/dist/components/data-table.d.ts +120 -54
  133. package/dist/components/data-table.d.ts.map +1 -1
  134. package/dist/components/data-table.js +163 -92
  135. package/dist/components/desktop-shell.d.ts +4 -2
  136. package/dist/components/desktop-shell.d.ts.map +1 -1
  137. package/dist/components/desktop-shell.js +3 -3
  138. package/dist/components/dialog.d.ts +63 -24
  139. package/dist/components/dialog.d.ts.map +1 -1
  140. package/dist/components/dialog.js +52 -31
  141. package/dist/components/disclosure-row.d.ts +1 -2
  142. package/dist/components/disclosure-row.d.ts.map +1 -1
  143. package/dist/components/disclosure-row.js +1 -2
  144. package/dist/components/disclosure.d.ts +1 -1
  145. package/dist/components/disclosure.js +1 -1
  146. package/dist/components/drawer.d.ts +33 -12
  147. package/dist/components/drawer.d.ts.map +1 -1
  148. package/dist/components/drawer.js +47 -35
  149. package/dist/components/dropdown-menu.d.ts +59 -14
  150. package/dist/components/dropdown-menu.d.ts.map +1 -1
  151. package/dist/components/dropdown-menu.js +58 -26
  152. package/dist/components/empty-state.d.ts +35 -12
  153. package/dist/components/empty-state.d.ts.map +1 -1
  154. package/dist/components/empty-state.js +35 -17
  155. package/dist/components/entry-scene.d.ts +4 -4
  156. package/dist/components/entry-scene.d.ts.map +1 -1
  157. package/dist/components/entry-scene.js +2 -2
  158. package/dist/components/field.d.ts +44 -14
  159. package/dist/components/field.d.ts.map +1 -1
  160. package/dist/components/field.js +49 -20
  161. package/dist/components/file-diff.d.ts.map +1 -1
  162. package/dist/components/file-diff.js +3 -4
  163. package/dist/components/form-dialog.d.ts +7 -7
  164. package/dist/components/form-dialog.d.ts.map +1 -1
  165. package/dist/components/form-dialog.js +13 -11
  166. package/dist/components/form-drawer.d.ts +10 -7
  167. package/dist/components/form-drawer.d.ts.map +1 -1
  168. package/dist/components/form-drawer.js +11 -10
  169. package/dist/components/gauge.d.ts +64 -0
  170. package/dist/components/gauge.d.ts.map +1 -0
  171. package/dist/components/gauge.js +73 -0
  172. package/dist/components/hover-card.d.ts +72 -14
  173. package/dist/components/hover-card.d.ts.map +1 -1
  174. package/dist/components/hover-card.js +51 -17
  175. package/dist/components/identity-mark.d.ts +8 -1
  176. package/dist/components/identity-mark.d.ts.map +1 -1
  177. package/dist/components/identity-mark.js +31 -5
  178. package/dist/components/input-group.d.ts +44 -21
  179. package/dist/components/input-group.d.ts.map +1 -1
  180. package/dist/components/input-group.js +74 -27
  181. package/dist/components/input.d.ts +64 -12
  182. package/dist/components/input.d.ts.map +1 -1
  183. package/dist/components/input.js +51 -15
  184. package/dist/components/item.d.ts +37 -15
  185. package/dist/components/item.d.ts.map +1 -1
  186. package/dist/components/item.js +41 -22
  187. package/dist/components/kbd.d.ts +49 -13
  188. package/dist/components/kbd.d.ts.map +1 -1
  189. package/dist/components/kbd.js +115 -16
  190. package/dist/components/kpi-card.d.ts +12 -4
  191. package/dist/components/kpi-card.d.ts.map +1 -1
  192. package/dist/components/kpi-card.js +13 -4
  193. package/dist/components/label.d.ts +17 -10
  194. package/dist/components/label.d.ts.map +1 -1
  195. package/dist/components/label.js +18 -11
  196. package/dist/components/library-shelf.d.ts +6 -6
  197. package/dist/components/library-shelf.d.ts.map +1 -1
  198. package/dist/components/library-shelf.js +5 -5
  199. package/dist/components/load-more-button.d.ts +35 -0
  200. package/dist/components/load-more-button.d.ts.map +1 -0
  201. package/dist/components/load-more-button.js +26 -0
  202. package/dist/components/message-notice.d.ts +8 -13
  203. package/dist/components/message-notice.d.ts.map +1 -1
  204. package/dist/components/message-notice.js +25 -22
  205. package/dist/components/middle-truncate.d.ts +33 -0
  206. package/dist/components/middle-truncate.d.ts.map +1 -0
  207. package/dist/components/middle-truncate.js +72 -0
  208. package/dist/components/multi-select.d.ts +94 -0
  209. package/dist/components/multi-select.d.ts.map +1 -0
  210. package/dist/components/multi-select.js +132 -0
  211. package/dist/components/native-select.d.ts +37 -13
  212. package/dist/components/native-select.d.ts.map +1 -1
  213. package/dist/components/native-select.js +27 -11
  214. package/dist/components/notice-banner.d.ts +17 -10
  215. package/dist/components/notice-banner.d.ts.map +1 -1
  216. package/dist/components/notice-banner.js +12 -11
  217. package/dist/components/numbered-steps.d.ts +10 -4
  218. package/dist/components/numbered-steps.d.ts.map +1 -1
  219. package/dist/components/numbered-steps.js +9 -7
  220. package/dist/components/object-page.d.ts +40 -14
  221. package/dist/components/object-page.d.ts.map +1 -1
  222. package/dist/components/object-page.js +12 -10
  223. package/dist/components/object-workspace.d.ts +29 -10
  224. package/dist/components/object-workspace.d.ts.map +1 -1
  225. package/dist/components/object-workspace.js +18 -8
  226. package/dist/components/overview-page.d.ts +2 -2
  227. package/dist/components/overview-page.d.ts.map +1 -1
  228. package/dist/components/overview-page.js +8 -7
  229. package/dist/components/page-actions.d.ts +28 -8
  230. package/dist/components/page-actions.d.ts.map +1 -1
  231. package/dist/components/page-actions.js +22 -25
  232. package/dist/components/page-header.d.ts +11 -4
  233. package/dist/components/page-header.d.ts.map +1 -1
  234. package/dist/components/page-header.js +10 -5
  235. package/dist/components/pending-label.d.ts +6 -5
  236. package/dist/components/pending-label.d.ts.map +1 -1
  237. package/dist/components/pending-label.js +14 -6
  238. package/dist/components/picture-label.d.ts +2 -2
  239. package/dist/components/picture-label.d.ts.map +1 -1
  240. package/dist/components/picture-label.js +3 -3
  241. package/dist/components/portal-sidebar-frame.d.ts +9 -21
  242. package/dist/components/portal-sidebar-frame.d.ts.map +1 -1
  243. package/dist/components/portal-sidebar-frame.js +13 -27
  244. package/dist/components/progress-checklist.d.ts +46 -0
  245. package/dist/components/progress-checklist.d.ts.map +1 -0
  246. package/dist/components/progress-checklist.js +46 -0
  247. package/dist/components/progress.d.ts +48 -8
  248. package/dist/components/progress.d.ts.map +1 -1
  249. package/dist/components/progress.js +92 -16
  250. package/dist/components/prose.d.ts +19 -0
  251. package/dist/components/prose.d.ts.map +1 -0
  252. package/dist/components/prose.js +29 -0
  253. package/dist/components/radio-group.d.ts +51 -12
  254. package/dist/components/radio-group.d.ts.map +1 -1
  255. package/dist/components/radio-group.js +42 -12
  256. package/dist/components/reading-column.js +1 -1
  257. package/dist/components/record-identity.d.ts +6 -5
  258. package/dist/components/record-identity.d.ts.map +1 -1
  259. package/dist/components/record-identity.js +12 -3
  260. package/dist/components/record-list.d.ts +117 -27
  261. package/dist/components/record-list.d.ts.map +1 -1
  262. package/dist/components/record-list.js +185 -55
  263. package/dist/components/record-time.d.ts +20 -7
  264. package/dist/components/record-time.d.ts.map +1 -1
  265. package/dist/components/record-time.js +38 -19
  266. package/dist/components/resizable.d.ts +7 -5
  267. package/dist/components/resizable.d.ts.map +1 -1
  268. package/dist/components/resizable.js +6 -6
  269. package/dist/components/scope-switcher.d.ts +2 -0
  270. package/dist/components/scope-switcher.d.ts.map +1 -1
  271. package/dist/components/scope-switcher.js +2 -2
  272. package/dist/components/scroll-area.d.ts +47 -11
  273. package/dist/components/scroll-area.d.ts.map +1 -1
  274. package/dist/components/scroll-area.js +72 -14
  275. package/dist/components/segmented-control.d.ts +82 -0
  276. package/dist/components/segmented-control.d.ts.map +1 -0
  277. package/dist/components/segmented-control.js +90 -0
  278. package/dist/components/select.d.ts +55 -15
  279. package/dist/components/select.d.ts.map +1 -1
  280. package/dist/components/select.js +62 -19
  281. package/dist/components/separator.d.ts +12 -5
  282. package/dist/components/separator.d.ts.map +1 -1
  283. package/dist/components/separator.js +11 -6
  284. package/dist/components/settings-facts.d.ts +34 -14
  285. package/dist/components/settings-facts.d.ts.map +1 -1
  286. package/dist/components/settings-facts.js +21 -12
  287. package/dist/components/settings-page.d.ts +4 -4
  288. package/dist/components/settings-page.js +5 -5
  289. package/dist/components/settings-section.d.ts +160 -34
  290. package/dist/components/settings-section.d.ts.map +1 -1
  291. package/dist/components/settings-section.js +92 -39
  292. package/dist/components/sheet.d.ts +47 -16
  293. package/dist/components/sheet.d.ts.map +1 -1
  294. package/dist/components/sheet.js +40 -19
  295. package/dist/components/sidebar-sections.d.ts +16 -12
  296. package/dist/components/sidebar-sections.d.ts.map +1 -1
  297. package/dist/components/sidebar-sections.js +30 -15
  298. package/dist/components/sidebar.d.ts +117 -21
  299. package/dist/components/sidebar.d.ts.map +1 -1
  300. package/dist/components/sidebar.js +180 -55
  301. package/dist/components/skeleton.d.ts +54 -8
  302. package/dist/components/skeleton.d.ts.map +1 -1
  303. package/dist/components/skeleton.js +44 -9
  304. package/dist/components/slider.d.ts +53 -11
  305. package/dist/components/slider.d.ts.map +1 -1
  306. package/dist/components/slider.js +71 -17
  307. package/dist/components/snippet.d.ts +57 -0
  308. package/dist/components/snippet.d.ts.map +1 -0
  309. package/dist/components/snippet.js +60 -0
  310. package/dist/components/sonner.d.ts +13 -14
  311. package/dist/components/sonner.d.ts.map +1 -1
  312. package/dist/components/sonner.js +25 -22
  313. package/dist/components/status-badge.d.ts +37 -11
  314. package/dist/components/status-badge.d.ts.map +1 -1
  315. package/dist/components/status-badge.js +63 -15
  316. package/dist/components/status-dot.d.ts +42 -11
  317. package/dist/components/status-dot.d.ts.map +1 -1
  318. package/dist/components/status-dot.js +40 -11
  319. package/dist/components/switch.d.ts +43 -12
  320. package/dist/components/switch.d.ts.map +1 -1
  321. package/dist/components/switch.js +42 -12
  322. package/dist/components/table-card.d.ts +21 -12
  323. package/dist/components/table-card.d.ts.map +1 -1
  324. package/dist/components/table-card.js +17 -9
  325. package/dist/components/table-layouts.d.ts +40 -8
  326. package/dist/components/table-layouts.d.ts.map +1 -1
  327. package/dist/components/table-layouts.js +52 -8
  328. package/dist/components/table-value.d.ts +4 -3
  329. package/dist/components/table-value.d.ts.map +1 -1
  330. package/dist/components/table-value.js +6 -5
  331. package/dist/components/table.d.ts +62 -25
  332. package/dist/components/table.d.ts.map +1 -1
  333. package/dist/components/table.js +62 -26
  334. package/dist/components/tabs.d.ts +86 -17
  335. package/dist/components/tabs.d.ts.map +1 -1
  336. package/dist/components/tabs.js +93 -22
  337. package/dist/components/text-link.d.ts +4 -1
  338. package/dist/components/text-link.d.ts.map +1 -1
  339. package/dist/components/text-link.js +4 -1
  340. package/dist/components/text-with-copy-button.d.ts +30 -0
  341. package/dist/components/text-with-copy-button.d.ts.map +1 -0
  342. package/dist/components/text-with-copy-button.js +32 -0
  343. package/dist/components/textarea.d.ts +40 -10
  344. package/dist/components/textarea.d.ts.map +1 -1
  345. package/dist/components/textarea.js +50 -11
  346. package/dist/components/theme-mode-switcher.d.ts +18 -1
  347. package/dist/components/theme-mode-switcher.d.ts.map +1 -1
  348. package/dist/components/theme-mode-switcher.js +20 -2
  349. package/dist/components/toggle-group.d.ts +28 -8
  350. package/dist/components/toggle-group.d.ts.map +1 -1
  351. package/dist/components/toggle-group.js +12 -6
  352. package/dist/components/toggle.d.ts +6 -3
  353. package/dist/components/toggle.d.ts.map +1 -1
  354. package/dist/components/toggle.js +10 -7
  355. package/dist/components/tooltip.d.ts +43 -12
  356. package/dist/components/tooltip.d.ts.map +1 -1
  357. package/dist/components/tooltip.js +45 -16
  358. package/dist/components/trend-badge.js +1 -1
  359. package/dist/components/update-card.js +1 -1
  360. package/dist/eslint/index.js +82 -0
  361. package/dist/eslint/rules/badge-is-static-label.js +93 -0
  362. package/dist/eslint/rules/button-icon-slot.js +102 -0
  363. package/dist/eslint/rules/button-link-for-navigation.js +146 -0
  364. package/dist/eslint/rules/button-no-aria-label-with-text.js +75 -0
  365. package/dist/eslint/rules/button-type-roles-only-in-buttons.js +83 -0
  366. package/dist/eslint/rules/choice-control-has-a-name.js +98 -0
  367. package/dist/eslint/rules/choice-group-in-fieldset.js +86 -0
  368. package/dist/eslint/rules/command-shortcut-single-owner.js +118 -0
  369. package/dist/eslint/rules/confirm-dialog-words.js +63 -0
  370. package/dist/eslint/rules/data-table-one-unsized-column.js +96 -0
  371. package/dist/eslint/rules/destructive-action-placement.js +69 -0
  372. package/dist/eslint/rules/disabled-choice-says-why.js +71 -0
  373. package/dist/eslint/rules/ellipsis-opens-a-dialog.js +110 -0
  374. package/dist/eslint/rules/input-group-owners.js +155 -0
  375. package/dist/eslint/rules/kbd-children-no-modifier-text.js +128 -0
  376. package/dist/eslint/rules/literal-text.js +41 -0
  377. package/dist/eslint/rules/middle-truncate-placement.js +69 -0
  378. package/dist/eslint/rules/no-bare-button-label.js +55 -0
  379. package/dist/eslint/rules/no-control-in-read-only-surface.js +106 -0
  380. package/dist/eslint/rules/no-hand-built-busy.js +104 -0
  381. package/dist/eslint/rules/no-hand-built-theme-picker.js +144 -0
  382. package/dist/eslint/rules/no-hand-built-tile.js +41 -0
  383. package/dist/eslint/rules/no-hand-formatted-date.js +87 -0
  384. package/dist/eslint/rules/no-hand-stacked-avatars.js +67 -0
  385. package/dist/eslint/rules/no-nested-collapse.js +60 -0
  386. package/dist/eslint/rules/no-search-in-form.js +47 -0
  387. package/dist/eslint/rules/no-shadcn-appearance-override.js +2 -16
  388. package/dist/eslint/rules/no-shell-prompt-in-snippet.js +74 -0
  389. package/dist/eslint/rules/no-stock-error-words.js +56 -0
  390. package/dist/eslint/rules/no-tooltip-around-labelled-field.js +72 -0
  391. package/dist/eslint/rules/no-weight-class-on-strong-in-type-role.js +57 -0
  392. package/dist/eslint/rules/one-submenu-level.js +43 -0
  393. package/dist/eslint/rules/one-toast-per-flow.js +91 -0
  394. package/dist/eslint/rules/option-count.js +167 -0
  395. package/dist/eslint/rules/page-command-link-not-render.js +43 -0
  396. package/dist/eslint/rules/progress-needs-name.js +57 -0
  397. package/dist/eslint/rules/settings-limit-in-footer.js +94 -0
  398. package/dist/eslint/rules/skeleton-in-region.js +80 -0
  399. package/dist/eslint/rules/sortable-heading-uses-sort.js +79 -0
  400. package/dist/eslint/rules/table-cell-inset-owner.js +107 -0
  401. package/dist/eslint/rules/tabs-selection-is-controlled.js +47 -0
  402. package/dist/eslint/rules/tooltip-only-arrow.js +38 -0
  403. package/dist/eslint/rules/tooltip-trigger-accessible-name.js +114 -0
  404. package/dist/eslint/strict-design-system.js +21 -7
  405. package/dist/index.d.ts +13 -13
  406. package/dist/index.d.ts.map +1 -1
  407. package/dist/index.js +12 -13
  408. package/dist/internal/assistant-ui/attachment.d.ts.map +1 -1
  409. package/dist/internal/assistant-ui/attachment.js +2 -2
  410. package/dist/internal/assistant-ui/reasoning.d.ts +1 -1
  411. package/dist/internal/assistant-ui/reasoning.d.ts.map +1 -1
  412. package/dist/internal/assistant-ui/tool-fallback.js +1 -1
  413. package/dist/internal/assistant-ui/tool-group.d.ts +1 -1
  414. package/dist/internal/assistant-ui/tool-group.d.ts.map +1 -1
  415. package/dist/internal/assistant-ui/tooltip-icon-button.d.ts +2 -3
  416. package/dist/internal/assistant-ui/tooltip-icon-button.d.ts.map +1 -1
  417. package/dist/internal/assistant-ui/tooltip-icon-button.js +1 -1
  418. package/dist/internal/catalog-list.js +2 -2
  419. package/dist/internal/collection-note.d.ts +23 -0
  420. package/dist/internal/collection-note.d.ts.map +1 -0
  421. package/dist/internal/collection-note.js +17 -0
  422. package/dist/internal/collection-rail.d.ts +57 -0
  423. package/dist/internal/collection-rail.d.ts.map +1 -0
  424. package/dist/internal/collection-rail.js +33 -0
  425. package/dist/internal/company-logo.d.ts +18 -0
  426. package/dist/internal/company-logo.d.ts.map +1 -0
  427. package/dist/internal/company-logo.js +14 -0
  428. package/dist/internal/control-name.d.ts +12 -0
  429. package/dist/internal/control-name.d.ts.map +1 -0
  430. package/dist/internal/control-name.js +1 -0
  431. package/dist/internal/diff-line.d.ts +9 -0
  432. package/dist/internal/diff-line.d.ts.map +1 -0
  433. package/dist/internal/diff-line.js +8 -0
  434. package/dist/internal/empty.d.ts +8 -4
  435. package/dist/internal/empty.d.ts.map +1 -1
  436. package/dist/internal/empty.js +14 -8
  437. package/dist/internal/field-frame.d.ts +33 -0
  438. package/dist/internal/field-frame.d.ts.map +1 -0
  439. package/dist/internal/field-frame.js +20 -0
  440. package/dist/internal/final-focus.d.ts +8 -0
  441. package/dist/internal/final-focus.d.ts.map +1 -0
  442. package/dist/internal/final-focus.js +10 -0
  443. package/dist/internal/footer-strip.d.ts.map +1 -1
  444. package/dist/internal/footer-strip.js +3 -1
  445. package/dist/internal/form-submit.d.ts +9 -5
  446. package/dist/internal/form-submit.d.ts.map +1 -1
  447. package/dist/internal/form-submit.js +31 -2
  448. package/dist/internal/input-box.d.ts +26 -0
  449. package/dist/internal/input-box.d.ts.map +1 -0
  450. package/dist/internal/input-box.js +59 -0
  451. package/dist/internal/menu-surface.d.ts +10 -3
  452. package/dist/internal/menu-surface.d.ts.map +1 -1
  453. package/dist/internal/menu-surface.js +10 -3
  454. package/dist/internal/notice.d.ts +25 -0
  455. package/dist/internal/notice.d.ts.map +1 -0
  456. package/dist/internal/notice.js +33 -0
  457. package/dist/internal/overlay-stem.d.ts +5 -0
  458. package/dist/internal/overlay-stem.d.ts.map +1 -0
  459. package/dist/internal/overlay-stem.js +7 -0
  460. package/dist/internal/phone-sheet.d.ts +4 -0
  461. package/dist/internal/phone-sheet.d.ts.map +1 -0
  462. package/dist/internal/phone-sheet.js +2 -0
  463. package/dist/internal/present-content.d.ts +3 -0
  464. package/dist/internal/present-content.d.ts.map +1 -0
  465. package/dist/internal/present-content.js +1 -0
  466. package/dist/internal/record-menu-items.d.ts +4 -3
  467. package/dist/internal/record-menu-items.d.ts.map +1 -1
  468. package/dist/internal/record-menu-items.js +21 -6
  469. package/dist/internal/settings-save.d.ts +9 -2
  470. package/dist/internal/settings-save.d.ts.map +1 -1
  471. package/dist/internal/settings-save.js +9 -6
  472. package/dist/internal/table-compact-line.d.ts +38 -0
  473. package/dist/internal/table-compact-line.d.ts.map +1 -0
  474. package/dist/internal/table-compact-line.js +35 -0
  475. package/dist/internal/title-when-cut.d.ts +8 -0
  476. package/dist/internal/title-when-cut.d.ts.map +1 -0
  477. package/dist/internal/title-when-cut.js +22 -0
  478. package/dist/internal/unsaved-guard.d.ts +18 -0
  479. package/dist/internal/unsaved-guard.d.ts.map +1 -0
  480. package/dist/internal/unsaved-guard.js +31 -0
  481. package/dist/lib/merge-tokens.generated.js +2 -2
  482. package/dist/showcase/foundations.d.ts.map +1 -1
  483. package/dist/showcase/foundations.js +10 -2
  484. package/dist/showcase/specimens/actions.d.ts +2 -0
  485. package/dist/showcase/specimens/actions.d.ts.map +1 -1
  486. package/dist/showcase/specimens/actions.js +30 -5
  487. package/dist/showcase/specimens/charts.d.ts.map +1 -1
  488. package/dist/showcase/specimens/charts.js +22 -13
  489. package/dist/showcase/specimens/choices.d.ts +14 -3
  490. package/dist/showcase/specimens/choices.d.ts.map +1 -1
  491. package/dist/showcase/specimens/choices.js +93 -13
  492. package/dist/showcase/specimens/compositions.d.ts.map +1 -1
  493. package/dist/showcase/specimens/compositions.js +136 -23
  494. package/dist/showcase/specimens/data.d.ts +12 -7
  495. package/dist/showcase/specimens/data.d.ts.map +1 -1
  496. package/dist/showcase/specimens/data.js +139 -35
  497. package/dist/showcase/specimens/feedback.d.ts +2 -0
  498. package/dist/showcase/specimens/feedback.d.ts.map +1 -1
  499. package/dist/showcase/specimens/feedback.js +39 -15
  500. package/dist/showcase/specimens/index.d.ts +20 -12
  501. package/dist/showcase/specimens/index.d.ts.map +1 -1
  502. package/dist/showcase/specimens/inputs.d.ts +4 -2
  503. package/dist/showcase/specimens/inputs.d.ts.map +1 -1
  504. package/dist/showcase/specimens/inputs.js +62 -19
  505. package/dist/showcase/specimens/menus.d.ts +0 -1
  506. package/dist/showcase/specimens/menus.d.ts.map +1 -1
  507. package/dist/showcase/specimens/menus.js +47 -17
  508. package/dist/showcase/specimens/navigation.d.ts +6 -4
  509. package/dist/showcase/specimens/navigation.d.ts.map +1 -1
  510. package/dist/showcase/specimens/navigation.js +61 -32
  511. package/dist/showcase/specimens/other.d.ts +1 -3
  512. package/dist/showcase/specimens/other.d.ts.map +1 -1
  513. package/dist/showcase/specimens/other.js +17 -13
  514. package/dist/showcase/specimens/status.d.ts +4 -1
  515. package/dist/showcase/specimens/status.d.ts.map +1 -1
  516. package/dist/showcase/specimens/status.js +64 -11
  517. package/dist/showcase/usage.generated.d.ts +1 -1
  518. package/dist/showcase/usage.generated.d.ts.map +1 -1
  519. package/dist/showcase/usage.generated.js +632 -544
  520. package/dist/standalone/index.js +1 -1
  521. package/dist/styles/components.css +13 -13
  522. package/dist/styles/globals.css +25 -0
  523. package/package.json +2 -3
  524. package/dist/components/approval-row.d.ts +0 -34
  525. package/dist/components/approval-row.d.ts.map +0 -1
  526. package/dist/components/approval-row.js +0 -27
  527. package/dist/components/channel-view.d.ts +0 -112
  528. package/dist/components/channel-view.d.ts.map +0 -1
  529. package/dist/components/channel-view.js +0 -84
  530. package/dist/components/dashboard-shell.d.ts +0 -51
  531. package/dist/components/dashboard-shell.d.ts.map +0 -1
  532. package/dist/components/dashboard-shell.js +0 -89
  533. package/dist/components/direction.d.ts +0 -2
  534. package/dist/components/direction.d.ts.map +0 -1
  535. package/dist/components/direction.js +0 -1
  536. package/dist/components/expandable-block.d.ts +0 -20
  537. package/dist/components/expandable-block.d.ts.map +0 -1
  538. package/dist/components/expandable-block.js +0 -32
  539. package/dist/components/form.d.ts +0 -42
  540. package/dist/components/form.d.ts.map +0 -1
  541. package/dist/components/form.js +0 -91
  542. package/dist/components/label-system.d.ts +0 -17
  543. package/dist/components/label-system.d.ts.map +0 -1
  544. package/dist/components/label-system.js +0 -21
  545. package/dist/components/memory-item.d.ts +0 -21
  546. package/dist/components/memory-item.d.ts.map +0 -1
  547. package/dist/components/memory-item.js +0 -18
  548. package/dist/components/menubar.d.ts +0 -43
  549. package/dist/components/menubar.d.ts.map +0 -1
  550. package/dist/components/menubar.js +0 -69
  551. package/dist/components/navigation-menu.d.ts +0 -26
  552. package/dist/components/navigation-menu.d.ts.map +0 -1
  553. package/dist/components/navigation-menu.js +0 -45
  554. package/dist/components/pagination.d.ts +0 -33
  555. package/dist/components/pagination.d.ts.map +0 -1
  556. package/dist/components/pagination.js +0 -41
  557. package/dist/components/plugin-slot-boundary.d.ts +0 -42
  558. package/dist/components/plugin-slot-boundary.d.ts.map +0 -1
  559. package/dist/components/plugin-slot-boundary.js +0 -41
  560. package/dist/components/status-row.d.ts +0 -32
  561. package/dist/components/status-row.d.ts.map +0 -1
  562. package/dist/components/status-row.js +0 -35
@@ -21,7 +21,7 @@ export const USAGE = [
21
21
  summary: "What happened to a record, and when: one row per event under its day, newest first.",
22
22
  when: "A record's history: a Customer's, a Partner's or an Installation's Activity tab in the Portal, the company wiki's updates in Nadia. Each answers \"what happened, and when?\".",
23
23
  instead: 'The steps of one Agent run, read in order, are a RunTimeline; a list of records a person finds and acts on is a CollectionPage; a single "last changed" fact is a RecordTime.',
24
- behaviour: "The search, filters and window sit right above the rows, kept in the URL by the product. Days stand newest first, each a title and a muted date over one card of rows; a row is the time of day, the actor's 24 px mark, the sentence with a status badge only for an exception, and one muted line of detail. \"Show older events\" follows the last day; there is never a page count. States, first that holds: failed, loading (still rows in the row's shape), no match with Clear filters, nothing yet in one line, the days. Under 900 px the time moves under the sentence. It draws no card header: the tab is the title.",
24
+ behaviour: "The search, filters and window sit right above the rows, kept in the URL by the product. Days stand newest first, each a title and a muted date over one card of rows; a row is the time of day, the actor's 24 px mark, the sentence with a status badge only for an exception, and one muted line of detail. \"Show older events\" follows the last day as a full-width `LoadMoreButton`, or as a `ButtonLink` when it navigates; there is never a page count. States, first that holds: failed, loading (still rows in the row's shape), no match with Clear filters, nothing yet in one line, the days. Under 900 px the time moves under the sentence. It draws no card header: the tab is the title.",
25
25
  words: 'The sentence names the actor in `<strong>`, the action in the past tense and the object as a link that keeps the text\'s colour: "Marco Rinaldi created the Agent Acme Quotes". Day titles are "Today", "Yesterday", then the weekday; dates are the product\'s own ("14 October"). Nothing yet: "No events yet." and what will appear. No match: "No events match these filters.".',
26
26
  access: "The toolbar is named by `label`; each day's title is a level-3 heading over a list of its events; while loading, one status named by `loadingLabel` stands for the rows; each time is a `<time>` with its exact moment for assistive technology. A system event has no mark, so nothing empty is announced.",
27
27
  },
@@ -32,7 +32,7 @@ export const USAGE = [
32
32
  summary: "The card an Agent's Profile opens with in every product.",
33
33
  when: "The head of an Agent's Profile: who she is, what she does, her state, and her model and reasoning beside them.",
34
34
  instead: "An Agent in a gallery is `AgentCard`; in a list `AgentListRow`. Another object's page is `ObjectPage`.",
35
- behaviour: "Her orb at 64, her name with the pencil, her description and her facts on the left; her model and reasoning on the right; the card's footer saves both sides. The pencil turns her name and description into fields in place. A fact being read draws its place; a missing one keeps its label and a dash.",
35
+ behaviour: "Her orb at 64, her name with the pencil, her description and her facts on the left; her description is read whole, wrapping and never cut; her model and reasoning on the right; the card's footer saves both sides. The pencil turns her name and description into fields in place. A fact being read draws its place; a missing one keeps its label and a dash.",
36
36
  words: "Each product brings its own data, fields and words; the shape, sizes and order are this component's. Her state is a word: *Working*, *Waiting for you*.",
37
37
  access: "Her name is the page's `h1`, kept as the heading while she is edited; the pencil is named (*Edit Nadia*); the orb is decorative beside her name.",
38
38
  },
@@ -40,12 +40,12 @@ export const USAGE = [
40
40
  name: "CatalogDrawer",
41
41
  module: "catalog-drawer",
42
42
  family: "Compositions",
43
- summary: "A catalogue to choose from in the right-side drawer, each entry added from its own row.",
44
- when: "A person picks one or more things from a list too long for a dialog, the page still behind it: add a skill, add an app to a new Agent.",
45
- instead: 'The shelf of everything the company holds, on its own page, is a `LibraryShelf`; a setup in steps is a `FormDrawer` with `job="steps"`; a new record from a few fields is a `FormDialog`.',
46
- behaviour: "The header holds the title, the description and Close; the body holds the search, then the entries in one column under their section titles. It draws one state at a time: the failure with its retry, three still entries while the catalogue loads, no match with a way to clear the search, nothing at all, or the entries. An entry already held shows a muted check and its word in place of Add. The footer's one button closes the drawer.",
47
- words: 'The title names the job: "Add a skill". The search names what it finds: "Search the hub". Each entry\'s button is "Add"; the held word is "Added"; the footer\'s is "Done".',
48
- access: "A modal dialog named by its title; focus starts in the search, Escape and Close close it, and focus returns to the opener. The loading entries are one status named by `loadingLabel`.",
43
+ summary: "A catalogue to choose from in the right-side sheet, each entry added from its own row.",
44
+ when: "Ours: a person picks one or more things from a list too long for a dialog, the page still behind it: add a skill, add an app to a new Agent.",
45
+ instead: '"Don’t use Sheet to confirm destructive actions. The default `modal=false` keeps the page interactive, which weakens severity for a delete or revoke." (https://vercel.com/geist/sheet) Ours: the shelf of everything the company holds, on its own page, is a `LibraryShelf`; a setup in steps is a `FormDrawer` with `job="steps"`; a new record from a few fields is a `FormDialog`.',
46
+ behaviour: '"Outside-click does not auto-close, so always render an explicit close affordance and honor Escape." (https://vercel.com/geist/sheet) Ours: a modal sheet from the right, as `FormDrawer`. The header holds the title, the description and Close; the body holds the search, then the entries in one column under their section titles. It draws one state at a time: the failure with its retry, three still entries while the catalogue loads, no match with a way to clear the search, nothing at all, or the entries. An entry already held shows a muted check and its word in place of Add. The footer\'s one button closes the drawer.',
47
+ words: '"Title is Title Case and names the entity (`Deployment Details`, `Member Profile`), not the page action." (https://vercel.com/geist/sheet) "Cancel literal stays `Cancel`. Acknowledgment-only Modals (after a key reveal, after a one-time-show) use `Done`, never `OK` or `Close`." (https://vercel.com/geist/modal) Ours: the title is sentence case and names the job: "Add a skill". The search names what it finds: "Search the hub". Each entry\'s button is "Add"; the held word is "Added"; the footer\'s is "Done".',
48
+ access: '"Trap focus inside the sheet while it’s open and return focus to the trigger row on close so keyboard users keep their place in the list." (https://vercel.com/geist/sheet) "Provide a visible close button labeled `Close` (or an icon button with `aria-label="Close"`) since clicking outside doesn’t dismiss." (https://vercel.com/geist/sheet) Ours: a modal dialog named by its title; focus starts in the search. The loading entries are one status named by `loadingLabel`.',
49
49
  },
50
50
  {
51
51
  name: "CollectionFilters",
@@ -54,7 +54,7 @@ export const USAGE = [
54
54
  summary: "The filters in a collection's toolbar: one Add filter button, a chip for each facet in force, and the facets most often narrowed to waiting as dashed chips.",
55
55
  when: "A collection or a feed a person narrows by a property its records share: a status, an owner, a kind. It stands in the toolbar after the search and before the sort.",
56
56
  instead: "Finding one record by its name is the search. Changing the order is the sort. Switching between sibling collections is the page's tabs.",
57
- behaviour: "Add filter opens a picker of the facets, then of a facet's values, each searchable; Backspace in an empty search goes back to the facets. A facet in force is a chip that reopens its values, with a button that removes it. A suggested value waits as a dashed chip while its facet is unset and applies in one click. Focus returns to Add filter when a facet is removed. The product keeps each facet's value, in the URL where it has one.",
57
+ behaviour: "Add filter opens a picker of the facets, then of a facet's values, each searchable; Backspace in an empty search goes back to the facets. A facet in force is a chip that reopens its values, with a button that removes it. A suggested value waits as a dashed chip in the muted grey, led by a + and never by the value's own mark, so it never reads as a filter in force, while its facet is unset, and applies in one click. Focus returns to Add filter when a facet is removed. The product keeps each facet's value, in the URL where it has one.",
58
58
  words: 'Every string is the product\'s: `copy` names Add filter, the two searches, the empty result, Back and Remove; each facet names itself, its values and its "All" choice.',
59
59
  access: "Each chip is a group named by its facet; its remove button reads Remove with the facet's name. The pickers are searchable lists named by what they search.",
60
60
  },
@@ -63,11 +63,11 @@ export const USAGE = [
63
63
  module: "collection-page",
64
64
  family: "Compositions",
65
65
  summary: "A page whose job is a list of records: its title with the page's actions on one row, its sibling collections, one toolbar, then one line per record.",
66
- when: "A list of records a person finds one of and acts on: Customers, Agents, Skills, Apps, MCP servers, Mailboxes.",
67
- instead: "Figures compared across records are a DataTable with headings; a list inside another page's section is a RecordList with its heading.",
68
- behaviour: "Nothing sits under the title. The title row holds at most one secondary, the primary, then the three dots, which open only for two commands or more. A primary that is also the first use's action waits until there are records; while a different primary stays, the first use's action is an outline button. One toolbar holds the search, which grows, then the filters, then the sort; it shows while there are records or a search or filter is set, and is glass only where the rows scroll under it. While a new search, filter or sort is read, the toolbar is busy and, after a second, shows the one waiting line, while the last rows stay. The rows, their states and their loading are the RecordList's; no match is offered only while a search or filter is set.",
69
- words: 'Every string is the product\'s. The title names the collection in the plural ("Agents"); the search says what it finds ("Find a skill"); no match says what found nothing and offers to clear it ("Clear search", "Clear filters").',
70
- access: "The title is the page's h1 when the composition draws it. The toolbar is named by `label` and busy while `pending`; the rows are the RecordList's table, named by `label`.",
66
+ when: "“Use `<Entity>` for a row of descriptive content paired with one or two controls (member rows, integration rows, domain rows).” (https://vercel.com/geist/entity) Ours: a list of entity rows a person finds one of, opens and acts on: Customers, Agents, Skills, Apps, MCP servers, Mailboxes, domains, environment variables (a row with an `editor` expands into its form). Its rows are a `RecordList`'s, so a row has a mark, a name over a muted line and at most three facts; anything wider is not this page. “Projects”: Vercel's Projects page stands a column of titled cards (Usage, Alerts, Recent Previews) at the left of its list. (https://vercel.com/dashboard) Ours: a collection whose reader also needs to know what is waiting on them across all its records, such as nadicode's Customers and Partners and a Partner's Customers, takes that column as its `rail`.",
67
+ instead: "“For tabular data with sortable columns and shared row shape, use `Table` instead.” (https://vercel.com/geist/entity) Ours: records compared across columns, read by their headings, are a `DataTable` under the page's own title and toolbar. A list inside another page's section is a `RecordList` with its `heading`.",
68
+ behaviour: "With a `rail`, the toolbar spans the page and under it stand two columns from 1280 px: the rail, 300 px, then the records' card, its top level with the rail's first title and named by the page's title alone, never a second heading, and the records' rows are `RecordList`'s `line` rows; below 1280 px the records come first and the rail follows them. Each rail card has its title above it at 16 px, an optional 44 px muted summary row with its one outline button at its end (a button never stands in a row of its own, so a card with an `action` has a `summary`), then its items: a figure is one 36 px row, its title and its number at the trailing edge; a record is its 24 px mark, a title over a muted line that wraps to a second before it is cut, and a figure or a status at its end, the whole item opening its record; a card with no items draws its `empty` inside it as one 44 px row like an item. While the rail's read is in flight a card's `items` are undefined and it holds its place with three placeholder lines, so the two columns stand from the first frame; a rail whose read `failed` is one card saying so, and the records beside it stay. Under the title sits only the `description`, when the collection's kind needs explaining; otherwise nothing. The title row holds at most one secondary, the primary, then the three dots, which open only for two commands or more. The primary stays in the title row whether the collection is empty or not, filled, as Vercel's Flags keeps Create Flag; a first use repeats it as the filled button in its card. One toolbar holds the search, which grows, then the filters, then the sort; it shows while there are records or a search or filter is set, and is glass only where the rows scroll under it. While a new search, filter or sort is read, the toolbar is busy and, after a second, shows the one waiting line, while the last rows stay. The rows, their states and their loading are the RecordList's; no match is offered only while a search or filter is set.",
69
+ words: '“For multi-facet filters use the plural template `No {Items} Match Your Filters` and suggest widening or clearing.” (https://vercel.com/geist/empty-state) Ours: every string is the product\'s, in sentence case. The title names the collection in the plural ("Agents"); the search says what it finds ("Find a skill"); no match says what found nothing and offers to clear it ("Clear search", "Clear filters").',
70
+ access: "“After an async filter change, wrap the region in `aria-live=\"polite\"` so screen readers announce the new state.” (https://vercel.com/geist/empty-state) Ours: the rows' region is the RecordList's, which holds it. The title is the page's h1 when the composition draws it. The toolbar is named by `label` and busy while `pending`; the rows are the RecordList's table, named by `label`.",
71
71
  },
72
72
  {
73
73
  name: "CollectionToolbar",
@@ -76,7 +76,7 @@ export const USAGE = [
76
76
  summary: "A collection's control bar: its filters, its search and its sort, over its own rows.",
77
77
  when: "The row above a collection's rows that holds its search, filters, sort and status.",
78
78
  instead: "A page action never sits here: it is `PageActions` on the title's row. Finding across the product is `CommandDialog`.",
79
- behaviour: 'Glass on the floating height, held at the top of the scrolling content while the rows pass under it; inside a titled `TableCard`, or with `surface="plain"`, the plain row. A search alone is always the plain row.',
79
+ behaviour: "Glass on the floating height, held at the top of the scrolling content while the rows pass under it; inside a titled `TableCard`, on the title's row at its trailing edge, or with `surface=\"plain\"`, the plain row. A search alone is always the plain row. Below 640 px the search takes the first line and the controls and the actions stand on one line under it, each scrolling sideways when it outgrows its share, never wrapping into a second; each line contains what it scrolls, a chip's hidden label included, so the page never scrolls with it.",
80
80
  words: "The label names the collection's tools: *Customer filters*.",
81
81
  access: '`role="toolbar"` named by `label`.',
82
82
  },
@@ -85,11 +85,11 @@ export const USAGE = [
85
85
  module: "confirm-dialog",
86
86
  family: "Compositions",
87
87
  summary: "A consequence confirmed before it happens, in a blocking alert dialog.",
88
- when: "An action removes, disconnects or deletes something, or otherwise cannot be taken back: remove a mailbox, delete an Agent.",
89
- instead: "An action that can be undone runs at once and offers its undo in a toast; a form of a few fields is a `FormDialog`; a choice from a catalogue is a `CatalogDrawer`.",
90
- behaviour: "The title names the record, the description the consequence; a failed confirm keeps the dialog open with the catalogue's message and its actions under the description. Cancel then the confirm at the footer's trailing edge, the confirm in the error colour when `destructive`. While `loading` both hold, Escape does not close it, and the confirm says its pending word. The backdrop does not dismiss it.",
91
- words: 'The title is a statement naming the record, never a question: "Remove pec@acme.it". The description says what stops and what stays. The confirm repeats the verb and the noun, "Remove mailbox", never just "Delete" or "OK".',
92
- access: '`role="alertdialog"` named by its title and described by its consequence; focus starts on Cancel, Escape cancels, and focus returns to the opener.',
88
+ when: '"Reach for this over `Modal` when the action is destructive and warrants friction: delete, rotate, revoke, disconnect, downgrade, disable a security setting. The typed gate forces deliberate intent." (https://vercel.com/geist/destructive-action-modal) "Use it for reversible destructive actions too, when the consequence is serious enough to warrant a pause — disabling deployment protection, revoking a shared token. Keep the typed gate; drop `irreversibleDescription`." (https://vercel.com/geist/destructive-action-modal) "For high-stakes destructive actions (delete production resource, rotate signing key, downgrade plan), gate the primary button on a typed match of the resource name." (https://vercel.com/geist/modal) Ours: `ConfirmDialog` is Geist\'s DestructiveActionModal: remove a mailbox, delete an Agent, with `verify` as the typed gate.',
89
+ instead: '"Don\'t use for routine confirmations (save draft, discard changes, close without saving). The typed gate reads as melodrama when the stakes are low — use a plain `Modal` instead." (https://vercel.com/geist/destructive-action-modal) "Confirm destructive actions in a Modal. `Drawer` and `Sheet` don’t fully dim the page, so they read as too soft for a delete or revoke." (https://vercel.com/geist/modal) Ours: an action that can be undone runs at once and offers its undo in a toast; a form of a few fields is a `FormDialog`; a choice from a catalogue is a `CatalogDrawer`.',
90
+ behaviour: '"The verification input gets focus on open so the user can start typing immediately. Submit stays disabled until the value matches `verificationPhrase` exactly." (https://vercel.com/geist/destructive-action-modal) "Enter submits only when the gate is open; it\'s a no-op otherwise. Cancel, outside-click, and Escape all dismiss." (https://vercel.com/geist/destructive-action-modal) "`loading` disables both buttons. The caller owns `open` — do not self-dismiss from `onConfirm`. Close the modal after the API settles (success or error), or keep it open on error so the user can retry." (https://vercel.com/geist/destructive-action-modal) "Default focus to `Cancel` on any destructive Modal. Enter must never trigger the destructive action without a typed confirmation." (https://vercel.com/geist/modal) "Allow Escape and outside-click to dismiss non-destructive Modals; gate dismissal on destructive ones with unsaved input." (https://vercel.com/geist/modal) Ours: `title`, `description`, `confirmLabel` and `cancelLabel` are required; `irreversible` and `verify` are optional, and the absence of `irreversible` is what marks the action reversible. Geist\'s `irreversibleDescription` is `irreversible`, `verificationPhrase` and `verificationLabel` are `verify.phrase` and `verify.prompt`, and `error` is the catalogue\'s message. 480 px wide over a scrim. The title and the description, then the body in a 24 px column: the `irreversible` note, the `verify` field, the failure under it. `irreversible` is a flat red note with a red mark and red words, not a band over the footer; besides it the only red is the failure and the enabled destructive confirm. While `loading` the field turns read-only and keeps focus, Escape does not close it, and the confirm says `confirmPendingLabel`, which falls back to the confirm\'s label. A press outside does not close it, so a stray click never discards a typed phrase or an action in flight; Escape and Cancel do. The footer is a bar that stays at the bottom while a tall confirmation scrolls, and a row on a phone too.',
91
+ words: '"`title` is Title Case, `Verb + Noun`, a statement — never a question. `Delete Project`, not `Delete this project?`." (https://vercel.com/geist/destructive-action-modal) "`description` is sentence case, names the consequence, and interpolates the specific resource when relevant. `<b>my-project</b> and all its deployments will be permanently deleted.` reads stronger than `This project and all its deployments will be deleted.`." (https://vercel.com/geist/destructive-action-modal) "`confirmLabel` matches the title 1:1. Never generic (`Confirm`, `OK`, `Continue`), never a bare verb (`Delete`)." (https://vercel.com/geist/destructive-action-modal) "`cancelLabel` defaults to `Cancel`. Override when Cancel is ambiguous, such as `Keep Turn Running` when the primary action is itself a cancellation." (https://vercel.com/geist/destructive-action-modal) "`verificationPhrase`: for entity deletes, type the **resource name itself** (`my-project`) and pair with `verificationLabel="project name"` so the prompt reads `To confirm, type the project name "my-project"`. That\'s the canonical signal that the user knows which thing they\'re acting on. Fall back to a lowercase verb phrase (`disable vercel authentication`) only when there\'s no entity to name." (https://vercel.com/geist/destructive-action-modal) "`irreversibleDescription` names the specific action and resource and ends with `cannot be undone.` — `Deleting my-project cannot be undone.` rather than the generic `This cannot be undone.`. Omit the prop entirely for reversible actions; the prop\'s absence is the signal, not a falsy value." (https://vercel.com/geist/destructive-action-modal) "`error` surfaces the API failure verbatim in Vercel voice: `Couldn\'t save settings. Try again.`. Never dump a raw error object." (https://vercel.com/geist/destructive-action-modal) "Pair the post-confirm success toast verb 1:1 with the primary button: `Delete Project` button, `Project deleted` toast. Never `Project removed`." (https://vercel.com/geist/destructive-action-modal) "Close irreversible bodies with `This cannot be undone.`; close cascade-only bodies with `Some effects cannot be undone.`. Don’t claim full irreversibility for a partial cascade." (https://vercel.com/geist/modal) Ours: the title is sentence case and names the record, "Remove pec@acme.it", and the confirm repeats the verb and the noun, "Remove mailbox", so the two share a verb and not every word; its success toast uses the same verb ("Mailbox removed"). The description names the record in medium weight.',
92
+ access: '"The verification input is associated with its prompt via `aria-labelledby` + `htmlFor`. Screen readers announce the full prompt (`To confirm, type "delete my-project"`) on focus." (https://vercel.com/geist/destructive-action-modal) "The Warning icon in the irreversibility band is `aria-hidden`; the sentence carries the meaning so nothing is announced twice." (https://vercel.com/geist/destructive-action-modal) "Focus stays inside the modal across an `error` transition so the user can retry without losing context. After success, return focus to the trigger." (https://vercel.com/geist/destructive-action-modal) "Set `aria-labelledby` to the `Modal.Title` id so screen readers announce the title on open." (https://vercel.com/geist/modal) "Keep the Cancel button literally `Cancel` so screen-reader users hear a stable dismissal label across destructive flows." (https://vercel.com/geist/modal) Ours: `role="alertdialog"` named by its title and described by its consequence; the note is `role="note"` with its icon hidden. Focus starts on Cancel, or in the field with `verify`.',
93
93
  },
94
94
  {
95
95
  name: "EntryScene",
@@ -98,7 +98,7 @@ export const USAGE = [
98
98
  summary: "Nadia's entry screen, contained: the approved picture of the surface behind, the wordmark at the upper left, Nadia's ribbon and her words centred as one group. The screen is black in every theme.",
99
99
  when: "Nadia's installer and the desktop's sign-in, start, update and failure screens, which paint the same screen; a page that shows that screen beside other things.",
100
100
  instead: "The Portal's entry pages are its funnel frame with the shared entry header; Alveare Server's entry is the HiveScene; a wait inside the product is a WaitingLine.",
101
- behaviour: "It holds no state and decides nothing: which view is open, the words and when the screen ends belong to whoever mounts it. It fills a box of the window's own shape (`aspectRatio`), so the picture's figure stands on the window's floor (`pictureFeet`). A failure quiets the picture and never removes it. Its children stand in order: the message, the actions, the progress line, the stage list.",
101
+ behaviour: "It holds no state and decides nothing: which view is open, the words and when the screen ends belong to whoever mounts it. Required: the picture, the wordmark and the heading, because every entry screen has all three; a screen that leaves one out does not compile. It fills a box of the window's own shape (`aspectRatio`), so the picture's figure stands on the window's floor (`pictureFeet`). A failure quiets the picture and never removes it. Its children stand in order: the message, the actions, the progress line, the stage list.",
102
102
  words: 'The heading names the surface and is never seen. Instructions and status are one plain sentence that changes only when the real stage changes ("Loading your tools").',
103
103
  access: "One level-1 heading for the screen, read but not shown; a message that reports work in progress is a status.",
104
104
  },
@@ -106,45 +106,45 @@ export const USAGE = [
106
106
  name: "FormDialog",
107
107
  module: "form-dialog",
108
108
  family: "Compositions",
109
- summary: "A new record, or one or two values from a command, in a centred dialog.",
110
- when: "A person adds a record through a few fields, or changes one or two values a command asks for: new customer, add a mailbox, add an MCP server.",
111
- instead: 'A record already in a list edits in its row with `RowEditForm`; a choice from a catalogue is a `CatalogDrawer`; a setup in steps with a wait is a `FormDrawer` with `job="steps"`; a consequence is confirmed in a `ConfirmDialog`.',
112
- behaviour: "Centred over a scrim without blur: the title and its help, the fields in a body that scrolls, then the footer with Cancel and the primary at its trailing edge. A refusal or a failed save sits above the footer and the fields keep what was typed. While its save runs it holds open, Cancel and the primary hold, and the waiting line runs under them.",
113
- words: 'The title names the job and its object: "New customer", "Add a mailbox". The primary repeats it: "Create customer", "Add mailbox", never "OK" or "Submit".',
114
- access: "A modal dialog named by its title; focus starts in its first field or `initialFocus`, Escape and Close close it unless the save runs, and focus returns to the opener or `finalFocus`.",
109
+ summary: "A new record, or one or two values from a command, in a modal dialog.",
110
+ when: '"Use Modal when a decision must block the rest of the page. For persistent associated context where the underlying page stays readable, use `Sheet` on desktop or `Drawer` on mobile." (https://vercel.com/geist/modal) Ours: a person adds a record through a few fields, or changes one or two values a command asks for: new customer, add a mailbox, add an MCP server.',
111
+ instead: '"Skip Modal for routine create flows that have a dedicated page; route to the page instead." (https://vercel.com/geist/modal) "Confirm destructive actions in a Modal. `Drawer` and `Sheet` don’t fully dim the page, so they read as too soft for a delete or revoke." (https://vercel.com/geist/modal) Ours: a record already in a list edits in its row with `RowEditForm`; a choice from a catalogue is a `CatalogDrawer`; a setup in steps with a wait is a `FormDrawer` with `job="steps"`; a consequence is confirmed in a `ConfirmDialog`.',
112
+ behaviour: '"Allow Escape and outside-click to dismiss non-destructive Modals; gate dismissal on destructive ones with unsaved input." (https://vercel.com/geist/modal) Ours: centred over a scrim without blur, 540 px wide, a bottom sheet on a phone: the title and its help, the fields in a 12 px column, then the footer, which stays at the bottom while a tall form scrolls, with Cancel at the leading edge and the primary at the trailing edge, in one row also on a phone. A refusal or a failed save sits above the footer and the fields keep what was typed. While its save runs it holds open, Cancel and the primary hold, and the waiting line runs under them. With `unsaved.dirty`, Escape and a press outside are held back and `unsaved.onDismissBlocked` runs so the caller can ask; Close and Cancel always close.',
113
+ words: '"`Modal.Title` is a Title Case statement, never a question. `Transfer Project` is correct; `Transfer Project?` is wrong." (https://vercel.com/geist/modal) "Primary button is `Verb + Noun` and matches the title verb (`Transfer Project` title pairs with `Transfer Project` button). Never `Confirm`, `OK`, or a bare verb on a destructive primary." (https://vercel.com/geist/modal) "Cancel literal stays `Cancel`. Acknowledgment-only Modals (after a key reveal, after a one-time-show) use `Done`, never `OK` or `Close`." (https://vercel.com/geist/modal) Ours: the title and the primary are sentence case, where Geist writes Title Case: the title names the job and its object, "New customer", "Add a mailbox"; the primary repeats it, "Create customer", "Add mailbox".',
114
+ access: '"Set `aria-labelledby` to the `Modal.Title` id so screen readers announce the title on open." (https://vercel.com/geist/modal) "Trap focus inside the Modal while it’s open and return focus to the trigger after close. Restore body scroll on the same tick the Modal unmounts." (https://vercel.com/geist/modal) "After an error inside the Modal, keep focus inside so the user can retry; after success, return focus to the trigger." (https://vercel.com/geist/modal) Ours: a modal dialog with a Close named by `closeLabel`; focus starts in its first field or `initialFocus` and returns to the opener or `finalFocus`. Escape and Close close it unless the save runs.',
115
115
  },
116
116
  {
117
117
  name: "FormDrawer",
118
118
  module: "form-drawer",
119
119
  family: "Compositions",
120
- summary: "The right-side drawer for a catalogue or a setup in steps, at one width.",
121
- when: "Choosing from a catalogue too long for a dialog (through `CatalogDrawer`), or setting something up in several steps with a wait: connect a messaging channel, Telegram's code.",
122
- instead: "A new record from a few fields is a `FormDialog`; a record already in a list edits in its row with `RowEditForm`; a fact on the record's own page changes in place in its section; a consequence is confirmed in a `ConfirmDialog`.",
123
- behaviour: 'Opens from the right edge at one width, 560 px (the full width on a compact screen), with the page still behind it; 12 px corners on the edges not flush with the window. The header holds the step ("Step 2 of 3") over the title, the description and Close; the body scrolls between the header and a recessed footer that stays, Cancel then the primary at its trailing edge. Cancel holds while a save runs.',
124
- words: 'The title names the job and its object: "Connect Telegram", "Add a skill". The step reads "Step N of M". The primary says what it does: "Connect", "Continue", never "OK".',
125
- access: "A modal dialog named by its title; focus moves inside on open and returns to the opener on close, or to `finalFocus` when the opener is gone. Escape and Close close it; the step is read before the title.",
120
+ summary: "The right-side sheet for a catalogue or a setup in steps, at one width.",
121
+ when: "Ours: choosing from a catalogue too long for a dialog (through `CatalogDrawer`), or setting something up in several steps with a wait: connect a messaging channel, Telegram's code.",
122
+ instead: "\"Don’t use Sheet to confirm destructive actions. The default `modal=false` keeps the page interactive, which weakens severity for a delete or revoke.\" (https://vercel.com/geist/sheet) Ours: a new record from a few fields is a `FormDialog`; a record already in a list edits in its row with `RowEditForm`; a fact on the record's own page changes in place in its section; a consequence is confirmed in a `ConfirmDialog`; a phone's short job is a `Drawer`.",
123
+ behaviour: '"Sheet defaults to `modal=false` so toasts and other high-z elements stay reachable. Keep that default unless the sheet owns the screen." (https://vercel.com/geist/sheet) "Pick `side` from the trigger location: a row inspector slides from `right`, a global filter from `left`. Don’t change sides mid-session." (https://vercel.com/geist/sheet) "Outside-click does not auto-close, so always render an explicit close affordance and honor Escape." (https://vercel.com/geist/sheet) Ours: a modal `Sheet` from the right edge over the scrim, because the drawer owns the screen, at the Sheet\'s one width that no caller sets. The header holds the step ("Step 2 of 3") over the title, the description and Close; the body scrolls between the header and a footer that stays, with no fill or rule of its own, Cancel then the primary at its trailing edge. A press outside does not close it; Escape does, unless `unsaved.dirty` holds it back. Cancel holds while a save runs.',
124
+ words: '"Title is Title Case and names the entity (`Deployment Details`, `Member Profile`), not the page action." (https://vercel.com/geist/sheet) "Body is read-mostly: sentence case prose with Title Case sub-headings. Action buttons are optional; when present, follow `Verb + Noun`." (https://vercel.com/geist/sheet) Ours: the title is sentence case and names the job and its object: "Connect Telegram", "Add a skill". The step reads "Step N of M". The primary says what it does: "Connect", "Continue", never "OK".',
125
+ access: '"Trap focus inside the sheet while it’s open and return focus to the trigger row on close so keyboard users keep their place in the list." (https://vercel.com/geist/sheet) "Provide a visible close button labeled `Close` (or an icon button with `aria-label="Close"`) since clicking outside doesn’t dismiss." (https://vercel.com/geist/sheet) "Announce the sheet with `aria-labelledby` pointing at the title; pair it with `aria-describedby` only when the body is short and load-bearing." (https://vercel.com/geist/sheet) Ours: a modal dialog named by its title; focus returns to the opener on close, or to `finalFocus` when the opener is gone. The step is read before the title.',
126
126
  },
127
127
  {
128
128
  name: "LibraryShelf",
129
129
  module: "library-shelf",
130
130
  family: "Compositions",
131
131
  summary: "A Library shelf: the one line saying what it holds, the search, then the reusable things a person adds, as two-column entries or Agent template cards.",
132
- when: "A page of reusable things a person adds to an Agent: Skills, Plugins, Agent templates.",
133
- instead: "Records a person already has and acts on are a CollectionPage; choosing from a catalogue without leaving the page is a CatalogDrawer.",
134
- behaviour: "Shows, the first that holds: the failure; still entries while the shelf loads; noMatch while the search finds nothing; empty when the shelf holds nothing; then each section's title over its entries, two to a row from md, or the templates three to a row. Each entry has its kind's tile, its name (the link to its page), one line, one fact, one Add control or the word saying it is already added, then its menu. A failed add puts its message in the entry's line and its next steps beside its controls. The search shows unless the shelf is empty.",
135
- words: "The product words every string: the definition (one sentence on what the shelf holds), the search's label (\"Search skills\"), the loading label, each entry's name, line, Add label and added word, the empty, noMatch and failed messages.",
132
+ when: "A gallery of reusable things a person adds, as Vercel's Templates gallery: the Portal's Agent templates, Nadia's New Agent gallery.",
133
+ instead: "The Portal's Skills and Plugins are entity rows a person finds, opens and adds, a CollectionPage; records a person already has and acts on are a CollectionPage too; choosing from a catalogue without leaving the page is a CatalogDrawer.",
134
+ behaviour: "The definition line only where the page has no title row to stand under (a titled page follows the title with the search, as every collection does). Shows, the first that holds: the failure; still entries while the shelf loads; noMatch while the search finds nothing; empty when the shelf holds nothing; then each section's title over its entries, two to a row from md, or the templates three to a row. Each entry has its kind's tile, its name (the link to its page), one line, one fact, one Add control or the word saying it is already added, then its menu. A failed add puts its message in the entry's line and its next steps beside its controls. The search shows unless the shelf is empty.",
135
+ words: "The product words every string: the optional definition (one sentence on what the shelf holds), the search's label (\"Search skills\"), the loading label, each entry's name, line, Add label and added word, the empty, noMatch and failed messages.",
136
136
  access: "Each section is a region named by its h2; entries are list items; loading is a status named by loadingLabel and aria-busy; a failed add is announced as an alert; each entry's menu is named by its label.",
137
137
  },
138
138
  {
139
139
  name: "ObjectPage",
140
140
  module: "object-page",
141
141
  family: "Compositions",
142
- summary: "One object's page: its mark, name, one line and key facts on the page ground, its commands on the name's row, then its sections as cards.",
142
+ summary: "One object's page: the object card with its name, commands, facts, rows that open and one link elsewhere, then its sections as cards.",
143
143
  when: "One thing a person opened from a list and now reads or acts on: a customer, a partner, an Installation, a Library entry.",
144
- instead: "A list of records is a CollectionPage; a page of settings only is a SettingsPage; an Agent opens on its AgentProfileCard, the documented exception.",
145
- behaviour: "The header sits on the page, never in a card: the mark at 64, the name, one line, the facts in one row, and at the trailing edge one secondary action then the three dots; below `md` the actions wrap under the name. The sections follow, 24 apart, each drawing its own states.",
146
- words: 'The name as the object is called everywhere else; the line says what it is and whose ("Customer of Partner Alpha · since 3 August 2026"); a fact\'s label is a noun ("Plan"), its value the figure; the menu\'s trigger reads "More for {name}".',
147
- access: "With `headingProps` the name is the page's h1; without them it is text, for a product whose chrome already titles the page. The facts are a description list; the mark is decorative beside the name.",
144
+ instead: '"Use Context Card to reveal entity metadata on hover or focus: a user, a deployment, a project, an API key." (https://vercel.com/geist/context-card) Ours: that hover is a `HoverCard`, never this page. A list of records is a CollectionPage; a page of settings only is a SettingsPage, whose cards are SettingsSections; an Agent opens on its AgentProfileCard, the documented exception. A card of values a person saves is a setting card, never this one.',
145
+ behaviour: '"For a static key/value metadata block on a detail page, use `Description`." (https://vercel.com/geist/entity) "The right column holds at most one or two controls. If the row needs more, move secondary actions into a `Dots Menu`." (https://vercel.com/geist/entity) "Lead the left column with a scannable identifier: an `Avatar` or icon, a Title Case label, then sentence-case secondary metadata (`Member since Mar 14, 2026`)." (https://vercel.com/geist/entity) "Don’t put interactive controls in the title slot. Buttons, menus, and links belong in the content (`<dd>`) or in the parent layout, not the label (`<dt>`)." (https://vercel.com/geist/description) Ours: as Vercel\'s deployment page (Deployment Details, then its steps), the object card is the first card of the page: a header bar with the name at the start and the object\'s commands at the end (one secondary action, then the three dots, on the name\'s row while the name keeps 128 px, as on a phone, and under it otherwise), then a body of the mark at 64 centred in a tile on the muted ground (300 px wide at 16:10 from `md`, 128 px high and full width below) beside the optional line and the facts in a grid of four columns from `lg` and two below (at least one fact is required: an object is never shown without what it is), each a muted label over its value in body text, then an optional footer with a hint at the start and one link at the end. Optional `entries` follow in a card of their own 12 px under it: rows that open one by one, each with its chevron first (it turns down when open), its title, and its summary at the trailing edge, as Vercel\'s Build Logs and Deployment Checks rows. The footer takes a link only, so no Save or other command of the object fits in it; the object\'s changes live in its sections, each a setting card with its own footer. The sections follow, 24 apart, each drawing its own states. The card is neutral: colour sits in a fact\'s value or an entry\'s summary (a status), never in the header, the footer or the card.',
146
+ words: '"Keep right-column buttons Verb + Noun (`Remove Member`, `Resend Invite`). Bare verbs like `Remove` or `Confirm` lose context once the row scrolls offscreen." (https://vercel.com/geist/entity) "Title slot is Title Case noun (`Last Deployed`, `Build Duration`); content is sentence case unless the value is a literal identifier, ID, or timestamp that should be preserved verbatim." (https://vercel.com/geist/description) Ours: sentence case throughout. The name as the object is called everywhere else; the line says what it is and whose ("Customer of Partner Alpha · since 3 August 2026"); a fact\'s label is a noun ("Plan"), its value the figure; an entry\'s title is a noun phrase; the hint is one sentence ("To update it, push to the main branch."); the link names where it goes ("Deployments"); the menu\'s trigger reads "More for {name}".',
147
+ access: '"Geist renders `<dl>`/`<dt>`/`<dd>` so screen readers announce each key/value pair as a definition." (https://vercel.com/geist/description) "Don’t wrap the component in extra paragraphs that break the list semantics." (https://vercel.com/geist/description) Ours: with `headingProps` the name is the page\'s h1; without them it is text, for a product whose chrome already titles the page. The mark is decorative beside the facts. Each entry is a button with `aria-expanded` that controls its panel.',
148
148
  },
149
149
  {
150
150
  name: "ObjectSection",
@@ -153,8 +153,8 @@ export const USAGE = [
153
153
  summary: "A section of an object's page: a card that owns one decision about the object.",
154
154
  when: "A part of an ObjectPage a person reads or changes on its own: a customer's Company details, an Agent's Instructions, a Library entry's source.",
155
155
  instead: "Values a person edits and saves together are a SettingsSection; a list of the object's records is a RecordList with its heading; a passing condition is a MessageNotice.",
156
- behaviour: "A resting card: its title and an optional line, at the trailing edge `meta` (muted) then `action` as an outline button, then its body. No icon in the header.",
157
- words: 'The title names what the section holds ("Company", "Instructions"); the action is a verb on it ("Edit"); `meta` is a short count or time ("2 of 5 Agents").',
156
+ behaviour: "A resting card: its title and the one-sentence description directly under it, both always given, at the trailing edge `meta` (muted) then `action` as an outline button, then its body. No icon in the header. Neutral throughout.",
157
+ words: 'The title names what the section holds ("Company", "Instructions"); the description says in one sentence what it holds or lets a person change; the action is a verb on it ("Edit"); `meta` is a short count or time ("2 of 5 Agents").',
158
158
  access: "The title is a heading one level under the page (h2), deeper only under another heading through `headingLevel`.",
159
159
  },
160
160
  {
@@ -162,9 +162,9 @@ export const USAGE = [
162
162
  module: "overview-page",
163
163
  family: "Compositions",
164
164
  summary: 'The page that answers "what needs attention?": is anything wrong, and where.',
165
- when: "The landing page of an area a person checks: the Portal's Dashboard, Analytics, Statements, System health, an Agent's Usage.",
165
+ when: "\"For in-product cards and dashboard tiles, use `Card`; Book is too decorative for repeated UI rows.\" (https://vercel.com/geist/book) Ours: the landing page of an area a person checks: the Portal's Dashboard, Analytics, Statements, System health, an Agent's Usage.",
166
166
  instead: "A list of records a person finds one of is a `CollectionPage`; one record is an `ObjectPage`. Never a stack of previews of every section: each exception links to its owner instead.",
167
- behaviour: "The reading period on the title row, then the figures in one row of equal `KpiCard`s (four columns from md, fewer when fewer, one column below), then the chart cards, then the exceptions, 16 px apart. It has no states of its own: each card and the exceptions' `RecordList` draw theirs.",
167
+ behaviour: '"Show a Skeleton when async data fills a known layout: table rows, card grids, profile blocks, sidebars." (https://vercel.com/geist/skeleton) "Use the Error component as a block surface when a section or page-level resource failed to load: a panel, a dashboard card, a route boundary." (https://vercel.com/geist/error) Ours: the reading period on the title row, then the figures in one row of equal `KpiCard`s (as many columns as figures up to five, five reading two by two from md, three over two from lg and one row from xl at 1280 px; six as three over three, more in rows of four; one column below md), then the chart cards, then the exceptions. Cards stand 24 px apart with no heading between them (the figures 16 px apart in their row), and the title row has no title: the path names the page. Its cards are dashboard cards: a figure or a chart that reads, drills into its owner and holds no setting; neither an object card nor a setting card appears here. Colour sits in a figure\'s change and tone, never in the page. It has no states of its own: each card and the exceptions\' `RecordList` draw theirs.',
168
168
  words: 'The product words every figure, chart and exception. A figure\'s label names its measure ("Active Agents", "Spend"), the chart\'s title names its reading ("Usage"), and the exceptions\' heading says what they are ("Needs attention").',
169
169
  access: "Each chart card is a named section at the page's heading level, and the exceptions carry their own heading; figures are text, never colour alone.",
170
170
  },
@@ -175,20 +175,20 @@ export const USAGE = [
175
175
  summary: "The top of a product page: its title, and the page's actions on the title's row.",
176
176
  when: "The top of every collection and working page: the title at heading size, `PageActions` at the trailing edge of its row.",
177
177
  instead: "An object's own header is `ObjectPage`'s. The path above it is the top bar's `Breadcrumb`; filters, search and sort sit in the toolbar below.",
178
- behaviour: "Nothing explains the page under its title. The actions arrive through the `PageActions` slot, so the page draws its commands once.",
179
- words: "The title is the page's name as the sidebar writes it: *Customers*, *Agents*.",
178
+ behaviour: "The title is always there and the actions arrive through the `PageActions` slot, so the page draws its commands once. On a phone the title keeps its row with one action, as Vercel's does; it gives way, and the actions wrap under it, only when they leave it less than 128 px. Under the title only the `description` may sit: one muted line saying what the page's records are for, for a page whose kind needs explaining. It ends in a link to learn more when there is one, and never counts or introduces anything else. The description is neutral; no hue sits in the header.",
179
+ words: "The title is the page's name as the sidebar writes it: *Customers*, *Agents*. The description is one sentence in the product's words, without a full stop when it ends in a link.",
180
180
  access: "The title is the page's `h1`; DOM order is visual order.",
181
181
  },
182
182
  {
183
183
  name: "RecordList",
184
184
  module: "record-list",
185
185
  family: "Compositions",
186
- summary: "One line per record, on one resting card: its identity mark and name over what it is, a few comparable facts, a status only for the exception, when it happened, the row's one command, and its three dots last.",
187
- when: "A list of records inside another page: an Overview's exceptions, an object page's \"Needs attention\", any section that lists records a person opens or acts on. A page whose whole job is the list is a `CollectionPage`, which draws this.",
188
- instead: "Figures compared across records, read by their column headings, are a `DataTable`. Events in time are an `ActivityFeed`. Reusable things a person adds from a catalogue are a `LibraryShelf`.",
189
- behaviour: "Each row reads left to right at fixed measures: the mark and name (at most a third of the row), facts on one line, the status in its 150 px slot only when the record is the exception, the time right-aligned in tabular figures with the exact moment on hover, the row command at most 192 px wide with its label cut short, then the three dots, or a muted chevron when the row only opens. On a card too narrow for them (768 px, or 1024 px for seven columns or more or a row command), the facts, the status and the time move under the record's line, leaving out a fact or status the record does not have, and the name and its commands keep theirs. A menu of one command that is not destructive, with no row command, is that command's outline button. With `open` the whole row is one target. A failed or refused action of the record replaces its detail line, with its next steps beside its commands; the record being edited opens its form under its own line. While the records load, three still skeleton rows hold the place; then, first that holds: the failed read, no match, the empty state, the rows. A list with no records and no empty state draws nothing.",
190
- words: 'Every string is the product\'s: `label` names the list, a time\'s `phrase` writes its sentence ("Updated {when}"), `missing` is what a screen reader hears for a record with no time, the menu\'s `label` reads "More for {record}". A status is the exception\'s word ("Needs attention"), never "Active" on every row.',
191
- access: "A table named by `label`, its column headings read by assistive technology only. The row link carries `open.label`; the three dots carry the menu's `label`, and the menu's first line names the record. The loading rows are a busy status named by `loadingLabel`.",
186
+ summary: "Entity rows: objects a person opens or acts on, one line each on one resting card: the identity mark and name over what it is, a few facts, a status only for the exception, when it happened, at most two row commands, and the three dots last.",
187
+ when: "“Use `<Entity>` for a row of descriptive content paired with one or two controls (member rows, integration rows, domain rows).” (https://vercel.com/geist/entity) Ours: the records are things a person opens or acts on and that name themselves: projects, deployments, domains, members, integrations, Agents, Skills, mailboxes. A list inside another page: an Overview's exceptions, an object page's \"Needs attention\". A page whose whole job is the list is a `CollectionPage`, which draws this. Values edited in place (a domain, an environment variable) are the same rows with an `editor`: the row expands into its form. Of the three kinds of list, entity rows and rows edited in place are this; records compared across columns are a `DataTable`.",
188
+ instead: "“For tabular data with sortable columns and shared row shape, use `Table` instead.” (https://vercel.com/geist/entity) “For a static key/value metadata block on a detail page, use `Description`.” (https://vercel.com/geist/entity) Ours: Geist's `Table` is a `DataTable`: this list has no visible headings and at most three facts, so a fourth fact is a table. Its `Description` is `ObjectFacts`. Events in time are an `ActivityFeed`. Reusable things a person adds from a catalogue are a `LibraryShelf`. A descriptive row with prose is an `Item`.",
189
+ behaviour: "One bordered card, a 1 px rule between records and none after the last. A `checklist` puts the record's `ProgressChecklist` ring just before its commands, where Vercel ends a project's row with its Production Checklist; the ring opens its own drawer, never the row. Every row has its 32 px mark, its name and, under it, one muted line; `name` is required and so are the mark and the line, and a record with nothing to say passes the dash. Give every column a `size` in pixels; the name takes the rest. With `layout=\"line\"`, as on Vercel's Projects page, the facts and the status share one muted line, joined by a middle dot, in a column as wide as the name's, and the row stays one line (mark, name over its line, that summary, the time, the ring, the three dots) on a card from 448 px, and on a narrower card the summary and the time move under the name as one muted line joined by a middle dot, in the same row, their labels read by assistive technology only. Rows of `columns` follow the same rule once the card is narrower than their columns: the facts, the status and the time leave the row for that one line, ending in an ellipsis, never a labelled list. `facts` takes at most three. A menu of one command that is not destructive, with no row command, is that command's button, except in a `line` row, which keeps it in the three dots as Vercel's project row does; without a `commandsSize` the column is as wide as the widest such button, so its label is never cut. Status is only for the exception, empty for the usual record, and its column stands only while a record in the list shows one, so the name and its line keep the room; the commands column likewise stands only while a record has a command, a menu or a message, or the rows open. A `line` row's summary joins only the facts the record has, so it never ends in a dot. A failed or refused action of the record replaces its detail line, with its next steps beside its commands, or under the message on a card too narrow for the row's columns. With `open` the whole row is one target and takes the `accent` hover; a row that opens nothing never reacts to hover. With `editor` the record's row opens its form under its line. `empty` is required: `null` is the choice to draw nothing for a list with no records, and any other value is the state it draws. A note about the whole list is `footer.note`: one muted line, an optional *Learn more* after it at the left and one secondary action at the right, in a strip of its own under the count and the paging, as Vercel ends its deployments list (\"Deployment retention is enabled. Learn more\", *View recently deleted*). Colour is neutral, the name in ink and every other cell in the body grey, except the status badge and a failed action's message. “Pick the variant by what the user needs: no-results for a filtered list that returned zero rows, blank slate or informational for a resource the user hasn’t created, cleared for completed work, permission for role or tier denials, error for a failed load.” (https://vercel.com/geist/empty-state) Ours: `failed`, `noMatch` and `empty` are those variants, drawn inside the card. “The right column holds at most one or two controls. If the row needs more, move secondary actions into a `Dots Menu`.” (https://vercel.com/geist/entity) Ours: one or two row commands (outline buttons of 32 px) stand before the three dots (36 px), the Dots Menu. “Render the Skeleton variant (`entity-with-skeleton`) during load instead of an empty row, and swap to real content once data resolves.” (https://vercel.com/geist/entity) Ours: three still skeleton rows hold the place, in a region that announces what replaces them; then the first that holds: the failed read, no match, the empty state, the rows.",
190
+ words: '“Lead the left column with a scannable identifier: an `Avatar` or icon, a Title Case label, then sentence-case secondary metadata (`Member since Mar 14, 2026`).” (https://vercel.com/geist/entity) Ours: the name is the record\'s own and the labels around it are sentence case, not Title Case. “Keep right-column buttons Verb + Noun (`Remove Member`, `Resend Invite`). Bare verbs like `Remove` or `Confirm` lose context once the row scrolls offscreen.” (https://vercel.com/geist/entity) Ours: every string is the product\'s: `label` names the list, a time\'s `phrase` writes its sentence ("Updated {when}"), `missing` is what a screen reader hears for a record with no time, the menu\'s `label` reads "More for {record}". A row command names its result in sentence case ("Edit domain"). A status is the exception\'s word ("Needs attention"), never "Active" on every row.',
191
+ access: '“Wrap the loading region in `aria-busy="true"` and announce completion with `aria-live="polite"` on the destination container, not the skeleton itself.” (https://vercel.com/geist/skeleton) Ours: the loading rows sit in one `SkeletonRegion` that is busy while they stand and announces what replaces them. A table named by `label`, its column headings read by assistive technology only. The row link carries `open.label`; the three dots carry the menu\'s `label`, and the menu\'s first line names the record. The loading rows are a status named by `loadingLabel`, inside the one live region that announces what replaces them. “For multi-select rows, the leading `Checkbox` carries `aria-label="Select {entity name}"` so the row is selectable without relying on the visual label.” (https://vercel.com/geist/entity) Ours: each box is named by `select.label`, which reads "Select {record}".',
192
192
  },
193
193
  {
194
194
  name: "RecordSheet",
@@ -216,45 +216,56 @@ export const USAGE = [
216
216
  name: "SettingsPage",
217
217
  module: "settings-page",
218
218
  family: "Compositions",
219
- summary: "A page of settings: a centred column of section cards, each saving its own values.",
219
+ summary: "A page of settings: section cards across the page's width under its title, each saving its own values.",
220
220
  when: "A place whose job is changing how something behaves: an account's profile, a company's identity, Nadia's notifications.",
221
- instead: "One object's page with a settings card among its sections is an ObjectPage.",
222
- behaviour: "The sections stack 20 apart in a centred column at most 56rem wide. The page draws no header and no save of its own: a choice applies at once, and each SettingsSection of typed values saves from its footer.",
223
- words: '`title` names the place as the navigation does ("Profile"); the sections\' titles and lines carry the rest.',
221
+ instead: 'One object\'s page with a settings card among its sections is an ObjectPage; "Place it once per app, in the footer or settings, not duplicated across pages." (https://vercel.com/geist/theme-switcher) Ours: Theme and Language, which apply on change, stay in the account menu.',
222
+ behaviour: "One setting per card (the card's shapes are SettingsSection's: one field, a row of short parts of one value, rows of switches or of selects, a header switch), the cards stacked 24 apart across the page's content width under the page's title row, as Vercel's settings cards fill their column, with no heading between them; a field inside keeps its own bound. A page that mixes cards with other content, such as a table or a list of rules, is not a SettingsPage: its sections take a plain h2 above each card. The page draws no header and no save of its own: each SettingsSection saves from its footer. The danger zone closes the page, in rising severity: transfer, then pause, then delete; only delete is `tone=\"error\"`, and only delete and pause take a button in a hue.",
223
+ words: "`title` names the place as the navigation does (\"Profile\"); the sections' titles and lines carry the rest. A card's title is never the page's h1.",
224
224
  access: "`title` becomes a visually hidden h1 for a product whose chrome shows the place without a heading; leave it out where the chrome already draws the h1.",
225
225
  },
226
226
  {
227
227
  name: "SettingsSection",
228
228
  module: "settings-section",
229
229
  family: "Compositions",
230
- summary: "A card of related settings that says what changing them does and saves them from its own footer.",
231
- when: "A few related settings on a SettingsPage or as a section of an ObjectPage: choices that apply at once, without `save` (Notifications), or typed values a person saves together, with `save` (Personal details, Company identity).",
232
- instead: "Read-only values are a SettingsFactsSection; a collection is a CollectionPage; a record's edit in a list is a RowEditForm.",
233
- behaviour: "With `save`: Save stays disabled until a field differs from what is saved, says `pendingLabel` while saving with Cancel held beside it, then calls `saved`, which the product answers with its own toast; the footer never shows a saved state. A failure keeps every value and shows under its field until that field changes, or above the footer until the next save. A `cancel` that runs work of its own says its `pendingLabel` while `pending`. Without `save`, the footer holds `footerHint` and `footer`, and is left out when both are.",
234
- words: 'The title names the values ("Personal details"); the description says what changing them does, in one line; the hint is optional and says what is specific to this card: a limit ("Please use 32 characters at maximum."), what a value is for, the plan a feature needs, or a "Learn more" link, never one sentence every card repeats; Save and Saving come from the product\'s catalogue, and the saved toast from its notices.',
235
- access: "The title is the section's h2 and names the form that saves it. The saved toast is announced by the product's toast region; a failure under a field marks the control invalid and describes it.",
230
+ summary: "A card of one setting that says what changing it does and ends in a footer.",
231
+ when: "One setting a person changes on a SettingsPage or as a section of an ObjectPage: Personal details, Company identity, Notifications. A typed value takes `save`; a switch or a short reversible choice applies at once through its own control's change, with no Save. Also a plan-gated feature, a delete card, one read-only value (an ID in a read-only mono InputGroup with its copy button, no Save), and a paid quantity (its stepper field, the price as the description, a two-fact summary under it).",
232
+ instead: "Read-only facts of an object are a SettingsFactsSection; a table or a list of records is a TableCard; Theme and Language live in the account menu; a record's edit in a list is a RowEditForm.",
233
+ behaviour: '"Groups related form controls inside a bordered card with optional footer actions." (https://vercel.com/geist/fieldset) Ours: one setting per card: a single SettingsField, the description its only help. One more shape exists, declared by a part and never by classes at the call site. A card of several settings holds SettingsRows, label left and control right, only when every row is a switch or every row is a select. "Use Toggle for a single boolean setting where ON takes effect immediately, like `Password Protection` or `Auto-Cancel Builds`." (https://vercel.com/geist/toggle) "Pair with a form footer only when the setting needs an explicit `Save` step." (https://vercel.com/geist/toggle) "Disable Toggle only when the action is impossible (missing plan, locked policy); pair with helper text or a `Tooltip` that names the resolver." (https://vercel.com/geist/toggle) Ours: that switch is `headerSwitch`: at the title\'s right, named by the title, the footer holding only its line, no Save; a switch blocked from above is shown off and disabled, the footer saying why and never a tooltip. Required: the title and the description. The header\'s actions stand at the title\'s right; on a card narrower than 28 rem they keep the title\'s row and the description runs under both, as Vercel\'s cards read on a phone. The footer\'s left line (`footerHint`, `save.hint`) is optional and specific to the card, never a generic line repeated on every card: a limit, what a value is for, a plan it needs, a link; absent, it is nothing, and a card with neither a line nor a button has no footer. Ours: `save`: a typed value, one card with Save. Save stays disabled until a field differs from what is saved and says `pendingLabel` while saving with Cancel beside it; a completed save calls `saved`, and the product raises one toast from its catalogue ("Completion toasts follow `{Noun} {past-participle}`: `Blob deleted`, `Domain added`, `Environment variable saved`. Never include `successfully`; the action name implies it." https://vercel.com/geist/toast), and Save returns to disabled. Ours: the section never raises a toast itself. A failed save keeps every value and shows under its field, or above the footer when no field owns it. `tone` marks a state with a command (a failed payment, a trial ending): a 1px border and a footer tinted in the hue with its line in it; title, line, fields and buttons stay neutral, an outline secondary then a default primary. A button takes the hue of its command, not of the card: `destructive` for a permanent loss, `warning` for a disruptive command that can be undone. A delete card is `tone="error"` with the object\'s summary as its body; an archive card is neutral with a `warning` button in the footer. "Use a Note for inline contextual feedback next to the field, card, or section it describes: a region change warning above a region picker, a rate-limit notice next to a usage gauge." (https://vercel.com/geist/note) "One Note per concept." (https://vercel.com/geist/note) Ours: a message about the fields inside is a MessageNotice in the body, the card staying neutral. "Disabled checkboxes still need a Tooltip naming the reason; a greyed box with no explanation reads as a bug." (https://vercel.com/geist/checkbox) Ours: `lockedReason` is the reason, in the card, not in a tooltip; it keeps the fields in view, inert with muted text, under a not-allowed cursor, the title and line unchanged; `unlock` is the way to get access, left out when nothing the person does unlocks it.',
234
+ words: '"Helper text is sentence case, one sentence with a trailing period, on a sibling element wired through `aria-describedby`." (https://vercel.com/geist/input) Ours: the description is that helper text for the whole card, and no field has its own. "Render an optional one-sentence description as a sibling under the label that explains ON only. Don’t describe OFF; it’s the negation." (https://vercel.com/geist/toggle) Ours: a header-switch card\'s description says what turning it on does. The title names the values ("Personal details"); the description says what changing them does, in one line; the footer line is a Learn more link, a limit ("Use 32 characters at most."), what stays unaffected, or what is blocked and why ("Ask an owner to change this."); Save and Saving come from the product\'s catalogue, and so does the toast a `saved` callback raises; a delete card\'s description opens "Permanently delete".',
235
+ access: "\"Provide an accessible name through `children`, `aria-label`, or `aria-labelledby`.\" (https://vercel.com/geist/toggle) Ours: the title is the section's h2 and names the form that saves it, and the header switch. The footer's line is one status, announced politely; a failure under a field marks the control invalid and describes it. Locked fields are inert, so the section's title, description and reason are what a screen reader meets.",
236
236
  },
237
237
  {
238
238
  name: "Button",
239
239
  module: "button",
240
240
  family: "Actions",
241
- summary: "The package's one button: five looks at 36 px, the label naming the result.",
242
- when: "A command that changes something or opens a surface. One `default` per surface: the thing this page or dialog is for.",
243
- instead: "Going somewhere is a link drawn with `buttonVariants`, and a link inside a sentence is `TextLink`, blue and underlined. A record's secondary commands go in `ActionMenu`. An on/off value is a `Switch`.",
244
- behaviour: "Enter and Space run it. A press settles at 97% scale in 180 ms, except on a trigger that opens a popup. Disabled is 50% and ignores the pointer, and its reason is written beside it. On touch the target grows to 44 px while the drawing stays 36. `sm` and `xs` are for a control nested in dense content, such as a composer or an input group, never for chrome.",
245
- words: "Sentence case, verb + noun naming the result: *Invite member*, *Remove mailbox*; never OK, Submit or Confirm. Italian runs about a third longer (*Rimuovi casella*), so a footer leaves room and a label never wraps.",
246
- access: 'A native `button`, `type="button"` unless it submits. An icon-only button carries an `aria-label` naming action and target. Focus is the 2 px ink outline at 2 px offset, drawn once by the base layer, never repainted.',
241
+ summary: "The package's one button: a command, its label naming the result.",
242
+ when: '"Use `Button` for actions that mutate state (deploy, save, delete); use `ButtonLink` for navigation that changes the URL." (https://vercel.com/geist/button) Ours: one `default` per surface, the thing this page or dialog is for.',
243
+ instead: '"Switch to a `Menu` or `Split Button` when more than one related action shares a row." (https://vercel.com/geist/button) Ours: a link inside a sentence is `TextLink`. A record\'s secondary commands go in `ActionMenu`. Appending to a list is a `LoadMoreButton`. An on/off value is a `Switch`.',
244
+ behaviour: '"Default `Button` is primary." (https://vercel.com/geist/button) "Pass `type="secondary"` for the supporting action and `type="error"` for destructive confirmations." (https://vercel.com/geist/button) "`primary`, `success`, `ghost`, and `violet` are not valid `type` values." (https://vercel.com/geist/button) "For form submits, use `typeName="submit"`." (https://vercel.com/geist/button) "The HTML `type` attribute lives on `typeName`, not on `type`, which controls the visual variant." (https://vercel.com/geist/button) Ours: the look is `variant` (`default`, `secondary`, `outline`, `ghost`, `link`, `destructive` for Geist\'s error, `warning`) and `type` is the HTML type, `button` unless it submits. "The default size is medium." (https://vercel.com/geist/button) Ours: `xs`, `sm`, `default` (medium) and `lg`; `sm` and `xs` are for a control nested in dense content, such as a composer or an input group, never for chrome. "Pass `loading` instead of swapping in a spinner so the button stays focusable and announces the busy state to assistive tech." (https://vercel.com/geist/button) Ours: `loading` is the only busy state: the button keeps its place and focus, says `loadingLabel` in the label\'s place, ignores a second press and shows no spinner. "Disable a button only when the action is impossible right now (missing input, insufficient permission); pair with a `Tooltip` that explains why." (https://vercel.com/geist/button) Ours: the reason is written beside the button. "Icon-only buttons should include the `svgOnly` prop and an `aria-label`." (https://vercel.com/geist/button) Ours: an icon-only button takes an `icon` size and its icon as the child; `shape="circle"` is only for it. An icon with text passes as `prefix` or `suffix`. "Combination of `shape="rounded"` and the `shadow` prop, often used on marketing pages." (https://vercel.com/geist/button) Ours: `shape="rounded"` is a pill for a call to action that stands apart (a banner\'s or a scroller\'s), with no `shadow` prop. Ours: text children sit in a label box that adds 6 px each side to the padding; an element child (a `render` target, a row with a trailing `Kbd`) lays itself out and gets no box. Colour sits on the whole button for three looks: `default` is ink, `destructive` is the error fill for permanent loss (Delete project), `warning` is the amber fill for a disruptive command that can be undone (Pause project). `link` is the link colour as text, with the arrow as `suffix` when the destination is outside the product. `outline` and `secondary` are neutral with a hairline edge, `ghost` has no chrome, and no look puts a hue on its edge. Held (disabled or `loading`), a filled look drops its hue to the muted fill, the muted text and the hairline, so it reads as unavailable; `ghost` and `link` dim instead. Pressed looks like hover, with no movement.',
245
+ words: '"Title Case the label and name what happens: `Deploy Project`, `Invite Member`, `Rotate Key`." (https://vercel.com/geist/button) "Avoid bare verbs (`Submit`) and generic confirms (`OK`, `Confirm`)." (https://vercel.com/geist/button) "Destructive buttons follow `Verb + Noun` and pair 1:1 with their toast: `Delete Project` then `Project deleted`." (https://vercel.com/geist/button) "Mode-switch buttons append `Instead`: `Use a Recovery Code Instead`." (https://vercel.com/geist/button) Ours: sentence case, not Title Case: *Invite member*, *Remove mailbox* then *Mailbox removed*, *Use a recovery code instead*. `loadingLabel` is the same words in the -ing form: *Inviting member*. Italian runs about a third longer (*Rimuovi casella*), so a footer leaves room and a label never wraps.',
246
+ access: '"Icon-only buttons require both `svgOnly` and `aria-label`; the validator throws without them." (https://vercel.com/geist/button) "The `aria-label` names the action and the target (`Copy deployment URL`), not the icon (`Copy`)." (https://vercel.com/geist/button) "Don’t set `aria-label` on a button that already has visible text; it overrides the label and creates a screen-reader mismatch." (https://vercel.com/geist/button) Ours: the type requires `aria-label` or `aria-labelledby` on an icon-only button. A native `button`, `type="button"` unless it submits. `loading` sets `aria-busy`. Focus shows the base outline in one colour for every look; nothing repaints it.',
247
247
  },
248
248
  {
249
249
  name: "ButtonGroup",
250
250
  module: "button-group",
251
251
  family: "Actions",
252
252
  summary: "Two or three buttons that share one edge and read as one control.",
253
- when: "Two or three buttons that act on one thing and read as one control: an applied filter with its remove, a pager. A split action (*Download PDF* with its chevron for CSV and Excel) is a group of a `Button` and a `DropdownMenu` trigger.",
254
- instead: "Choosing one value of several is a `RadioGroup` drawn as a segmented control, or a `ToggleGroup`. Unrelated actions stay separate buttons 8 px apart. A destructive command never sits in a split, neither as the default nor in its menu.",
255
- behaviour: "One shared edge between members and rounded outer corners only. The focused member rises above its neighbours so its whole outline shows. Each member keeps its own tab stop. In a split, the left half runs the default and the chevron opens the menu with the default listed first and checked.",
256
- words: "Each member is labelled on its own. The group's name says what it holds: *Status filter*, *Pages*. A split's default names its variant (*Download PDF*); its chevron names the family: *More download formats*.",
257
- access: '`role="group"` with an `aria-label`. An icon member names its action and target: *Remove filter: Status*. A split\'s chevron has `aria-haspopup="menu"`, `aria-expanded` and its own `aria-label`.',
253
+ when: "Two or three buttons that act on one thing and read as one control: an applied filter with its remove, a pager.",
254
+ instead: "Two or three views of one surface are a `SegmentedControl`; one value of several is a `RadioGroup`; pressed buttons that can be several or none are a `ToggleGroup`. Unrelated actions stay separate buttons 8 px apart.",
255
+ behaviour: "One shared edge between members and rounded outer corners only, at the radius of the members' size. The focused member rises above its neighbours so its whole outline shows. Each member keeps its own tab stop.",
256
+ words: "Each member is labelled on its own. The group's name says what it holds: *Status filter*, *Pages*.",
257
+ access: '`role="group"` named by `aria-label` or `aria-labelledby`, and the type requires it. An icon member names its action and target: *Remove filter: Status*.',
258
+ },
259
+ {
260
+ name: "ButtonLink",
261
+ module: "button",
262
+ family: "Actions",
263
+ summary: "A link drawn as a button, with the props of `Button`.",
264
+ when: '"Use `Button` for actions that mutate state (deploy, save, delete); use `ButtonLink` for navigation that changes the URL." (https://vercel.com/geist/button) Ours: a sign-in, a download, a record\'s page. It is read as a link and drawn as a button.',
265
+ instead: 'A command that changes something is a `Button`. A link inside a sentence is `TextLink`. The product\'s router link keeps its own behaviour through `render`: `render={<Link href="/agents" />}`.',
266
+ behaviour: '"Use `ButtonLink` for links with the same props as `Button`." (https://vercel.com/geist/button) Ours: takes the looks, sizes, `prefix`, `suffix` and `loading` of `Button`. While it is `loading` or `disabled` it ignores a click and stays focusable.',
267
+ words: "Sentence case, verb + noun naming the destination or the result: *Open Portal*, *Download invoice*. A destination outside the product ends in the arrow as `suffix`.",
268
+ access: 'A native `a` with an `href`, never `role="button"`. An icon-only link carries an `aria-label` or `aria-labelledby` naming destination, and the type requires it.',
258
269
  },
259
270
  {
260
271
  name: "CopyButton",
@@ -263,9 +274,20 @@ export const USAGE = [
263
274
  summary: "Copies an exact value someone will paste elsewhere.",
264
275
  when: "An exact value someone will paste elsewhere: an Agent ID, a server address, a command. A shell prompt shown before a command stays outside the copied value.",
265
276
  instead: "Several lines of code are a `CodeBlock`. Ordinary prose never gets copy chrome. A secret needs its reveal policy first.",
266
- behaviour: "Copies the full underlying value even when the shown one is shortened. The check shows for 2 s, and only after the clipboard confirms. A failure shows the alert mark and the button stays for a retry. Sizes: `icon` (36), `icon-sm` (32) after a value, `icon-xs` (24) inside a field.",
277
+ behaviour: '"A button that copies a given string to the clipboard and provides feedback when copied." (https://vercel.com/geist/copy-button) Ours: copies the full underlying value even when the shown one is shortened. The copy mark fades into the check for about a second, and only after the clipboard confirms; no colour changes. A failure shows the alert mark and the button stays for a retry. The standalone `icon` button is outlined; `icon-sm` sits after a value and `icon-xs` inside a field, both without chrome. `copied` shows the check from outside for a parent that copies other text, and `onCopy` runs once the clipboard confirms. While the clipboard works a second press is ignored and the button looks as it did, with no held look.',
267
278
  words: 'Three catalogue strings: `copyLabel` names the target ("Copy Agent ID"), `copiedLabel` ("Copied"), `copyFailedLabel` ("Couldn\'t copy").',
268
- access: '`aria-label` is the copy label and does not change. The result is read once from a hidden `role="status"`. The button stays focusable while it works.',
279
+ access: '`aria-label` is the copy label and does not change. The result is read once from a hidden `role="status"`.',
280
+ },
281
+ {
282
+ name: "LoadMoreButton",
283
+ module: "load-more-button",
284
+ family: "Actions",
285
+ summary: "The button at the end of a list that appends its next items in place.",
286
+ when: '"A full-width button used to append more items to a paginated list, with loading and styling variants." (https://vercel.com/geist/load-more-button) Ours: a list that grows in place by a page of items at a time: older events, more results. The button follows the last item.',
287
+ instead: "A list that is whole stays whole, with no button. Going to another page of the product is a `ButtonLink`.",
288
+ behaviour: "Takes the list's full width, 36 px high, below the last item, always `outline`: neutral, no hue in any state. The label and the `loadingLabel` are both required. While the next items load it is `loading`, held in the muted look, and the rows already shown stay. It sits one `md` step (16 px) below the list; `noGap` removes that space and `noBorderRadius` squares its corners, for a button that closes a list inside a card. The caller drops the button when no items are left.",
289
+ words: "Ours: sentence case, naming what comes: *Load more*, *Show older events*. The loading word is the same in the -ing form: *Loading more*, *Showing older events*; Italian *Carica altro*, *Caricamento*.",
290
+ access: "A native `button` that is `aria-busy` while it loads. New items appear after the last shown one, so focus stays on the button's place in the list.",
269
291
  },
270
292
  {
271
293
  name: "PageActions",
@@ -274,7 +296,7 @@ export const USAGE = [
274
296
  summary: "A page's actions, drawn on its title's row. Only the page shown last owns the row: a page kept hidden for a quick return gives the row back. Outside a `PageActionsHostProvider`, they render where they stand.",
275
297
  when: "The actions of a collection, an object or an overview page, on the title's row at its trailing edge. A Configuration page has none: each section saves from its footer.",
276
298
  instead: "A row's actions are the row's one button and its `ActionMenu`. A collection toolbar holds search and filters, never a page action.",
277
- behaviour: "Takes `period`, `secondary`, `primary` and `menu` and draws their order and variants itself: the period, outline, default, then the three dots last. The menu needs two commands or more; a single extra command is the secondary. A destination outside the product ends in the arrow. A running command keeps its place and icon, says its pending word and holds.",
299
+ behaviour: "Takes `period`, `secondary`, `primary` and `menu` and draws their order and variants itself: the period, outline, default, then the three dots last. The menu needs two commands or more; a single extra command is the secondary. A destination outside the product ends in the arrow. A running command (`pending`) keeps its place and icon, says its pending word and holds, and the type requires `pendingLabel` whenever `pending` is set. A command here is never destructive or warning: those sit in the danger zone of the page, in a card.",
278
300
  words: 'The primary is the page\'s main job as verb and noun: "Invite member", "New Agent". The pending word is its -ing form: "Inviting member". The menu names the page\'s record at its top and keeps the destructive command last, after a divider.',
279
301
  access: 'DOM order is visual order. A command whose label does not name its target carries `accessibleName`. A running command is `aria-busy` and keeps focus. The three dots carry "More for {record}"; the menu returns focus to them when it closes.',
280
302
  },
@@ -283,66 +305,66 @@ export const USAGE = [
283
305
  module: "calendar",
284
306
  family: "Inputs",
285
307
  summary: "A month grid for choosing a day or a range of days.",
286
- when: "A date or a window whose day and month matter. A chart or a report's window is one `DateRangeControl` in its toolbar, which opens this calendar from its *Custom* preset.",
287
- instead: "A row of range buttons is never drawn; the presets live in one select. A time of day or a recurring schedule is its own field, not a calendar.",
288
- behaviour: "The first click starts a range, the second ends it. 28 px days, Monday first in Italian, ends in ink, today in the muted fill. Arrow keys move by day, Page Up and Down by month.",
289
- words: "Dates are written in the reader's locale (*7 Oct – 13 Oct*, *7–13 ott*), never the ISO pair a URL stores.",
290
- access: "A grid of day buttons, each named with its full date. Focus goes into the grid when it opens and back to its trigger when it closes, and the chosen range is read out.",
308
+ when: '"Pick `<Calendar>` for analytics ranges and any picker where day-of-week and month context matter." "Provide `<Calendar.Presets>` for the common ranges (`Last 7 Days`, `Month to Date`) so users land on the right window in one click." (https://vercel.com/geist/calendar) Ours: a chart or a report\'s window is one `DateRangeControl` in its toolbar, which puts the common windows as buttons beside this grid.',
309
+ instead: '"For ISO dates pasted whole or relative shorthand like `7d`, use a free-form `Input`." (https://vercel.com/geist/calendar) Ours: a time of day or a recurring schedule is its own field, not a calendar.',
310
+ behaviour: '"Set `min` and `max` to the data window so users can’t pick outside the retention range." "Support arrow-key day navigation, `Shift` + arrow for week jumps, and `Page Up` / `Page Down` for month jumps." (https://vercel.com/geist/calendar) Ours: `min` and `max` are `minValue` and `maxValue`: a day outside the window cannot be picked and the month arrows stop at it. Up and Down move a week without `Shift`, Left and Right a day, Page Up and Page Down a month. `mode`, `selected` and `onSelect` are required: the grid always chooses and is controlled. The month\'s name sits left, the round previous and next buttons right, and the weekday letters start 42 px below the top; a day is a 32 px square centred in its column of a 256 px grid, 8 px between weeks and 15 px between the weekday letters and the first week. The first click starts a range, the second ends it. Monday first in Italian. Colour: the ends of a range are ink over a muted band, today is the one blue day (a solid info fill, unless selected), everything else is neutral, Saturday and Sunday are a step lighter than the weekdays and days of other months lighter still, and hover draws only a hairline edge. A calendar that opens from a trigger sits 8 px below it, 280 px wide with its own 12 px padding and no padding of the popover around it.',
311
+ words: '"Default to the user’s locale and timezone; never silently render UTC for a US-Pacific viewer." (https://vercel.com/geist/calendar) Ours: dates are written in the reader\'s locale (*7 Oct – 13 Oct*, *7–13 ott*), never the ISO pair a URL stores.',
312
+ access: '"Trap focus inside the popover so Tab cycles day cells and presets instead of the page behind it." "Announce range changes through `aria-live="polite"` so a screen reader hears `From Apr 1 to Apr 28` after the second click." (https://vercel.com/geist/calendar) Ours: a grid of day buttons, each named with its full date. Focus goes into the grid when it opens and back to its trigger when it closes. The selected days are marked as selected; the control that owns the grid announces the range.',
313
+ },
314
+ {
315
+ name: "ClearableInput",
316
+ module: "clearable-input",
317
+ family: "Inputs",
318
+ summary: "A text box that empties itself: a clear button once it has text, and Escape.",
319
+ when: '"Text input with a clear button that resets the value on Escape." (https://vercel.com/geist/clearable-input) Ours: a single-line value a person may want to empty in one move: a filter, a search, a reference they paste and replace.',
320
+ instead: "A collection's search is `CollectionSearch`. A value that is never emptied is `Input`. A box with a fixed prefix or suffix is `Input`; one with a copy button is `InputGroup`.",
321
+ behaviour: 'Same ring, halo, hover, disabled and error as `Input`; colour sits where it does there and nowhere else. The clear button shows only while the box has text and is enabled, as an Esc key cap flush against the end of the box that darkens on hover and keeps a transparent fill (a close mark on a touch screen, which has no Escape). Pressing it, or Escape inside the box, empties the value, calls `onClear` and puts focus back in the box. Escape on an empty box does nothing here, so it closes the dialog or drawer around it. "Automatically clears the input if escape is pressed." (https://vercel.com/geist/input) "Displays the <kbd>⌘</kbd> <kbd>K</kbd> shortcut to indicate that the input supports a command palette." (https://vercel.com/geist/input) "Transitions to showing <kbd>Esc</kbd> when the field is dirty." (https://vercel.com/geist/input) Ours: the `shortcut` keys are one cap per key (*⌘* then *K*, 4 px apart, 10 px from the edge), hidden on a touch screen. `loading` marks the box busy and, after a second, draws the waiting line along its bottom edge; nothing spins and the results stay. `label` and `error` draw the `Field` parts around the box as on `Input`, and a string `label` requires `id`; a box with no name does not compile. `className` lands on the box.',
322
+ words: 'The clear button is named for what it empties (*Clear search*, *Clear filter*). "Placeholders show an example value (`my-awesome-project`, `example.com`), never instructions like `Enter your project name`." (https://vercel.com/geist/input) Ours: the placeholder may also be the scope.',
323
+ access: "A visible `label` or `aria-label` names the box. The clear button is a named button, `aria-keyshortcuts` Escape; the prefix and the keys are hidden from assistive technology. `aria-busy` while `loading`.",
291
324
  },
292
325
  {
293
326
  name: "CollectionSearch",
294
327
  module: "collection-search",
295
328
  family: "Inputs",
296
329
  summary: "The search a collection toolbar fills its row with.",
297
- when: "Finding a record inside one collection, at the start of its toolbar.",
298
- instead: "Finding anything across the product is `CommandDialog`. Narrowing by a facet is `CollectionFilters`. A free value in a form is `Input`.",
299
- behaviour: "Results stay on screen while a search updates. Escape clears only while the box holds the interaction and has a value; otherwise it closes the overlay as usual.",
300
- words: "The search's label is what it finds, also shown as its placeholder: *Find a skill*, *Search the hub*.",
301
- access: 'The input carries `aria-label` from `label` and `type="search"`; the search mark is decorative.',
330
+ when: '"Pre-configured search input with a magnifying glass icon and clear button." (https://vercel.com/geist/search-input) Ours: finding a record inside one collection, at the start of its toolbar.',
331
+ instead: 'Finding anything across the product is `CommandDialog`. Narrowing by a facet is `CollectionFilters`. A free value in a form is `Input`. "For an inline search box, set the `search` variant with a scoped placeholder like `Search projects`; don’t embed a `<SearchInput>` inside an unrelated form." (https://vercel.com/geist/input) Ours: this component is the search; `Input` has no `search` variant.',
332
+ behaviour: 'A `ClearableInput` with the search mark in a 14 px slot: an Esc key cap flush against the end of the box once it has text. Ring, halo, hover and disabled are `Input`\'s. Results stay on screen while a search updates; `loading` marks the box busy and draws the waiting line after a second. "Automatically clears the input if escape is pressed." (https://vercel.com/geist/input) "Displays the <kbd>⌘</kbd> <kbd>K</kbd> shortcut to indicate that the input supports a command palette." (https://vercel.com/geist/input) "Transitions to showing <kbd>Esc</kbd> when the field is dirty." (https://vercel.com/geist/input)',
333
+ words: '"A `SearchInput` placeholder names the scope (`Search projects`) so the role is clear without sighted context." (https://vercel.com/geist/input) Ours: the search\'s label is what it finds, also shown as its placeholder: *Find a skill*, *Search the hub*.',
334
+ access: 'The input carries `aria-label` from `label` and `type="search"`; the clear button is named by `clearLabel`; the search mark is decorative. A search always has both names, so neither can be left out.',
302
335
  },
303
336
  {
304
337
  name: "Field",
305
338
  module: "field",
306
339
  family: "Inputs",
307
- summary: "A control with its label, its help and its error, tied together.",
308
- when: "Every control with a name: it ties the label, the help and the error to the control. Inside a settings section `SettingsField` does it and hands the control its `controlProps`. A rich choice tile is a `FieldLabel` holding a `Field` with its radio or checkbox.",
309
- instead: "A group of radios or checkboxes is a `FieldSet` with a `FieldLegend`. A whole save group is `SettingsSection`.",
310
- behaviour: "Label, then help, then the error, then the control, 8 px apart. Invalid turns the label and message red and puts the red edge and outline on the control. Validate on submit or on leaving a field, then update the message as the value is corrected.",
311
- words: "The help says what the setting changes, in one sentence. The error says how to fix it, not what went wrong: *Enter a full address, like name@company.it.* A disabled field says why.",
312
- access: '`FieldError` is `role="alert"`. The control carries `id`, `aria-invalid`, `aria-describedby` (help, then error), `disabled` and `required`.',
313
- },
314
- {
315
- name: "Form",
316
- module: "form",
317
- family: "Inputs",
318
- summary: "A react-hook-form form whose fields bind their label, help and message for the reader.",
319
- when: "A client form whose values, validation and submit run through react-hook-form: `FormField` binds a control, `FormMessage` shows its error.",
320
- instead: "A settings group that saves on its own is `SettingsSection`. A short form in a dialog is `FormDialog`; one in a row is `RowEditForm`. A form with no client validation is a plain `form` with `Field`.",
321
- behaviour: 'Validation runs on submit and on leaving a field, then updates while the value is corrected. A failed save keeps every value. The submit is `type="submit"`, so Enter in a field submits.',
322
- words: "Messages come from the product's catalogue or schema: how to fix the value, not what went wrong.",
323
- access: "`FormControl` gives the control its `id`, `aria-invalid` and `aria-describedby` (help, then message); `FormMessage` is read when it appears.",
340
+ summary: "A control with its label and its error, tied together; a checkbox, a radio or a switch with the sentence beside it.",
341
+ when: "A checkbox, a radio or a switch with its sentence, or a group of such rows. Inside a settings section `SettingsField` does it and hands the control its `controlProps`.",
342
+ instead: 'A text box, a textarea, a select, a combobox or a multi-select takes `label` and `error` as props and draws these same parts: wrapping one in `Field` with a `FieldLabel` is the same job done twice. A bordered tile that holds a radio or checkbox and a sentence is `ChoiceboxGroup`. A whole save group is `SettingsSection`. A group of radios or checkboxes is a `FieldSet` with a `FieldLegend`: "Wrap related radios in `<fieldset>` + `<legend>` so screen readers announce the group name before each option." (https://vercel.com/geist/radio) "Wrap related checkboxes in `<fieldset>` with a `<legend>` so screen readers announce the group name before each option." (https://vercel.com/geist/checkbox)',
343
+ behaviour: '"Accessible text label for form controls." (https://vercel.com/geist/label) Label, then the control, then the error under it, 8 px apart, at regular weight; the fields of a `FieldGroup` stand 16 px apart. The label stays neutral when the control is invalid: red sits on the control\'s edge and on the message, the message with an alert mark before it at 13 px (16 px beside a large control). A control fills its field. "Helper text is sentence case, one sentence with a trailing period, on a sibling element wired through `aria-describedby`." (https://vercel.com/geist/input) Ours: help is the one-line description of the card or the form that holds the field, not a line under each field; a `FieldDescription` is for the sentence beside a checkbox, a radio or a switch. "Validate on blur, not on every keystroke; surface the message by passing a string to `error`." (https://vercel.com/geist/input) "Validate on blur and pass a string to `error` to render the inline message; the validation replaces helper text on failure." (https://vercel.com/geist/textarea) Ours: the message updates as the value is corrected. "Keep the field focusable while saving; pair `disabled` with a spinner only when input is impossible." (https://vercel.com/geist/input) Ours: no spinner, the loading state stays still. "Trim leading and trailing whitespace before submit so ` example.com` and `example.com` resolve to one value." (https://vercel.com/geist/input) Ours: the save of `FormDialog`, `FormDrawer`, `RowEditForm` and `SettingsSection` trims the ends of every text value; a bare `<form>` does not. "The click target already extends to the label. Don’t override the `<label>`/`htmlFor` association with a custom wrapper that breaks the click region." (https://vercel.com/geist/checkbox) "Disabled checkboxes still need a Tooltip naming the reason; a greyed box with no explanation reads as a bug." (https://vercel.com/geist/checkbox) Ours: a disabled field says why in the card\'s footer, never in a tooltip around the control.',
344
+ words: '"Labels are short Title Case nouns: `Project Name`, `Domain`, `Environment Variable Name`." (https://vercel.com/geist/input) Ours: sentence case, a short noun (*Display name*); a field that may stay empty says so in its label (*Address line 2 (optional)*), never in its placeholder. "Placeholders show an example value (`my-awesome-project`, `example.com`), never instructions like `Enter your project name`." (https://vercel.com/geist/input) "Validation names the field and the constraint, ends in a period, and skips `please` (`Project name is required.`, `Code must be 6 digits.`)." (https://vercel.com/geist/input) Ours: *Enter a full address, like name@company.it.*',
345
+ access: '`FieldError` is `role="alert"`. The control carries `id`, `aria-invalid`, `aria-describedby` (the error), `disabled` and `required`. "Don’t wrap a labelled `<Input>` in a `Tooltip`; put the explainer on a sibling icon button so the label stays announced." (https://vercel.com/geist/input)',
324
346
  },
325
347
  {
326
348
  name: "Input",
327
349
  module: "input",
328
350
  family: "Inputs",
329
- summary: "One line of free text, edged in the input grey.",
330
- when: "One free-form value on one line: a name, an address, a number.",
331
- instead: "Text with a fixed prefix, an icon or a button inside is `InputGroup`. Several lines are `Textarea`. A known set of values is a select, radios or a combobox.",
332
- behaviour: "36 px, edged in the `input` role's grey with no shadow, 6 px corners. Focus is the 2 px outline. Invalid keeps a red edge and a red outline even without focus. Disabled is 50% on a half-grey fill. The value survives a failed save.",
333
- words: "The label is a noun in sentence case with no colon. The placeholder is an example (*Bizeta Arredamenti*), never the label or an instruction. In a configuration section the box stops at 20 rem under its label.",
334
- access: "A visible `label` tied by `id`, the help and the error joined through `aria-describedby`, `aria-invalid` when wrong. Use the right `type`, `autocomplete` and `inputMode`.",
351
+ summary: "One line of free text.",
352
+ when: '"Retrieve text input from a user." (https://vercel.com/geist/input) "Pick `<Input>` for a single line of free-form text such as names, domains, or tokens." (https://vercel.com/geist/input)',
353
+ instead: '"Switch to `Textarea` the moment content can wrap to multiple lines." (https://vercel.com/geist/input) "Use `Combobox` when the value comes from a known list the user filters by typing." (https://vercel.com/geist/input) "For an inline search box, set the `search` variant with a scoped placeholder like `Search projects`; don’t embed a `<SearchInput>` inside an unrelated form." (https://vercel.com/geist/input) Ours: there is no `search` variant. A collection\'s search is `CollectionSearch`, and a value people empty often is `ClearableInput`. A short known set is radios or a select. A button inside the box, or a server-given value with its copy button, is an `InputGroup`.',
354
+ behaviour: 'Sizes `sm` (32 px), default (36 px) and `lg` (40 px, larger type) are shared by every field, so fields in a row line up. The edge is a 1 px ring outside the box that darkens on hover; focus makes it full strength with a soft 4 px halo around it. Colour sits in one place: an error turns the ring red and fills the halo, always and also while focused, and colours the message; the label, fill, text and placeholder stay neutral. Disabled steps down in fill and text, never in opacity, and ignores hover. `label` and `error` draw the `Field` parts around the box: the label above it, the message under it with an alert mark. `prefix` and `suffix` are filled cells inside the ring, divided from the text, with 14 px marks (8 px of padding at `sm`); the suffix reads in the text colour. `look="plain"` sets both on the box\'s own fill with no divider, flush against the text. `rounded` makes the box a pill. `className` lands on the box. "Validate on blur, not on every keystroke; surface the message by passing a string to `error`." (https://vercel.com/geist/input) "Trim leading and trailing whitespace before submit so ` example.com` and `example.com` resolve to one value." (https://vercel.com/geist/input) "Keep the field focusable while saving; pair `disabled` with a spinner only when input is impossible." (https://vercel.com/geist/input) "Don’t wrap a labelled `<Input>` in a `Tooltip`; put the explainer on a sibling icon button so the label stays announced." (https://vercel.com/geist/input) "Helper text is sentence case, one sentence with a trailing period, on a sibling element wired through `aria-describedby`." (https://vercel.com/geist/input) Ours: help is the one-line description of the card or form around the field, not a line under it. Nothing spins while a field saves. Only the forms `Field` names trim.',
355
+ words: '"Labels are short Title Case nouns: `Project Name`, `Domain`, `Environment Variable Name`." (https://vercel.com/geist/input) "Placeholders show an example value (`my-awesome-project`, `example.com`), never instructions like `Enter your project name`." (https://vercel.com/geist/input) "Validation names the field and the constraint, ends in a period, and skips `please` (`Project name is required.`, `Code must be 6 digits.`)." (https://vercel.com/geist/input) Ours: the label is a short noun in sentence case with no colon, not Title Case, and the example comes from our catalogue (*Bizeta Arredamenti*).',
356
+ access: '"Passing a string to `label` requires `id`; the `InputPropsWithStringLabelAndId` union won’t compile without it and screen readers lose the association." (https://vercel.com/geist/input) Ours: a field whose name is drawn elsewhere (a toolbar\'s search, a table cell) takes `aria-label` or `aria-labelledby` instead, and one with no name does not compile. The error is joined through `aria-describedby`. Use the right `type`, `autocomplete` and `inputMode`. "For icon-only affordances inside a row of inputs, use `<Button shape="circle" svgOnly aria-label="…">` rather than an unlabeled icon." (https://vercel.com/geist/input)',
335
357
  },
336
358
  {
337
359
  name: "InputGroup",
338
360
  module: "input-group",
339
361
  family: "Inputs",
340
362
  summary: "One input box with something fixed inside it: a mark, a prefix, a button.",
341
- when: "A single-line value with something fixed inside the box: a search mark, a clear button, a prefix like *https://*, a copy button.",
342
- instead: "A collection's search is `CollectionSearch`, never a hand-built group. A plain value is `Input`. Many known values to filter is `Combobox`.",
343
- behaviour: "The group draws the edge and the focus outline for the input inside it. The clear button empties the value and puts focus back in the box. Escape clears only while the box holds the interaction and has a value; otherwise it closes the overlay as usual.",
344
- words: "Prefix and suffix are literal text, never the label. An inline button names what it does: *Clear search*, *Copy address*.",
345
- access: '`role="group"` around the box. The input inside keeps its own label. Inline buttons are named: *Clear search*, *Copy address*.',
363
+ when: "A single-line value with a button inside the box: a copy button, a block addon, a textarea with a toolbar, a server-given value (a project ID) shown read-only in mono beside its copy button, with no Save.",
364
+ instead: "A prefix or suffix of text or an icon (*https://*, *.com*), in a cell or plain, is the `prefix`, `suffix` and `look` props of `Input`. A value that clears itself is `ClearableInput`; a collection's search is `CollectionSearch`, never a hand-built group. Many known values to filter is `Combobox`.",
365
+ behaviour: "The group draws the edge and the focus halo for the input inside it, so the ring wraps the addons too, and takes the sizes of `Input`: the input follows the group's. An addon is a `segment` (a filled cell with a divider, the look of `Input`'s prefix and suffix) or `plain` (a mark on the box's own fill, flush against the text). An icon button in an addon fills the row, sits flush against the edge, is muted until hovered and never fills on hover or press. A disabled input steps the whole group down in fill and text, never in opacity, an icon button included; a button in an addon that is held, such as a send button waiting for text, leaves the box as it is. Colour sits where it does on `Input`: only an error reddens the ring; a suffix reads in the text colour in both looks, a prefix in the muted grey. `rounded` makes the group a pill. The group's own `size` is the only place a size is set.",
366
+ words: "Prefix and suffix are literal text, never the label. An inline button names what it does: *Copy address*.",
367
+ access: '`role="group"` around the box. The input inside is named by `aria-label` or `aria-labelledby`. "For icon-only affordances inside a row of inputs, use `<Button shape="circle" svgOnly aria-label="…">` rather than an unlabeled icon." (https://vercel.com/geist/input) Ours: an inline button carries its `aria-label`.',
346
368
  },
347
369
  {
348
370
  name: "InputOTP",
@@ -360,11 +382,11 @@ export const USAGE = [
360
382
  module: "label",
361
383
  family: "Inputs",
362
384
  summary: "The visible name of one control.",
363
- when: "A control standing on its own that needs a visible name: a checkbox's sentence, a switch's setting.",
364
- instead: "A control inside a form takes `FieldLabel` through `Field`; a group of controls takes `FieldLegend`. A short tag of metadata is `MetaLabel`.",
365
- behaviour: "Sits beside or above its control and dims with it when the control is disabled. Clicking it moves to its control.",
366
- words: "A noun or a positive statement in sentence case, no colon: *Approvals*, *Send a copy to the person who asked*.",
367
- access: "A `label` tied to its control by `htmlFor`. Never replaced by a placeholder or overridden by another `aria-label`.",
385
+ when: "\"Accessible text label for form controls.\" (https://vercel.com/geist/label) Ours: a control standing on its own that needs a visible name: a checkbox's sentence, a switch's setting, a bare control in a toolbar or a table cell.",
386
+ instead: "A text box, a textarea or a select names itself with its own `label`, so it never needs this. A group of controls takes `FieldLegend`. A short tag of metadata is a `Tag`.",
387
+ behaviour: "Above its control, 8 px clear of it, in 13 px type in the secondary grey, one step quieter than the control's own text. Beside a checkbox or a radio it is the sentence at 13 px in the text colour; beside a switch it is 12 px at medium weight. It stays neutral when the control is invalid. When its control is disabled it steps down to the placeholder grey, never in opacity. Clicking it moves to its control.",
388
+ words: '"Labels are short Title Case nouns: `Project Name`, `Domain`, `Environment Variable Name`." (https://vercel.com/geist/input) Ours: a noun or a positive statement in sentence case, no colon, kept as written (the label never recases its text, which is what Geist\'s `bypassCasing` asks for): *Approvals*, *Send a copy to the person who asked*. A placeholder never stands in for it.',
389
+ access: "A `label` tied to its control by `htmlFor` and the control's `id`, or wrapping it. Never overridden by another `aria-label`.",
368
390
  },
369
391
  {
370
392
  name: "NativeSelect",
@@ -372,54 +394,76 @@ export const USAGE = [
372
394
  family: "Inputs",
373
395
  summary: "The platform's own select, drawn as our input box.",
374
396
  when: "A long fixed list inside a native form, where the platform's own picker serves best: a time zone, a country.",
375
- instead: "Two or three options are radios or a segmented control. Up to about ten, drawn in our style, is `Select`. A list people type into is `Combobox`.",
376
- behaviour: "Same box as `Input`, the chevron 10 px from the edge in the muted grey. It opens the system's list, with native keyboard and typing. As an uncontrolled field it keeps a saved value instead of snapping back to the page's first default.",
377
- words: "The label names the thing chosen (*Time zone*). A placeholder option is distinct from a real one and never preselected as if chosen.",
378
- access: "A real `select` with a visible label; option groups are `optgroup`. The chevron is decorative.",
397
+ instead: 'Two or three options are radios or a segmented control. "Pick `<Select>` for short, fixed lists (under ~10 items) where typing adds nothing; switch to `Combobox` once filtering helps." (https://vercel.com/geist/select) Ours: `Select` draws the short list in our style.',
398
+ behaviour: "Same box, sizes, ring, hover, focus halo and disabled as `Input`, filling its container, the chevron in the muted grey with room reserved for it, after an optional `prefix` mark (a `suffix` mark takes the chevron's place). Colour sits in one place: an error turns the ring red and fills the halo, and colours the message; the fill, the value and the label stay neutral. `label` and `error` draw the `Field` parts around the box, and a string `label` requires `id`; a box with no name does not compile. It opens the system's list, with native keyboard and typing. As an uncontrolled field it keeps a saved value instead of snapping back to the page's first default. \"Validate on blur and pass a string to `error`; messages name the field and end in a period (`Select a framework.`).\" (https://vercel.com/geist/select) Other form rules are `Field`'s.",
399
+ words: '"Label is a short Title Case noun (`Framework`, `Region`); accept the prop directly on `<Select>`." (https://vercel.com/geist/select) "Options are Title Case when short and match canonical branding (`Next.js`, not `NextJS`); keep the same register across the list." (https://vercel.com/geist/select) Ours: the label names the thing chosen (*Time zone*) and options are sentence case. A placeholder option is distinct from a real one and never preselected as if chosen.',
400
+ access: 'A real `select` with a visible label tied by `id`, or `aria-label`. "Group options past ~10 items with native `<optgroup>`; Geist `<Select>` has no `.Group` static." (https://vercel.com/geist/select) Ours: option groups are `optgroup`. The chevron, the prefix and the suffix are decorative.',
379
401
  },
380
402
  {
381
403
  name: "SettingsField",
382
404
  module: "settings-section",
383
405
  family: "Inputs",
384
- summary: "One setting: its label and help above its control, and what went wrong under it.",
385
- when: "A value inside a SettingsSection: a name, an address, a choice, a switch.",
386
- instead: "A field of a drawer's or a dialog's form is a Field from the form's own owner; a read-only value is a fact in a SettingsFactsSection.",
387
- behaviour: 'The control is 20rem wide under its label and help by default; a switch row passes `orientation="horizontal"` and sits at the end of its label\'s row at its own width. A validation `error`, or a failed save about this field, shows under the control and marks it invalid.',
388
- words: 'The label is a noun for the value ("Display name"); the help is one line saying what it is for or where it shows; an error says what to do next.',
389
- access: "The control receives its id, `aria-invalid` and `aria-describedby` through `controlProps`, so the label, help and error are read with it.",
406
+ summary: "One setting: its label above its control, and what went wrong under it.",
407
+ when: "The one value of a SettingsSection: a name, an address, a choice, a switch.",
408
+ instead: "A field of a drawer's or a dialog's form is a Field from the form's own owner; a read-only fact is a SettingsFactsSection; several switches or selects in one card are SettingsRows; a switch that is the whole card and applies at once is the section's `headerSwitch`.",
409
+ behaviour: '"Helper text is sentence case, one sentence with a trailing period, on a sibling element wired through `aria-describedby`." (https://vercel.com/geist/input) Ours: label required, no help line: the card\'s description helps, and the (i) `info` is for a part of a value that needs a sentence. The control is 20rem wide under its label (`controlWidth`: `full` for a textarea, `natural` for a segmented control). `control="choice"` puts a checkbox or radio before its sentence; `control="switch"` puts the word for the switch\'s state after it and requires `stateLabel`. "Validate on blur, not on every keystroke; surface the message by passing a string to `error`." (https://vercel.com/geist/input) Ours: a validation `error` updates as the value is corrected, and it or a failed save about this field shows under the control and marks it invalid; colour sits on the control\'s edge and the message, never the label.',
410
+ words: '"Labels are short Title Case nouns: `Project Name`, `Domain`, `Environment Variable Name`." (https://vercel.com/geist/input) Ours: sentence case, a noun for the value ("Display name"). "Validation names the field and the constraint, ends in a period, and skips `please` (`Project name is required.`, `Code must be 6 digits.`)." (https://vercel.com/geist/input) Ours: an error says what to do next; `info.text` is one sentence and `info.label` names its button ("About display name"); a switch shows its state word ("Enabled", "Disabled") beside it.',
411
+ access: '"Don’t wrap a labelled `<Input>` in a `Tooltip`; put the explainer on a sibling icon button so the label stays announced." (https://vercel.com/geist/input) Ours: that sibling button is the (i) `info`. The control receives its id, `aria-invalid` and `aria-describedby` through `controlProps`, so the label, the info and the error are read with it. The info button has its own name and opens on focus.',
412
+ },
413
+ {
414
+ name: "SettingsRows",
415
+ module: "settings-section",
416
+ family: "Inputs",
417
+ summary: "Several switches, or several selects, in one card: each row its label on the left and its control on the right.",
418
+ when: "A card of settings of one kind, all switches or all selects, that the card's one description covers: a project's Git switches, a team's notification channels, a set of defaults.",
419
+ instead: "One setting is a SettingsField. Fields of different kinds, text boxes among them, are one card each. A switch that is the whole card and applies at once is the section's `headerSwitch`.",
420
+ behaviour: 'Declares `kind` once; every row is that kind, so a card cannot mix a switch with a select. Required on every row: the label and its `info` (an (i) with its sentence in a tooltip and on the control); no help line sits under a row. A switch row shows its state word beside the switch (`stateLabels`) and is controlled, so the word always follows the switch. Rows stand 16 px apart; below 28rem a row stacks its control under its label. "Pair with a form footer only when the setting needs an explicit `Save` step." (https://vercel.com/geist/toggle) Ours: a switch in a card with a Save is a draft the Save applies; in a card without one it applies at once through its `onCheckedChange`, and the card\'s footer holds only its line, if it has one.',
421
+ words: '"Title Case noun phrase, 1–4 words, naming what is true when ON: `Password Protection`, not `Enable Password Protection`." (https://vercel.com/geist/toggle) Ours: sentence case, a noun for what it controls ("Pull request comments"), the same for a switch as for a select. "Render an optional one-sentence description as a sibling under the label that explains ON only. Don’t describe OFF; it’s the negation." (https://vercel.com/geist/toggle) Ours: that sentence is `info.text`. "Options are Title Case when short and match canonical branding (`Next.js`, not `NextJS`); keep the same register across the list." (https://vercel.com/geist/select) Ours: a select\'s options are sentence case.',
422
+ access: "Each row's control is named by the row's label and described by its info and its error. The info button has its own name and opens on focus.",
390
423
  },
391
424
  {
392
425
  name: "Slider",
393
426
  module: "slider",
394
427
  family: "Inputs",
395
- summary: "A bounded number chosen by a thumb's position on a track.",
396
- when: "A bounded number where the position matters more than the digit: a limit, a share.",
397
- instead: "An exact number is `Input`; pair the two when precision counts. A few named levels are radios.",
398
- behaviour: "A 4 px track, the filled part in ink, a 12 px thumb edged in the input grey. Hover, drag and focus outline the thumb. Arrows step, Page Up and Down jump, Home and End reach the bounds. The thumb's touch target is 44 px.",
399
- words: "The label names the quantity; the current value and its unit are always written beside it (*€ 400*). The help states the bounds and the step.",
400
- access: '`role="slider"` with a name, min, max and `aria-valuetext` carrying the unit.',
428
+ summary: "A bounded number chosen by a thumb's position on a track, with its name and its live value.",
429
+ when: '"Use a slider for ranged numeric input where shape and proximity matter more than precision: bandwidth caps, opacity, audio volume, color channels." (https://vercel.com/geist/slider) Ours: a limit, a share.',
430
+ instead: '"For exact values like a port number or memory limit, pair the slider with a numeric `Input`; keyboard users default to typing." (https://vercel.com/geist/slider) Ours: `showEndInput`, and `showStartInput` on a range, add that box. A few named levels are radios.',
431
+ behaviour: '"Snap to a sensible step (`1`, `5`, `10%`) so dragging never produces values like `47.83291`." (https://vercel.com/geist/slider) "Clamp `min` and `max` to real product limits." (https://vercel.com/geist/slider) "Always render the live value next to the track in tabular nums, and label what the number represents (`Sample Rate · 44 kHz`)." (https://vercel.com/geist/slider) "The track alone is not self-describing." (https://vercel.com/geist/slider) "Threshold colors (warning, error tints past a limit) should match the same numeric breakpoint surfaced elsewhere in the UI; don’t invent a slider-only threshold." (https://vercel.com/geist/slider) Ours: `label`, `min`, `max` and `step` are required. The label sits above the track at 13 px in the secondary grey, with the live value beside it. The track is 8 px high and grey, the filled part is blue, and the thumb is a 6 by 14 px white bar; a disabled slider turns the filled part grey and leaves the rest. Colour: blue is the one hue, the filled part and nothing else. The thumb grows by a fifth while it is dragged (only the dragged one) or has keyboard focus, and hovering changes nothing. A range has two thumbs and cannot cross. A typed value is snapped to the step and kept inside the bounds when the box loses focus or Enter is pressed. `onValueCommitted` fires once when the change settles.',
432
+ words: "Ours: the label names the quantity: *Monthly limit*. The unit comes from `format`, so the value reads *€400*, not 400. The help under the slider states the bounds and the step.",
433
+ access: '"Give the slider an accessible name through a sibling `<label htmlFor>` or `aria-label`, and let the native arrow-key, Page Up/Down, Home/End behavior do the keyboard work." (https://vercel.com/geist/slider) "Don’t intercept those keys." (https://vercel.com/geist/slider) Ours: each thumb is `role="slider"` named by the label, with `aria-valuetext` carrying the unit from `format`. A single thumb and its box are named by the label; the two thumbs and the two boxes of a range by the label and their position, *Price 1* and *Price 2*.',
401
434
  },
402
435
  {
403
436
  name: "Textarea",
404
437
  module: "textarea",
405
438
  family: "Inputs",
406
439
  summary: "Several lines of free text that grow with their content.",
407
- when: "Several lines of free text: an Agent's instructions, a description, notes.",
408
- instead: "One line is `Input`. Code or a value to copy is `CodeBlock`, read-only.",
409
- behaviour: "At least 64 px, then it grows with its content (`field-sizing: content`). The same edge, focus, invalid and disabled as `Input`. Whitespace is kept exactly; nothing trims it. A character count shows only when a real limit exists.",
410
- words: "The label names the content (*Instructions*); the help says who reads it and when. The placeholder is a short example, not a paragraph.",
411
- access: "A visible label tied by `id`; help and error through `aria-describedby`. In a dialog, the footer's Save stays reachable while the box grows.",
440
+ when: '"Retrieve multi-line user input." (https://vercel.com/geist/textarea) "Pick `<Textarea>` for content that wraps to multiple lines (commit messages, descriptions, notes); use `Input` for a single value like a name or domain." (https://vercel.com/geist/textarea)',
441
+ instead: "Ours: code or a value to copy is `CodeBlock`, read-only.",
442
+ behaviour: 'Same ring, halo, hover, disabled and error as `Input`; the red of an error sits on the ring, the halo and the message only, and holds while the box has focus. It grows with its content (`field-sizing: content`) from a roomy 100 px minimum with a 10 px by 12 px inset; `lg` takes larger type and the larger corner, `sm` is the default\'s size. Read-only keeps the plain fill: it does not look disabled. `label` and `error` draw the `Field` parts around the box, and a string `label` requires `id`, as on `Input`; a box with no name does not compile. "Set a generous default `rows` and grow only when the surface has vertical room; don’t let the field push primary actions below the fold." (https://vercel.com/geist/textarea) Ours: there is no resize handle and `rows` does not set the height. Nothing caps the growth, so a form keeps its footer\'s primary action in view and lets the body scroll. "Validate on blur and pass a string to `error` to render the inline message; the validation replaces helper text on failure." (https://vercel.com/geist/textarea) "Trim leading and trailing whitespace on submit so empty newlines don’t pass a `required` check." (https://vercel.com/geist/textarea) Ours: typing keeps every space and newline; the save of `FormDialog`, `FormDrawer`, `RowEditForm` and `SettingsSection` trims the ends (see `Field`). A character count shows only when a real limit exists.',
443
+ words: '"Labels are short Title Case nouns (`Description`, `Release Notes`); placeholders show an example value, not instructions like `Enter a description`." (https://vercel.com/geist/textarea) "Validation names the field and constraint, ends in a period, and skips `please` (`Description is required.`, `Release notes can’t exceed 500 characters.`)." (https://vercel.com/geist/textarea) "Render helper text as a sibling `<p>` linked through `aria-describedby`; sentence case, one sentence, with a period." (https://vercel.com/geist/textarea) Ours: the label names the content (*Instructions*) in sentence case. Help is the one-line description of the card or form around the box, not a `<p>` under it. The placeholder is a short example, not a paragraph.',
444
+ access: "A visible label tied by `id`, or `aria-label`; the error through `aria-describedby`. In a dialog, the footer's Save stays reachable while the box grows.",
412
445
  },
413
446
  {
414
447
  name: "Checkbox",
415
448
  module: "checkbox",
416
449
  family: "Choices",
417
450
  summary: "A box that is checked, unchecked or mixed.",
418
- when: "Independent yes-or-no choices in a form, an acknowledgment before a step, selecting rows.",
419
- instead: "A setting that is on or off is a `Switch`. One of several is a `RadioGroup`. Several values from a long list is a multi-select.",
420
- behaviour: "Unchecked is only the input-grey edge, showing the surface beneath. Checked fills with ink and a 14 px check; mixed shows a minus and is worked out from the rows below. The label toggles it too, and on touch the target is 44 px. A required acknowledgment is checked on submit.",
421
- words: "The label says what is true when checked, positively: *Send a copy to the person who asked*, not *Don't send*. A related set gets a legend.",
422
- access: '`role="checkbox"` with `aria-checked` true, false or mixed. A row\'s box is named for its record: *Select pec@acme.it*. A set sits in a `FieldSet` with its legend.',
451
+ when: '"Multi-select inside a list, like table-row pickers, multi-pick filters, and opt-in preference groups." (https://vercel.com/geist/checkbox) "Acknowledgments where the user must affirm a specific statement (terms of service, irreversible export)." (https://vercel.com/geist/checkbox)',
452
+ instead: '"For a single boolean setting like dark mode or password protection, use `Toggle`." (https://vercel.com/geist/checkbox) "The on/off mechanic is clearer there than a lone checkbox." (https://vercel.com/geist/checkbox) Ours: Geist\'s `Toggle` is our `Switch`. One of several exclusive options is a `RadioGroup`, and one that needs a description is a `ChoiceboxGroup`. Several values from a long list is a multi-select.',
453
+ behaviour: '"Indeterminate is a visual state, not a third value." (https://vercel.com/geist/checkbox) "Drive it from a parent that knows partial selection, and clear it as soon as every child is fully checked or unchecked." (https://vercel.com/geist/checkbox) "Validation on a required acknowledgment fires on submit, not on blur, so checking and unchecking shouldn’t flash an error." (https://vercel.com/geist/checkbox) "Disabled checkboxes still need a Tooltip naming the reason; a greyed box with no explanation reads as a bug." (https://vercel.com/geist/checkbox) Ours: named by `aria-label`, `aria-labelledby` or an `id` that a `FieldLabel` points at; a box with none does not compile. It is 16 px with a 4 px corner, the label 8 px away at 13 px regular weight on a 20 px row, and the label is part of the target. Colour: only a checked box fills, in ink; an unchecked box shows the surface and greys on hover. Mixed is an unfilled box with a grey dash. A disabled box turns grey, an unchecked or mixed one with a lighter edge and a checked one mid grey, and ignores hover.',
454
+ words: '"Group label above a `<fieldset>` is a Title Case noun like `Notifications` or `Required Permissions`. No trailing colon." (https://vercel.com/geist/checkbox) "Acknowledgment label is a full sentence ending in a period: `I agree to the Terms of Service.`" (https://vercel.com/geist/checkbox) "Indeterminate copy names the partial count next to the group label (`3 of 5 selected`). Never leave the dash state unlabeled." (https://vercel.com/geist/checkbox) Ours: sentence case, not Title Case. The label says what is true when checked, positively: *Send a copy to the person who asked*, not *Don\'t send*.',
455
+ access: '"Wrap related checkboxes in `<fieldset>` with a `<legend>` so screen readers announce the group name before each option." (https://vercel.com/geist/checkbox) "Row-select checkbox in a table has no visible label. Set `aria-label="Select {row name}"` so the row stays identifiable out of context." (https://vercel.com/geist/checkbox) "The click target already extends to the label. Don’t override the `<label>`/`htmlFor` association with a custom wrapper that breaks the click region." (https://vercel.com/geist/checkbox) Ours: `role="checkbox"` with `aria-checked` true, false or mixed. A row\'s box is named for its record: *Select pec@acme.it*. A set sits in a `FieldSet` with its legend.',
456
+ },
457
+ {
458
+ name: "ChoiceboxGroup",
459
+ module: "choicebox",
460
+ family: "Choices",
461
+ summary: "A group of larger tiles, each a radio or a checkbox with a title and a description.",
462
+ when: '"A choice that benefits from a larger tap target plus a description or icon, like a framework picker, plan comparison, or deployment region with latency." (https://vercel.com/geist/choicebox) "Single-select for mutually exclusive choices, multi-select for additive ones." (https://vercel.com/geist/choicebox) "Don’t mix the two within one group." (https://vercel.com/geist/choicebox) "Cap at 4–6 tiles." (https://vercel.com/geist/choicebox) Ours: where a Library entry applies, an API key\'s access level.',
463
+ instead: '"Past that, switch to `Select` or `Combobox` so the page doesn’t scroll for a single field." (https://vercel.com/geist/choicebox) "For plain text labels with no description, use `Radio`." (https://vercel.com/geist/choicebox) Ours: Geist\'s `Radio` is our `RadioGroup`, or a `Checkbox` for several. Two or three views of one surface are a `SegmentedControl`.',
464
+ behaviour: '"The whole tile is the click and focus target; tapping anywhere inside selects it." (https://vercel.com/geist/choicebox) "Don’t place nested buttons or links inside a tile that would steal the click." (https://vercel.com/geist/choicebox) "Selected state shows a check or filled dot in the corner." (https://vercel.com/geist/choicebox) "The border highlight alone isn’t enough on low-contrast screens." (https://vercel.com/geist/choicebox) "Disabled tiles need a Tooltip naming why (`Available on Pro`)." (https://vercel.com/geist/choicebox) "A faded tile with no reason reads as broken." (https://vercel.com/geist/choicebox) Ours: the group has a `label` and a `type`, and every tile a `title` and a `description`; a group or a tile without them does not compile. Tiles share one row and wrap to a column on a narrow screen; `orientation="vertical"` stacks them. They are 12 px apart, each with 12 px inside and 24 px between its words and its corner mark, the mark centred against the words. Colour: a selected, available tile is the one place blue sits on a choice, as a blue border, a blue tint behind the title and description, blue words, and a blue corner mark; a selected tile that is unavailable stays grey, never a faded blue. An unselected tile stays neutral and takes a grey fill on hover. `children` of a tile appear below its tinted row while it is selected. A tile, or the whole group, is unavailable only with a `disabledReason`, written under the tile\'s description or the legend, and only its title greys. A tile may carry `specs`, grey chips under its title, and one `badge` beside it for the option to prefer (*Recommended*); both are grey `secondary` badges, because blue marks only the selected tile.',
465
+ words: '"Titles are parallel: one Title Case title plus one sentence-case description per tile, ending in a period." (https://vercel.com/geist/choicebox) "Don’t restate the title in the description." (https://vercel.com/geist/choicebox) "The description adds the differentiator (`$20/mo · 100 GB bandwidth`), not a synonym." (https://vercel.com/geist/choicebox) "Icons are decorative when paired with a title; if the icon is the only label, give the tile an `aria-label` naming the choice." (https://vercel.com/geist/choicebox) Ours: the title is a short noun phrase in sentence case: *Pro trial*; the description is *Free for two weeks.* The label names the choice: *Plan*.',
466
+ access: '"Tiles render as radios or checkboxes under the hood, so keep them inside a `<fieldset>` with a `<legend>` so screen readers announce the group." (https://vercel.com/geist/choicebox) "Arrow keys move within a single-select group, Space toggles in multi-select." (https://vercel.com/geist/choicebox) "Don’t override those keys with custom handlers." (https://vercel.com/geist/choicebox) "Color is not the selection signal." (https://vercel.com/geist/choicebox) "Pair the highlight border with the corner check so colorblind users still see what’s active." (https://vercel.com/geist/choicebox) Ours: a `FieldSet` with the label as its legend, which names a group of checkboxes; a `role="radiogroup"` is labelled by the same legend. Each tile is named by its title and described by its description and reason. Keyboard focus outlines the whole tile, and the box in its corner draws no outline of its own.',
423
467
  },
424
468
  {
425
469
  name: "CollectionSort",
@@ -437,11 +481,11 @@ export const USAGE = [
437
481
  module: "combobox",
438
482
  family: "Choices",
439
483
  summary: "Type to find one value, or several, in a known set.",
440
- when: "Typing to find in a known set: assign an Agent, choose a time zone, add several Agents at once with `multiple`.",
441
- instead: "A short fixed list is a `Select`. Free text is an `Input`. Narrowing a collection is `CollectionFilters`.",
442
- behaviour: "Typing filters the list. Loading and no match are different states; the chosen value stays while results load. Enter picks the highlighted row and never submits the form around it. Clear empties the box and keeps focus there. With `multiple`, each choice is a chip with its own remove; Backspace in an empty box removes the last.",
443
- words: "The hint is scoped: *Find an Agent*. No match quotes the query: *No Agent matches “Xy”.* A chip reads as the record's name.",
444
- access: "A visible label; combobox and listbox roles; each chip's remove is named: *Remove Acme Quotes*. An Agent's orb in a row is a decorative mark beside her name.",
484
+ when: '"Filters large lists to selectable options based on the matching query." (https://vercel.com/geist/combobox) "Pick `<Combobox>` when users type to filter a known list of values (regions, frameworks, env-var names)." (https://vercel.com/geist/combobox) Ours: assign an Agent, choose a time zone, add several Agents at once with `multiple`.',
485
+ instead: '"For short, fixed lists where typing adds nothing, switch to `Select`." (https://vercel.com/geist/combobox) "When more than one value can be chosen at once, use `MultiSelect`." (https://vercel.com/geist/combobox) "For free-form filter strings that don’t resolve to a single option, drop in `Input` with the `search` variant." (https://vercel.com/geist/combobox) Ours: several values chosen from a list you scan rather than type into is a `MultiSelect`; `Combobox` takes several with `multiple`. `Input` has no `search` variant, so free text is an `Input` and narrowing a collection is `CollectionFilters`.',
486
+ behaviour: 'The box is `Input`\'s: its three sizes (`size` on `ComboboxInput`), its ring, hover, focus halo and disabled, with the search mark at the start, a `placeholder` that names the scope, and the open-list or clear button flush at the end, in the muted grey until hovered (the open-list button is 30 px high and centred at the default size, full height at `sm` and `lg`). Colour sits in one place: an error turns the ring red, fills the halo and colours the typed text; the placeholder, marks and list stay neutral. `label` and `error` go on `ComboboxInput`, and a string `label` requires `id`; a box with no name does not compile. The list opens 8 px under it at its width (a narrower `className` width centres under the box), in a 6 px inset, up to five and a half rows tall (210 px) so a cut row shows there is more, with rows 36 px tall and no gap. A row is one line that ends in an ellipsis, with a 16 px mark 12 px before the name and a 14 px check at the chosen row\'s end, after the name. A row that cannot be chosen turns its text muted, never dimmed. Typing filters the list. "Options can opt out of the fixed row height with `ignoreDefaultHeight` to render multi-line content (e.g. a shared environment variable name plus its target). The list sizes each row to its content and stays scrollable." (https://vercel.com/geist/combobox) Ours: there is no `ignoreDefaultHeight`; a two-line row is an explicit column child. "Show a loading state for async results; don’t collapse the list while the request is in flight." (https://vercel.com/geist/combobox) Ours: while results load the list stays and `ComboboxLoading` says so, and the chosen value stays; `ComboboxContent` requires `empty`, the sentence for no match, or `null` while a search is still running. "Render a custom empty state of the form `No {items} match "{query}"` rather than a bare `No results`." (https://vercel.com/geist/combobox) "Keep arrow-key navigation through `<Combobox.Option>` items; don’t hijack Enter to submit the surrounding form while the list is open." (https://vercel.com/geist/combobox) "Set `clearable` to show a clear button when a value is selected." (https://vercel.com/geist/combobox) Ours: `showClear` with a `clearLabel` does it; clear empties the box and keeps focus there. With `multiple`, each choice is a chip with its own remove; Backspace in an empty box removes the last. "It is common to use Combobox inside a Modal. On mobile, the Modal automatically renders a Dialog instead." (https://vercel.com/geist/combobox) "Inside a `Modal`, Geist switches to a Dialog on mobile automatically; don’t layer a second portal." (https://vercel.com/geist/combobox) Ours: our overlay is `Dialog`, and `ComboboxContent` is the list\'s one portal.',
487
+ words: '"Visible label is a short Title Case noun (`Region`, `Environment Variable Name`)." (https://vercel.com/geist/combobox) "Placeholder is the inline hint: `Search regions`, `DATABASE_URL`. Never bare `Search…` and never the label restated." (https://vercel.com/geist/combobox) "Option text is Title Case for short values and matches canonical branding (`Next.js`, not `NextJS`); same register across the list." (https://vercel.com/geist/combobox) "Validation names the field and constraint, sentence case with a period (`Select a region.`)." (https://vercel.com/geist/combobox) Ours: the label and the rows are sentence case. The hint is scoped: *Find an Agent*. No match quotes the query: *No Agent matches “Xy”.* A chip reads as the record\'s name.',
488
+ access: '"`<Combobox>` has no `label` prop; pair a sibling `<Label htmlFor>` with the root `id`, or set `aria-label` on the root for icon-only triggers." (https://vercel.com/geist/combobox) "The root accepts `aria-label` only; there is no `aria-labelledby` prop, so the sibling-label path is the way to reference visible text." (https://vercel.com/geist/combobox) "Trap focus inside the popover when nested in a `Modal` so Tab cycles options instead of the page behind it." (https://vercel.com/geist/combobox) Ours: `ComboboxInput` takes a visible `label` tied to the box by `id`, or `aria-label` or `aria-labelledby`; combobox and listbox roles; the open-list and clear buttons are named by `triggerLabel` and `clearLabel`; each chip\'s remove is named: *Remove Acme Quotes*. An Agent\'s orb in a row is a decorative mark beside her name.',
445
489
  },
446
490
  {
447
491
  name: "ModelSelect",
@@ -454,16 +498,27 @@ export const USAGE = [
454
498
  words: "Model names come from `modelDisplayName`, or the product's own `nameOf` when its runtime names models itself. The placeholder says what to choose: *Choose a model*.",
455
499
  access: "The trigger takes a visible label; listbox and option roles from Base UI; the marks are decorative beside the names.",
456
500
  },
501
+ {
502
+ name: "MultiSelect",
503
+ module: "multi-select",
504
+ family: "Choices",
505
+ summary: "A menu of checkbox rows that chooses several values from a known list, with the count in its trigger.",
506
+ when: '"A keyboard-navigable dropdown for selecting multiple items with advanced focus management." (https://vercel.com/geist/multi-select) "Pick `<MultiSelect>` when users pick more than one value from a known list (regions, scopes, tags)." (https://vercel.com/geist/multi-select) Ours: also the facets of a collection.',
507
+ instead: '"For a single value from a short list, use `Select`." (https://vercel.com/geist/multi-select) "When typing to filter is more important than seeing every option at once, use `Combobox`." (https://vercel.com/geist/multi-select) "Skip `<MultiSelect>` for boolean settings; `Toggle` handles those." (https://vercel.com/geist/multi-select) Ours: `Combobox` takes several values with `multiple`; a yes-or-no setting is a `Switch`, never a one-row multi-select.',
508
+ behaviour: '"Use controlled mode when state lives in the URL or syncs to the server so the trigger label and stored value stay in lockstep." (https://vercel.com/geist/multi-select) "Use controlled state to manage selections programmatically." (https://vercel.com/geist/multi-select) Ours: the caller always owns the selection and passes it back through each row, so the trigger and the stored value (the URL, the server) never disagree; the root takes the same `selectedCount` and `totalCount` the trigger\'s summary is built from. "Show the selected count in the trigger (`3 regions selected`); show the single name when only one is picked." (https://vercel.com/geist/multi-select) The trigger is `Input`\'s box at its default size, in regular weight, with the chevron at the end (it turns up while the list is open); it keeps the hover edge while the list is open and takes the page\'s outline for keyboard focus, as a button does. Everything is neutral: a checked box is ink, never blue, and an error turns only the trigger\'s ring red. `label` and `error` go on `MultiSelectTrigger`, and a string `label` requires `id`; a trigger with no name does not compile. The list opens 8 px under the trigger at least as wide as it, in an 8 px inset, with rows 28 px tall and no gap, and stops 16 px short of the screen\'s edge (at most 24rem tall, then it scrolls). A row is the checkbox in a 28 px square that fills on hover, then a button holding the row\'s name; pressing the name runs the button, pressing the box toggles the row. "**Checkbox focus + Enter/Space**: Toggle individual item" (https://vercel.com/geist/multi-select) "**Button focus + Enter/Space**: Smart selection (Select Only, Select All, or Toggle based on context)" (https://vercel.com/geist/multi-select) "**Hidden action labels**: Appear on hover/focus to show available actions" (https://vercel.com/geist/multi-select) Ours: the button\'s label reads *Select all* on a chosen row while some rows are unchosen, and *Select only* on every other row; it shows on hover and focus of the row, and always on touch. "Keep checkbox focus and button focus distinct so Up/Down navigates rows and Left/Right toggles between the row’s checkbox and action button." (https://vercel.com/geist/multi-select) "**Up/Down arrows**: Navigate between rows while maintaining checkbox/button focus state" (https://vercel.com/geist/multi-select) "**Left/Right arrows**: Switch between checkbox and button focus within the current row" (https://vercel.com/geist/multi-select) "**Tab**: Focus away from the menu (natural tab behavior)" (https://vercel.com/geist/multi-select) "**Enter/Space**: Execute actions based on current focus (checkbox toggle or button click)" (https://vercel.com/geist/multi-select) "Trap focus inside the menu while it’s open and return focus to the trigger on close." (https://vercel.com/geist/multi-select) Ours: focus moves into the list when it opens; Tab leaves it in order. "For empty filters, render `No {items} match "{query}"` rather than `No results`." (https://vercel.com/geist/multi-select) Ours: there is no filter box, so a list that needs one is a `Combobox` with `multiple`.',
509
+ words: 'The trigger says how many are chosen in the product\'s words: the name when exactly one is chosen, *No regions selected* when none. It is never empty. Rows are sentence case names. "Announce bulk actions (`Select All`, `Select Only`) through the visible button label so the action matches what the screen reader speaks." (https://vercel.com/geist/multi-select) Ours: the two button labels come from the caller, in sentence case.',
510
+ access: '"Each row checkbox needs an `aria-label` that names the item (`Select us-east-1`); a bare `Select` is unanchored for screen readers." (https://vercel.com/geist/multi-select) Ours: each row\'s checkbox is named by the row\'s name; each button is named by its label and the row (*Select only us-east-1*). "The trigger button needs a stable accessible name even when zero items are selected; don’t rely on the placeholder alone." (https://vercel.com/geist/multi-select) Ours: the trigger is a button with `aria-expanded`, named by its visible label tied by `id`, or by `aria-label` or `aria-labelledby`; the summary describes it.',
511
+ },
457
512
  {
458
513
  name: "RadioGroup",
459
514
  module: "radio-group",
460
515
  family: "Choices",
461
516
  summary: "One option of a few, all shown at once.",
462
- when: "One of 2 to 6 options that exclude each other, all shown at once.",
463
- instead: "Options that each need a sentence are choice tiles. Seven or more is a `Select`; a list to search is a `Combobox`. Another view of the same thing is `Tabs`.",
464
- behaviour: "One tab stop for the group; the arrows move and choose. The chosen circle fills with ink and a white 8 px dot. Preselect the safe option when there is one; leave a consequential choice empty until the person makes it.",
465
- words: "Options are parallel phrases in sentence case with no end punctuation: *Only me*, *Everyone in the company*. The legend names the setting; it does not ask a question.",
466
- access: '`role="radiogroup"` named by its legend, each radio by its label. A disabled option shows why in its own help line.',
517
+ when: '"A single choice from 2–6 mutually exclusive options where seeing every option matters (deploy regions, plan tiers, billing cycle)." (https://vercel.com/geist/radio) Ours: a plan\'s billing cycle, who can see an Agent.',
518
+ instead: '"Past 6 options, switch to `Select` or `Combobox` so the list doesn’t dominate the form." (https://vercel.com/geist/radio) "For binary on/off, use `Toggle`." (https://vercel.com/geist/radio) "For richer per-option content (icon, description, badge), use `Choicebox`." (https://vercel.com/geist/radio) Ours: Geist\'s `Toggle` is our `Switch` and its `Choicebox` our `ChoiceboxGroup`. Two or three views of one surface are a `SegmentedControl`. Another view of the same thing is `Tabs`.',
519
+ behaviour: '"Pre-select the safest default so the field reads as configured, never as required-but-empty." (https://vercel.com/geist/radio) "Skip the default only when the choice has real consequences and you want a deliberate pick." (https://vercel.com/geist/radio) "Required state goes on the `RadioGroup`, not on individual options." (https://vercel.com/geist/radio) "Required-against-a-single-radio is meaningless." (https://vercel.com/geist/radio) "Arrow keys move selection within the group and skip disabled options." (https://vercel.com/geist/radio) "Tab moves to the next field, not the next radio." (https://vercel.com/geist/radio) "Disabled options need a Tooltip naming why (`Available on Pro and Enterprise`)." (https://vercel.com/geist/radio) "A greyed-out radio with no reason reads as broken." (https://vercel.com/geist/radio) Ours: the group is named by `aria-label` or `aria-labelledby`, and each `RadioGroupItem` by `aria-label`, `aria-labelledby` or an `id` that a `FieldLabel` points at; one without a name does not compile. Options are 16 px circles 24 px apart (a dense call site passes its own `gap`), each label 8 px away at 13 px regular weight on a 20 px row. Colour: the circle is unfilled, and the chosen one darkens its edge and shows an 8 px ink dot; nothing takes a hue, and a disabled option greys edge, dot and label together, with no hover change. The arrows also choose. The reason for a disabled option is written in its own help line.',
520
+ words: '"Group label is a Title Case noun like `Deployment Region` or `Billing Cycle`." (https://vercel.com/geist/radio) "Render it via `<legend>` or a sibling label tied with `aria-labelledby`." (https://vercel.com/geist/radio) "Option labels are parallel: same part of speech, same length range, same register." (https://vercel.com/geist/radio) "`Monthly` / `Yearly`, not `Monthly` / `Pay yearly`." (https://vercel.com/geist/radio) Ours: sentence case with no end punctuation: *Only me*, *Everyone in the company*. The legend names the setting; it does not ask a question.',
521
+ access: '"Wrap related radios in `<fieldset>` + `<legend>` so screen readers announce the group name before each option." (https://vercel.com/geist/radio) "The standalone unlabeled radio (custom UI) needs an `aria-label` describing the choice." (https://vercel.com/geist/radio) "Never ship a radio with no accessible name." (https://vercel.com/geist/radio) "Don’t replace the native focus ring with a CSS hack that drops outline-offset; keyboard users lose track of which option is focused." (https://vercel.com/geist/radio) Ours: `role="radiogroup"` inside a `FieldSet`. The legend names the fieldset, not the group, so point `aria-labelledby` at the `FieldLegend`\'s id.',
467
522
  },
468
523
  {
469
524
  name: "SegmentedChoice",
@@ -471,32 +526,54 @@ export const USAGE = [
471
526
  family: "Choices",
472
527
  summary: "One choice among a few short levels, drawn as segments that share a row.",
473
528
  when: "One of two to five short levels a person compares at a glance, such as an Agent's reasoning: Default, Low, Medium, High.",
474
- instead: "Options that each need a sentence are choice tiles. Seven or more is a `Select`. The theme is `ThemeModeSwitcher`.",
529
+ instead: "Two or three views of one surface are a `SegmentedControl`. Options that each carry a description are a `ChoiceboxGroup`. Seven or more is a `Select`. The theme is `ThemeModeSwitcher`.",
475
530
  behaviour: "A `RadioGroup` drawn with the toggle classes: one outline, one seam between members, the chosen one in the selection fill. Arrows move and choose; it applies through the form it sits in.",
476
531
  words: "Each segment is one or two words of equal weight, in sentence case.",
477
532
  access: "A `radiogroup` named by its label; each segment a radio named by its word.",
478
533
  },
534
+ {
535
+ name: "SegmentedControl",
536
+ module: "segmented-control",
537
+ family: "Choices",
538
+ summary: "Two or three options that show different views of one surface, the chosen one filled.",
539
+ when: '"Use Switch as a segmented selector for 2–3 mutually exclusive options that show different views of the same surface, like `Source` vs `Output`." (https://vercel.com/geist/switch) Ours: Geist\'s `Switch` is our `SegmentedControl`.',
540
+ instead: '"For a boolean on/off setting, use `Toggle`." (https://vercel.com/geist/switch) "Switch implements radio semantics, so two options stay mutually exclusive rather than reading as a checkbox." (https://vercel.com/geist/switch) "Past 3 options or when labels grow beyond a couple of words, switch to `Tabs` or a `Select`." (https://vercel.com/geist/switch) Ours: Geist\'s `Toggle` is our `Switch`. The theme is `ThemeModeSwitcher`, never a second picker. Four to six that must be compared are a `RadioGroup`. Pressed buttons that can be several or none are a `ToggleGroup`. Panels of one object are `Tabs`.',
541
+ behaviour: '"Pass a `name` so the underlying radios are grouped; without it, more than one option can appear selected at once." (https://vercel.com/geist/switch) "Set `defaultChecked` (or controlled `checked`) on exactly one `Switch.Control` so the group has a defined initial state." (https://vercel.com/geist/switch) "Pad each `Switch.Control` so the widest label fits without the active pill resizing on selection." (https://vercel.com/geist/switch) "Test with the longest copy in the set." (https://vercel.com/geist/switch) Ours: the group\'s name (`aria-label` or `aria-labelledby`), a label on every option and a starting choice (`defaultValue` or `value`) are required; without them it does not compile. The options are one radio group, so they stay exclusive without a form `name`. The options share the width of the widest, so the fill never resizes as the choice moves; where the row is narrower than that, each option keeps its own word\'s width rather than cutting it. The track is white with a hairline and a 4 px inset, so it is 32, 36 or 40 px high around options of 24, 28 or 32 px. Colour: neutral throughout, the chosen option filled grey with ink words and the others grey words that turn ink on hover; nothing takes a hue. Icons are 16 px, 20 px at `lg`. A disabled option keeps the chosen fill, lightens only its words and has no hover change. The options are radios in one group: arrows move and choose, and pressing the chosen one changes nothing. It applies at once; it is not a field that waits for a Save. `fullWidth` when it heads a form or a dialog, as the two-factor form\'s method switch does.',
542
+ words: '"Title Case each `Switch.Control` label." (https://vercel.com/geist/switch) "Keep labels to one or two words and parallel: `Source` / `Output`, not `Source` / `Show output`." (https://vercel.com/geist/switch) Ours: sentence case: *Source*, *Output*.',
543
+ access: '"Always provide a `label` on every `Switch.Control`, even when an `icon` carries the meaning, so screen readers announce the option." (https://vercel.com/geist/switch) "The component renders it as `geist-sr-only` for icon-only controls." (https://vercel.com/geist/switch) "Pair an icon-only Switch with a `Tooltip` on each control so sighted users get the same label assistive tech receives." (https://vercel.com/geist/switch) Ours: `role="radiogroup"` named by `aria-label` or `aria-labelledby`; each option a radio named by its label, shown as a tooltip on an icon-only option.',
544
+ },
479
545
  {
480
546
  name: "Select",
481
547
  module: "select",
482
548
  family: "Choices",
483
549
  summary: "A short fixed list in our style, opened from a trigger that shows the choice.",
484
- when: "Four to about ten fixed options in our style, each short enough to need no sentence: how often a task runs.",
485
- instead: "Two or three options are radios or a segmented control. A list to type into is a `Combobox`. Inside a native form a long list is `NativeSelect`.",
486
- behaviour: "The trigger shows the chosen value; the list floats under it with 12 px corners and a check at the chosen row's end. Arrows, typing, Enter, Escape. A placeholder reads in the muted grey and is never an option.",
487
- words: "Options are parallel phrases in sentence case: *Every hour*, *Every weekday*. The trigger never says *Select…*.",
488
- access: "A visible label tied to the trigger; listbox and option roles from Base UI; focus returns to the trigger on close.",
550
+ when: '"Display a dropdown list of items." (https://vercel.com/geist/select) "Pick `<Select>` for short, fixed lists (under ~10 items) where typing adds nothing; switch to `Combobox` once filtering helps." (https://vercel.com/geist/select) Ours: four or more options, each short enough to need no sentence: how often a task runs.',
551
+ instead: '"Use `MultiSelect` when more than one value can be chosen at once; use `Switch` for a 2–3 option segmented choice." (https://vercel.com/geist/select) Ours: two or three options are radios or a segmented control. Inside a native form a long list is `NativeSelect`.',
552
+ behaviour: 'The trigger is `Input`\'s box: its sizes, its ring, hover, focus halo and disabled, the label above it and the message under it. Colour sits in one place: an error turns the ring red and fills the halo, and colours the message; the fill, the value, the chevron and the label stay neutral. It shows the chosen value, after an optional `prefix` mark, with the chevron at the end in the muted grey (a `suffix` mark takes the chevron\'s place), and fills its container; it keeps the hover edge while its list is open. The list opens under it, 8 px away, never covering the trigger (`alignItemWithTrigger` is off), in a 6 px inset, with a 14 px check at the chosen row\'s end, rows 36 px tall and no gap between them; a row that cannot be chosen turns its text muted, never dimmed. Arrows, typing, Enter, Escape. A placeholder reads in the muted grey and is never an option. `label` and `error` go on `SelectTrigger`, and a string `label` requires `id`; a trigger with no name does not compile. "Group options past ~10 items with native `<optgroup>`; Geist `<Select>` has no `.Group` static." (https://vercel.com/geist/select) Ours: group the options with `SelectGroup`. "Validate on blur and pass a string to `error`; messages name the field and end in a period (`Select a framework.`)." (https://vercel.com/geist/select) "Don’t wrap a labelled `<Select>` in a `Tooltip`; put the hint on a sibling icon button so the label stays announced." (https://vercel.com/geist/select) Other form rules are `Field`\'s.',
553
+ words: '"Options are Title Case when short and match canonical branding (`Next.js`, not `NextJS`); keep the same register across the list." (https://vercel.com/geist/select) "Label is a short Title Case noun (`Framework`, `Region`); accept the prop directly on `<Select>`." (https://vercel.com/geist/select) "Placeholder is action-oriented (`Select a framework`), never `Choose one…`, `Pick`, or the label restated." (https://vercel.com/geist/select) Ours: options are parallel phrases in sentence case (*Every hour*, *Every weekday*), the label is a short noun in sentence case, and the trigger never says *Select…* or *Choose one*.',
554
+ access: "A visible label tied to the trigger by `id`, or `aria-label` or `aria-labelledby`. Listbox and option roles from Base UI; focus returns to the trigger on close. The prefix and suffix are decorative and not part of the trigger's name.",
489
555
  },
490
556
  {
491
557
  name: "Switch",
492
558
  module: "switch",
493
559
  family: "Choices",
494
560
  summary: "A setting that is on or off.",
495
- when: "A setting that is either on or off, inside a settings section.",
496
- instead: "Choosing or agreeing to something is a `Checkbox`. A tool button that stays pressed is a `Toggle`. Choosing between named values is a `RadioGroup`.",
497
- behaviour: "Off is the input grey, on is ink; the thumb travels 16 px. Space flips it. On a settings page the change applies at once and the product's toast confirms it. A failed change leaves the switch showing the true state.",
498
- words: "The label names what on means: *Approvals*, with help saying when. Never *Enable approvals*, and no On/Off text beside the switch.",
499
- access: '`role="switch"` with `aria-checked`. The name is the visible label and stays the same in both states. `sm` only inside a dense row.',
561
+ when: '"Use Toggle for a single boolean setting where ON takes effect immediately, like `Password Protection` or `Auto-Cancel Builds`." (https://vercel.com/geist/toggle) Ours: Geist\'s `Toggle` is our `Switch`, inside a settings section.',
562
+ instead: '"Switch to `Checkbox` for a multi-select list, or to `Switch` for 2–3 mutually exclusive views." (https://vercel.com/geist/toggle) Ours: Geist\'s `Switch` is our `SegmentedControl`. A tool button that stays pressed is a `ToggleGroup`. Choosing between named values is a `RadioGroup`.',
563
+ behaviour: '"`checked` is required and the component is controlled. Own the state and update it from `onChange`." (https://vercel.com/geist/toggle) "Persist on change and surface the result in a `useToasts().success(…)` toast (`Password protection enabled`); don’t leave the user wondering whether the flip stuck." (https://vercel.com/geist/toggle) "Pair with a form footer only when the setting needs an explicit `Save` step." (https://vercel.com/geist/toggle) "Disable Toggle only when the action is impossible (missing plan, locked policy); pair with helper text or a `Tooltip` that names the resolver." (https://vercel.com/geist/toggle) Ours: `checked` is optional, since a switch also runs uncontrolled with `defaultChecked`; the toast is raised with `showMessageToast`; the reason for a disabled switch is written in the card\'s footer, and it is left off. Always named: `aria-label` or `aria-labelledby`, or an `id` that a `FieldLabel`\'s `htmlFor` points at, and a switch with none of them does not compile. A bare `Switch` is the 28 by 14 px track with an 11 px thumb; `md` is 36 by 20 px and `lg` 40 by 24 px. Colour: off is the input grey, on is ink, and a disabled switch turns grey, on or off, with a greyer thumb and no shadow. Space flips it. On a settings page it applies at once and the product\'s toast confirms it, in one of two places: in the card\'s header at the right of the title when the switch is the whole card, the footer holding only its line, or at the end of a row of a card of several settings, the label to its left. A failed change leaves the switch showing the true state.',
564
+ words: '"The label is `children`, not a `label` prop. Title Case noun phrase, 1–4 words, naming what is true when ON: `Password Protection`, not `Enable Password Protection`." (https://vercel.com/geist/toggle) "Render an optional one-sentence description as a sibling under the label that explains ON only." (https://vercel.com/geist/toggle) "Don’t describe OFF; it’s the negation." (https://vercel.com/geist/toggle) "Keep `labelCasing="title"` (the default) so labels match other Title Case surfaces." (https://vercel.com/geist/toggle) "Use `labelCasing="normal"` only when sentence-case content sits inline next to the toggle." (https://vercel.com/geist/toggle) Ours: the label is 12 px at medium weight, in sentence case: *Approvals*, with the card\'s description in one sentence. The state word is a separate word beside the switch, not its name.',
565
+ access: '"Provide an accessible name through `children`, `aria-label`, or `aria-labelledby`." (https://vercel.com/geist/toggle) "Geist warns in development if all three are missing." (https://vercel.com/geist/toggle) "Set `aria-label` only when the visible label sits elsewhere on the row; otherwise let `children` carry it so sighted and screen-reader copy stay aligned." (https://vercel.com/geist/toggle) Ours: `role="switch"` with `aria-checked`, the name the same in both states; a missing name does not compile.',
566
+ },
567
+ {
568
+ name: "ThemeModeSegments",
569
+ module: "theme-mode-switcher",
570
+ family: "Choices",
571
+ summary: "The appearance as one small pill of three icons, for the corner of a page.",
572
+ when: "The appearance outside a settings page, where it must take little room: the corner of a sign-in page or a site's footer.",
573
+ instead: "On a settings page, where the choice has room for its names, it is ThemeModeSwitcher; inside a menu, ThemeModeMenu.",
574
+ behaviour: "One control of three icon segments, System, Light and Dark, the chosen one filled; choosing applies at once. Each segment shows its name as a tooltip.",
575
+ words: 'Each segment\'s name from the product\'s catalogue: "System", "Light", "Dark"; the control\'s name is the setting\'s: "Theme".',
576
+ access: "A radio group named by `aria-label`; each segment is a radio named by its label, moved through with the arrow keys.",
500
577
  },
501
578
  {
502
579
  name: "ThemeModeSwitcher",
@@ -515,8 +592,8 @@ export const USAGE = [
515
592
  family: "Choices",
516
593
  summary: "A button that stays pressed.",
517
594
  when: "A button that stays pressed, where pressing is the action: pin a chat, mute a thread, show hidden rows in a toolbar.",
518
- instead: "A setting saved with its section is a `Switch`. Choosing one value of several is a `RadioGroup`, drawn segmented when it is short.",
519
- behaviour: "Space or Enter flips `aria-pressed`. Pressed takes the accent fill and weight 600; the label does not change. Hover is the muted fill. Disabled is 50%.",
595
+ instead: "A setting saved with its section is a `Switch`. Choosing one value of several is a `RadioGroup`, or a `SegmentedControl` when it is two or three views of one surface.",
596
+ behaviour: "Space or Enter flips `aria-pressed`. Sizes, type, corners and the text inset are `Button`'s (32, 36 and 40 px; 12, 16 and 20 px), and the padding does not change with an icon; an icon-only toggle is square. Pressed takes the accent fill and weight 600, a neutral fill with no hue; the label does not change. Hover is the muted fill. Disable it only when pressing is impossible, and say why beside it.",
520
597
  words: "The label is the action and stays the same in both states: *Pin*, never *Pinned* or *Unpin*.",
521
598
  access: "A `button` with `aria-pressed`. An icon-only toggle names what it does: *Pin chat*.",
522
599
  },
@@ -525,22 +602,22 @@ export const USAGE = [
525
602
  module: "toggle-group",
526
603
  family: "Choices",
527
604
  summary: "Pressed buttons that belong together.",
528
- when: "Pressed buttons that belong together: a list or grid view, which details a chat shows.",
529
- instead: "A segmented control that sets a value, such as the theme or a period, is a `RadioGroup` drawn with the toggle classes, as `ThemeModeSwitcher` does. Panels of one object are `Tabs`.",
530
- behaviour: "Spacing 0 joins the members into one outline with one seam; spacing 2 keeps them 8 px apart. Single or multiple pressed. Arrow keys move between members; Space presses.",
531
- words: "Text members are short words of equal weight. Icon members always carry a name: *List*, *Grid*.",
605
+ when: "Pressed buttons that belong together: which details a chat shows, the marks on a toolbar.",
606
+ instead: "A value that is always exactly one of two or three, such as a list or grid view, is a `SegmentedControl`: its options are radios, so pressing the chosen one never clears it. The theme is `ThemeModeSwitcher`. Panels of one object are `Tabs`.",
607
+ behaviour: "Spacing 0 joins the members into one outline with one seam; spacing 2 keeps them 8 px apart. With `multiple`, members are pressed independently and any number may be pressed, none included. Without it, at most one is pressed and pressing it again clears it; a value that is always one of two or three is a `SegmentedControl`. The group is named with `aria-label` or `aria-labelledby`, and a member is either a word as its children or carries an `aria-label`; a group or an icon member without a name does not compile. Colour: a pressed member takes the accent grey and nothing takes a hue. Arrow keys move between members; Space presses.",
608
+ words: "Text members are short words of equal weight. Icon members always carry a name: *Tools*, *Reasoning*.",
532
609
  access: '`role="group"` with a name; each member a button with `aria-pressed`.',
533
610
  },
534
611
  {
535
612
  name: "ActionStatus",
536
613
  module: "action-status",
537
614
  family: "Status",
538
- summary: "A control's three words on one action: what it does, what it is doing, what it did.",
539
- when: "A control that reports its own action: what it does, what it is doing, what it did. Waits under about a second.",
540
- instead: "The record's resulting state is a `StatusBadge`. A wait past a second adds one waiting line beside the work. An Agent's work is her orb.",
541
- behaviour: "Saving keeps the idle icon, the size and the place; only the word changes. The caller sets `disabled` and `aria-busy`. Done shows the check.",
615
+ summary: "A button's three words on one action: what it does, what it is doing, what it did.",
616
+ when: "A button that reports its own action: what it does, what it is doing, what it did. Waits under about a second.",
617
+ instead: "The record's resulting state is a `StatusBadge`. A wait past a second adds one waiting line beside the work. An Agent's work is her orb. A button with no done word is a `Button` with `loading`.",
618
+ behaviour: "Takes the props of a text `Button`. Saving is the button's `loading`: the idle icon, the size and the place stay, only the word changes, a second press is ignored and focus stays. Done swaps the icon for the check.",
542
619
  words: "Verb and noun idle (*Save changes*), the verb in progress (*Saving*), the result (*Saved*). No ellipsis. Italian: *Salva modifiche*, *Salvataggio*, *Salvato*.",
543
- access: 'The button\'s name is the current word. The result is announced once in a `role="status"` region; a failure as an alert.',
620
+ access: "The button's name is the current word, and it is `aria-busy` while saving. It renders no status region: the caller's `role=\"status\"` region announces the result once and a failure as an alert.",
544
621
  },
545
622
  {
546
623
  name: "AgentOrb",
@@ -580,22 +657,22 @@ export const USAGE = [
580
657
  module: "avatar",
581
658
  family: "Status",
582
659
  summary: "A person or a team: their picture, else their initials.",
583
- when: "A person or a team: the account at the foot of the sidebar, a member list, who changed something. The picture stands in for a person's own photo.",
584
- instead: "An Agent is always its `AgentOrb`, never an avatar. A company is its initials in a 6 px tile inside `RecordIdentity`.",
585
- behaviour: "A picture that fails falls back to initials, then to the person glyph while the owner is unknown; never blank. A group overlaps 8 px with a 2 px ring of the ground. The profile's 80 px upload button is `AvatarUploadButton`.",
586
- words: "Two uppercase initials from the name, else from the email. The overflow is a count: *+2*.",
587
- access: "Beside a visible name it is `aria-hidden`. Alone, its alt is the person's name. A group is labelled: *5 members*.",
660
+ when: '"Use a single `<Avatar>` for one person, team, or organization." (https://vercel.com/geist/avatar) "For two or more stacked avatars, use `<AvatarGroup>` so the cluster gets correct overlap, sizing, and a single accessible label." (https://vercel.com/geist/avatar) Ours: a person or a team: the account at the foot of the sidebar, a member list, who changed something. It always says who it shows: `label` names the entity (*Jane Doe*), and `decorative` stands for a name beside it that already does. The type asks for one of the two. "Pass `src` first and fall back to `letter` (1–2 uppercase chars) when the image is missing." (https://vercel.com/geist/avatar) Ours: the children are an `AvatarImage` and an `AvatarFallback` with the initials. "Pick a size that matches adjacent type: 20–24px next to `text-label-14`, 32px next to `text-label-16`, 48–64px in headers and onboarding states." (https://vercel.com/geist/avatar) Ours: `size` follows the type beside it: `xs` (16 px) and `sm` (24 px) next to small labels, `default` (32 px) next to body text, `lg` (48 px) in a header or an onboarding step. `AvatarWithIcon` puts a status glyph at the corner of a person\'s avatar.',
661
+ instead: "Ours: an Agent is always its `AgentOrb`, never an avatar. A company is its initials in a 6 px tile inside `RecordIdentity`.",
662
+ behaviour: '"By default the first member sits on top of the stack, so the first credited author stays the most prominent." (https://vercel.com/geist/avatar) "Set `reverse` to flip the order so the last member sits on top instead." (https://vercel.com/geist/avatar) "The visual left-to-right order is unchanged." (https://vercel.com/geist/avatar) "By default `overlap="auto"` scales the spacing with `size`, keeping a generous, evenly-spaced cluster at any size." (https://vercel.com/geist/avatar) "Pass a number to set the overlap in pixels instead." (https://vercel.com/geist/avatar) "Lower values give more generous spacing; higher values pack tighter for dense, space-constrained UI." (https://vercel.com/geist/avatar) Ours: `auto` is about 30 % of the size (5, 7, 10 and 14 px), and each member is cut out with a 1 px ring in the page colour. When `limit` cuts the group, the last place is a dark `+N` count that covers the member it hides. "Reserve `placeholder` for the loading shell, never as a permanent fallback." (https://vercel.com/geist/avatar) Ours: there is no `placeholder`; a picture that fails falls back to initials from `avatarInitials`, set in the neutral accent fill with the text colour on it, in both themes. For a name with no readable letter the caller passes a glyph, so it is never blank. The picture is held by a 1 px hairline. The corner glyph of `AvatarWithIcon` is the person\'s state, in the muted foreground colour, so its `label` names the person and the state. The profile\'s upload button is `AvatarUploadButton`.',
663
+ words: '"Keep `letter` uppercase and derived from the entity name. No emoji, no punctuation, no `?`." (https://vercel.com/geist/avatar) Ours: the initials come from the name, else from the email; one initial at `xs`. The overflow is a count: *+2*.',
664
+ access: '"`title` is the literal entity name (`Acme Inc.`, `Jane Doe`)." (https://vercel.com/geist/avatar) "Geist already prefixes letter avatars with `Avatar with initials:` for screen readers, so don’t hand-write `Avatar of …`." (https://vercel.com/geist/avatar) Ours: the name is `label`, as it is written (*Jane Doe*, not *Avatar of Jane Doe*), and we add no prefix. Beside a visible name it is `decorative`. A group carries one label: *5 members*. A corner glyph is `aria-hidden`; `label` says the state: *Jane Doe, paused*.',
588
665
  },
589
666
  {
590
667
  name: "Badge",
591
668
  module: "badge",
592
669
  family: "Status",
593
670
  summary: 'Short metadata beside its subject: a role, a plan, "Beta".',
594
- when: 'Short metadata beside its subject: a role ("Owner"), a plan, "Beta". 20 px high, 6 px corners, 12 px label at 500. The category fills (teal, purple, pink) name a kind.',
595
- instead: "For a state use `StatusBadge`, which picks the tone and its mark: the gray, green, red, amber and blue fills are its alone. For one categorical fact in a row use `Tag`. For a change in a figure, `TrendBadge`.",
596
- behaviour: "A static label. It becomes a link only through `render` with a real anchor, which then takes the hover. Never a click handler, never nested, never a paragraph.",
597
- words: 'One to three words, sentence case, a noun: "Business plan", not "This is on Business".',
598
- access: "Read in place as text. A coloured variant still carries its word.",
671
+ when: '"Use Badge for short, scannable metadata that sits next to the thing it describes: status, plan tier, environment, or role." (https://vercel.com/geist/badge) "One badge per row; two side by side is a sign the row needs a second column." (https://vercel.com/geist/badge) Ours: a state is a `StatusBadge`; a `Badge` is a role (*Owner*), a plan, an environment, *Beta*. It always has a word, or one `icon` with a `title`, and the type asks for it. `size` follows the type beside it: `sm` (20 px) next to small labels, `md` (24 px) in a row, `lg` (32 px) in a header. The category fills (teal, purple, pink) name a kind.',
672
+ instead: '"For a colored dot without text, use `Status Dot`." (https://vercel.com/geist/badge) Ours: that is `StatusDot`. "For clickable filter chips that toggle a query, use the `pill` variant or a small `Button`." (https://vercel.com/geist/badge) Ours: there is no `pill` variant; a filter that toggles a query is a `Button` or `CollectionFilters`. For a state use `StatusBadge`, which picks the tone and its mark: the gray, green, red, amber and blue fills, soft or solid, are its alone. One categorical fact in a row is a `Tag`. A change in a figure is a `TrendBadge`.',
673
+ behaviour: '"Badges are static labels." (https://vercel.com/geist/badge) "Don’t wire `onClick` onto them; promote to a Button or link if the user can act on the value." (https://vercel.com/geist/badge) Ours: it takes no `onClick`, no hover and no focus; it becomes a link only through `render` with a real anchor, and only `outline` takes the hover, a gray fill. "Keep badge content to text or `icon` + text." (https://vercel.com/geist/badge) "Never stack two icons or a child Badge inside a Badge." (https://vercel.com/geist/badge) Ours: never a paragraph either. "Pair lifecycle badges (`Alpha`, `Beta`, `Early Access`) with a `Tooltip` that names the limit, like `Alpha: API may change before GA`." (https://vercel.com/geist/badge) Ours: the badge is the `render` of a `TooltipTrigger` with `tabIndex={0}`, so keyboard and touch users reach the limit it names. "Don’t add a checkmark icon for success states or an X for errors; the variant carries that signal." (https://vercel.com/geist/badge) Ours: only `StatusBadge` carries a state, and it always draws its tone\'s mark. "Map meaning to color: `green` for healthy, `red` for error, `amber` for warning, `blue` for informational or production, `gray` for neutral." (https://vercel.com/geist/badge) "The `-subtle` suffix tones any of them down on dense surfaces." (https://vercel.com/geist/badge) Ours: those fills are `StatusBadge`\'s, as the tones `success`, `danger`, `warning`, `info` and `neutral`, with `emphasis` `subtle` or `solid`. `Badge` is a fixed-height pill (20, 24 or 32 px) with no border: colour sits on the fill and the word. A soft fill takes its hue\'s strong shade for the word, `default` is the foreground colour inverted, and `outline` is neutral, a page-colour fill held by a 1 px inset ring. With an icon, the left padding is 6, 8 or 12 px, so the mark and the word look evenly spaced. The icon takes the word\'s colour.',
674
+ words: '"Title Case, one word when possible, two max: `Active`, `Pending`, `Pro`, `Enterprise Trial`." (https://vercel.com/geist/badge) Ours: sentence case (*Enterprise trial*). "Match the canonical API or log term: `Production` not `Prod`, `Deployed` not `Live`, `Canceled` not `Cancelled` (the Vercel API uses one L)." (https://vercel.com/geist/badge) Ours: the term is our own API\'s. "Skip stuffing sentences inside (`Currently Active`, `You are on Pro`); the surrounding row supplies the context." (https://vercel.com/geist/badge)',
675
+ access: '"Set `title` for icon-only or ambiguous badges so screen readers announce the meaning." (https://vercel.com/geist/badge) Ours: an icon alone must set `title`, and the type asks for it. Read in place as text. "Don’t rely on color alone; the text has to be readable without it." (https://vercel.com/geist/badge) Ours: a coloured variant still carries its word.',
599
676
  },
600
677
  {
601
678
  name: "BrandWordmark",
@@ -608,6 +685,17 @@ export const USAGE = [
608
685
  words: "None beyond its name, *nadicode*, written as the glossary defines it.",
609
686
  access: '`role="img"` named *nadicode* by default; pass `alt=""` where the name is already beside it, and it is hidden.',
610
687
  },
688
+ {
689
+ name: "CheckCounts",
690
+ module: "check-counts",
691
+ family: "Status",
692
+ summary: "The checks run on one thing, counted by how they ended, each count a small outlined pill.",
693
+ when: "The summary of a set of checks on one object, at the trailing edge of the row that opens them: an Agent's health checks (tasks completed, failed, still running), as Vercel counts a deployment's checks on its Deployment Checks row.",
694
+ instead: "One state of a record is a `StatusBadge`. Whether the row as a whole passed is `CheckState`. A count of changed lines is `DiffCount`.",
695
+ behaviour: "Each outcome is a 24 px outlined pill of its mark, in the outcome's colour (passed blue, failed red, warning amber, pending grey), and its count in ink and tabular figures, zeros included, in the order given, as Vercel draws a deployment's check counts. Static.",
696
+ words: "The label is the count in words with its noun: *3 tasks completed*, *0 failed*.",
697
+ access: "A list, drawn in phrasing elements so it can stand inside the button of the row it sums up; each chip is read by its label, and the mark is hidden, so colour never carries the outcome alone.",
698
+ },
611
699
  {
612
700
  name: "FileTypeIcon",
613
701
  module: "file-type-icon",
@@ -626,21 +714,10 @@ export const USAGE = [
626
714
  summary: "What says which thing a record is, at a fixed size at the start of its row, its object's head or its feed line: an Agent's orb, an app's logo, a company's initials, a person's picture, or a record kind's icon on its tile.",
627
715
  when: "The first thing in a record's row (32), at the head of its object page (64), in a feed or a sidebar identity line (24), on a Library entry (40): wherever it tells a person which thing they are looking at.",
628
716
  instead: "An icon beside a card's title, a KPI's label or a section heading only repeats the words and is left out. A picture for a first use or a whole-page moment is the empty state's or the page's own. A model is its `ModelIdentity`.",
629
- behaviour: "Static, except an Agent's orb, which moves with her state. An Agent is always her orb from the orb engine, never an avatar or a painted circle, and `frame` lets the product wrap that orb, such as in the transition that carries her to her page; an app keeps its own logo on the white tile; a company is two initials on the ringed card tile; a person is their picture, then their initials; a record with no picture of its own takes its kind's navigation icon, on the purple ground for a skill and the blue one for a plugin.",
717
+ behaviour: "Static, except an Agent's orb, which moves with her state. An Agent is always her orb from the orb engine, never an avatar or a painted circle, and `frame` lets the product wrap that orb, such as in the transition that carries her to her page; an app keeps its own logo on the white tile; a company is its logo: the one white ink Sofia kept from its site on a black tile in both themes, as the company's card stamps it, or a square SVG icon in its own colours filling the tile, as Vercel shows a project's favicon; its two initials on the ringed card tile until a logo is found, or when the stored logo does not load; a person is their picture, then their initials; a record with its own object picture, such as a Library skill or plugin, draws that picture on its kind's ground; a record with no picture of its own takes its kind's navigation icon, on the purple ground for a skill and the blue one for a plugin.",
630
718
  words: "None of its own: the record's name stands beside it. Initials are the first letters of the name's first two words.",
631
719
  access: "Decorative beside the record's name. Alone, as in a narrow rail, `label` names it.",
632
720
  },
633
- {
634
- name: "MetaLabel",
635
- module: "label-system",
636
- family: "Status",
637
- summary: "A small muted label of metadata, and its mono sibling for a command's suffix.",
638
- when: "A short fixed fact set beside a name in dense chrome: *Built in*, *Read only*. `CommandSuffix` holds a count or a key after a command.",
639
- instead: "A state is `StatusBadge`; a category is `Tag`; a plan or role is `Badge`. A bound shortcut is `Kbd`.",
640
- behaviour: "20 px, 6 px corners, the muted fill, never interactive, never wraps.",
641
- words: "One or two words in sentence case.",
642
- access: "Read in place as text.",
643
- },
644
721
  {
645
722
  name: "ModelIdentity",
646
723
  module: "model-identity",
@@ -674,6 +751,17 @@ export const USAGE = [
674
751
  words: "None of its own; the platform's name always stands beside it.",
675
752
  access: "Always `aria-hidden`: the name beside it is the reading.",
676
753
  },
754
+ {
755
+ name: "ProgressChecklist",
756
+ module: "progress-checklist",
757
+ family: "Status",
758
+ summary: "How far one record is through the steps that make it ready: a blue ring at the end of its row that opens the steps in a drawer at the right edge.",
759
+ when: "“Production Checklist”, the ring at the end of a project's row on Vercel's Projects page, which opens the steps that make a project ready for production. (https://vercel.com/dashboard) Ours: the steps a Customer or a Partner takes before nadicode's work runs for them, each one a fact the Portal can read (company details confirmed, a plan, Nadia connected, an Agent at work), at the end of the record's row.",
760
+ instead: 'Steps a person follows on a page, one after the other, are `NumberedSteps`; a setup with a wait is `FormDrawer job="steps"`. A ratio with no steps behind it is a `Gauge`. One state of a record is a `StatusBadge`.',
761
+ behaviour: 'The ring is a 24 px `Gauge` in blue with the double check inside in ink, so the glyph reads at a glance, on a 36 px ghost button whose tooltip names the checklist and how far it is ("Customer setup 3/4"). It opens a drawer from the right: the ring again, the title, one line, then one card per step: its icon on a tile of its tone, its title, one line, and the outline button that goes where the step is done; every card is the same hairline card, and a done step differs only by its blue Done chip after its title, with no button, so the steps still to do, with their buttons, draw the eye. The footer\'s button closes it. A step is done because the thing it asks for exists, so the checklist has no Skip and remembers nothing.',
762
+ words: 'Every string is the product\'s. The title names the checklist ("Customer setup"); a step\'s title reads as the thing done ("Plan chosen"); its button names where it goes ("Open billing").',
763
+ access: "The ring's button is named by `label`, which says whose checklist and how many steps are done; the ring itself is hidden from assistive technology. The drawer is a dialog named by its title; Escape, × and the footer's button close it and give focus back to the ring. The steps are a list.",
764
+ },
677
765
  {
678
766
  name: "RecordIdentity",
679
767
  module: "record-identity",
@@ -681,8 +769,8 @@ export const USAGE = [
681
769
  summary: 'A record\'s identity in a row (DESIGN.md "Tables and missing values"): its mark at 32, its name, and under it one muted `detail` line saying what the record is or where it comes from, so the row reads without a column heading.',
682
770
  when: "The first cell of every collection row: the mark, the name, and one muted line saying what the record is or where it comes from, so the row reads without a heading.",
683
771
  instead: "An Agent in a gallery is `AgentListRow`; a model is `ModelIdentity`. A descriptive item with prose and actions is `Item`.",
684
- behaviour: "The name holds one line and ends in an ellipsis, whole in its title, with a `hint` such as an ID read after it. `wraps` only when the name is the whole record, such as a memory. The detail breaks between words, so a failure inside the row is read whole. The table owns the link.",
685
- words: 'The name as the record calls itself. Detail: kind, then relation, three to six words: "Company skill · every Agent has it".',
772
+ behaviour: "The mark stands 16 px before the name. The name is semibold ink and the detail muted, two lines with no gap between them, so the row is an entity row; `mark` and `detail` are both required, and a record with nothing to say in the detail passes the dash of `TableValue`. The name holds one line and ends in an ellipsis, whole in its title, with a `hint` such as an ID read after it. The detail is one line too and ends in an ellipsis, whole in its title when it is text; in a row on a card narrower than its columns, the facts that leave the row join it after a middle dot, so the record stays the name over one line. `wraps` only when the name is the whole record, such as a memory, and then the detail wraps too. The table owns the link.",
773
+ words: '“Lead the left column with a scannable identifier: an `Avatar` or icon, a Title Case label, then sentence-case secondary metadata (`Member since Mar 14, 2026`).” (https://vercel.com/geist/entity) Ours: the name as the record calls itself, and the detail in sentence case: kind, then relation, three to six words, "Company skill · every Agent has it".',
686
774
  access: "The name is the row link's text; the mark is decorative; the title carries the full name.",
687
775
  },
688
776
  {
@@ -690,22 +778,22 @@ export const USAGE = [
690
778
  module: "status-badge",
691
779
  family: "Status",
692
780
  summary: "A record's state: a mark and a word in one of five tones.",
693
- when: "A record's state, when it sets the row apart: failed, waiting on a person, a step in motion, paused, connected. Gray is every terminal state that needs no action.",
694
- instead: "For a category (an installation, a partner) use `Tag`. For a control reporting its own action use `ActionStatus`. For a mark beside a word that already names the state, `StatusDot`.",
695
- behaviour: "Still: a status never moves and is never a button. Each app keeps one value-to-tone map in one module; nothing else picks a status colour. A healthy record shows no badge.",
696
- words: "A state, not a verb: *Failed*, *Waiting for you*. Sentence case, one to three words, no trailing ellipsis. Italian runs a third longer: *In attesa di te*.",
697
- access: "The word is the name; the mark is `aria-hidden` and repeats the tone as a shape, so colour never carries the state alone. Not focusable.",
781
+ when: '"Use Badge for short, scannable metadata that sits next to the thing it describes: status, plan tier, environment, or role." (https://vercel.com/geist/badge) Ours: a record\'s state, when it sets the row apart: failed, waiting on a person, a step in motion, paused, connected. Gray is every terminal state that needs no action. "Map meaning to color: `green` for healthy, `red` for error, `amber` for warning, `blue` for informational or production, `gray` for neutral." (https://vercel.com/geist/badge) Ours: success is green, danger red, warning amber, info blue, neutral gray, whatever the state\'s name, in our colour values. The tone and the word are required, and the type asks for both.',
782
+ instead: '"For non-deployment statuses (Workflow runs, Queue messages, Sandbox, Cron jobs), use a `Badge` with the canonical state vocabulary instead of repurposing the dot." (https://vercel.com/geist/status-dot) Ours: that `Badge` is `StatusBadge`. For a category (an installation, a partner) use `Tag`. For plain metadata (a role, a plan) use `Badge`. For a control reporting its own action use `ActionStatus`. For a mark beside a word that already names the state, `StatusDot`.',
783
+ behaviour: '"Badges are static labels." (https://vercel.com/geist/badge) "Don’t wire `onClick` onto them; promote to a Button or link if the user can act on the value." (https://vercel.com/geist/badge) Ours: a status never moves and is never a button. "The `-subtle` suffix tones any of them down on dense surfaces." (https://vercel.com/geist/badge) Ours: `subtle`, the default, is for rows, tables, lists and menus: it fills the pill with the tone\'s soft ground and sets the mark and the word in its strong shade; gray takes the foreground colour, never a muted one. `solid` is for a status that stands on its own, such as an object\'s page header or a card title: it fills the pill with the tone\'s strong colour and sets the mark and the word on it. No border and no ring either way. "Don’t add a checkmark icon for success states or an X for errors; the variant carries that signal." (https://vercel.com/geist/badge) Ours: it always draws its tone\'s mark, even for success and error: the mark repeats the tone as a shape so a status reads without colour. Each app keeps one value-to-tone map in one module; nothing else picks a status colour. A healthy record shows no badge.',
784
+ words: '"Title Case, one word when possible, two max: `Active`, `Pending`, `Pro`, `Enterprise Trial`." (https://vercel.com/geist/badge) Ours: a state, not a verb (*Failed*, *Waiting for you*), in sentence case, one to three words, no trailing ellipsis. Italian runs a third longer: *In attesa di te*. "Match the canonical API or log term: `Production` not `Prod`, `Deployed` not `Live`, `Canceled` not `Cancelled` (the Vercel API uses one L)." (https://vercel.com/geist/badge) "Skip stuffing sentences inside (`Currently Active`, `You are on Pro`); the surrounding row supplies the context." (https://vercel.com/geist/badge)',
785
+ access: '"Don’t rely on color alone; the text has to be readable without it." (https://vercel.com/geist/badge) Ours: the word is the name; the mark is `aria-hidden`, so colour never carries the state alone. Not focusable.',
698
786
  },
699
787
  {
700
788
  name: "StatusDot",
701
789
  module: "status-dot",
702
790
  family: "Status",
703
- summary: "A small still dot beside a word that already names the state.",
704
- when: "A small mark in dense chrome, beside a word that already names the state: a channel, a machine in a list.",
705
- instead: "A status that stands alone is a `StatusBadge`. Something in motion is the waiting line or an Agent's orb; the dot has no info tone.",
706
- behaviour: "Still; never pulses. Tones are `good`, `muted`, `warn`, `bad`, at full strength.",
707
- words: "None of its own. The word beside it says the state.",
708
- access: "Always `aria-hidden`. If the word is missing, the state is missing.",
791
+ summary: "A small still dot for one state, named by its label or by the word beside it.",
792
+ when: '"Deployment lifecycle only. `state` accepts `QUEUED | BUILDING | READY | ERROR | CANCELED | DELETED`." (https://vercel.com/geist/status-dot) Ours: any one state in dense chrome (a channel, a machine in a list), set as `tone`. It always says which state it shows: `label` names it (*Ready*), and `decorative` stands for a word beside it that already does. The type asks for one of the two. "Don’t flash the color through every transitional state on a polling tick; only update when the readyState changes." (https://vercel.com/geist/status-dot) Ours: pick the tone from the state and keep it until the state changes.',
793
+ instead: '"For non-deployment statuses (Workflow runs, Queue messages, Sandbox, Cron jobs), use a `Badge` with the canonical state vocabulary instead of repurposing the dot." (https://vercel.com/geist/status-dot) Ours: that `Badge` is `StatusBadge`; so is a status that stands alone, or one that needs a mark and a word. "For health summaries that need a number (uptime, hit rate), use `Gauge`; for in-flight work with a knowable total, use `Progress`." (https://vercel.com/geist/status-dot) Ours: something in motion is the waiting line or an Agent\'s orb; the dot has no info tone.',
794
+ behaviour: '"The dot animates while `BUILDING` or `QUEUED` and goes static once the deployment reaches a terminal state." (https://vercel.com/geist/status-dot) "Don’t add a separate spinner alongside it." (https://vercel.com/geist/status-dot) Ours: still, even while a state is in progress: it never pulses. A 10 px dot; tones are `good`, `muted`, `warn`, `bad`, at full strength. Colour sits on the dot alone: with `showLabel` the word beside it is 14 px and neutral, never in the tone\'s colour. `label` alone draws the dot with the word as its name and tooltip; `showLabel` draws the word 8 px after it, for a dot with no other text around it.',
795
+ words: '"Use `label` only when the dot stands alone without surrounding text; Geist sentence-cases the state for you (`Building`, `Ready`, `Error`)." (https://vercel.com/geist/status-dot) Ours: `label` is the required name unless the dot is `decorative`, and we write it in sentence case ourselves: *Ready*, *Error*, *Canceled*. "Don’t wrap the dot in extra prose like `Status: Ready`. The label already names the state." (https://vercel.com/geist/status-dot)',
796
+ access: '"The component composes its own `aria-label` from `titlePrefix` plus the state message; don’t override it with a generic `aria-label="status"`." (https://vercel.com/geist/status-dot) Ours: there is no `titlePrefix` and no `aria-label` prop; `label` is the dot\'s name, an image role with that name. "When the dot sits inline with text that already names the state, mark the dot decorative with `aria-hidden` so screen readers don’t announce it twice." (https://vercel.com/geist/status-dot) Ours: `decorative` does it; the word beside the dot must name the state, and if that word is missing, the state is missing. "Color is not the only signal: every state ships with a distinct title and label so colorblind users get the same information." (https://vercel.com/geist/status-dot)',
709
797
  },
710
798
  {
711
799
  name: "Tag",
@@ -734,33 +822,22 @@ export const USAGE = [
734
822
  module: "breadcrumb",
735
823
  family: "Navigation",
736
824
  summary: "The top bar's path to the page.",
737
- when: "The top bar of every product page: the sidebar group or the parents, ending at the destination or the object. An object opened from search still shows its parent.",
825
+ when: "\"Navigation aid that shows the user's location within a site's hierarchy, with text and menu variants.\" (https://vercel.com/geist/breadcrumbs) Ours: the top bar of every product page: the sidebar group or the parents, ending at the destination or the object. An object opened from search still shows its parent.",
738
826
  instead: "Never a back button in its place. A return link may remember a visit, beside the path, not instead of it. Sibling views are tabs.",
739
- behaviour: "Ancestors are links in the body colour that turn ink on hover; the current item is ink and not a link. A long path folds its middle into `BreadcrumbEllipsis` and keeps the parent and the current item.",
827
+ behaviour: "Neutral only, one step of grey carries every state. `text` (the default) is a 20 px line of 14 px text with a 16 px chevron between steps and 6 px around each; `menu` is a row of 22 px chips with a rule and no chevrons, each chip cut with an ellipsis past 10 rem and carrying its full name in a tooltip, scrolling sideways when the path is long; a chip's rule darkens a step on hover. Ancestors are links in the body colour that turn ink on hover; the current item is ink and not a link. An ancestor the person may not open is a `BreadcrumbLink` with `disabled`: the placeholder grey, which sits close to the muted grey, so `aria-disabled`, the not-allowed cursor and no hover carry the state, and the type refuses an `href` or a `render` on it, so it cannot navigate. A long path folds its middle into `BreadcrumbEllipsis` and keeps the parent and the current item.",
740
828
  words: "The groups' and records' own names, exactly as the sidebar and the record write them. The `aria-label` and the ellipsis's `label` come from the product's catalogue: \"Path\", \"More\".",
741
- access: 'A labelled `nav` with an ordered list; the current item has `aria-current="page"`; the chevrons and the ⋯ glyph are hidden, and the folded middle reads its `label`.',
829
+ access: 'A labelled `nav` with an ordered list; the current item has `aria-current="page"`; a disabled ancestor has `aria-disabled`; the chevrons and the ⋯ glyph are hidden, and the folded middle reads its `label`.',
742
830
  },
743
831
  {
744
832
  name: "CommandDialog",
745
833
  module: "command",
746
834
  family: "Navigation",
747
835
  summary: "Find anything across the product and go there.",
748
- when: "Finding anything across the product and going there: an Agent, a customer, a page, a creation command. Opened by ⌘K or the sidebar's Find.",
749
- instead: "One record's commands are its `ActionMenu`. Search inside one collection is `CollectionSearch`. Choosing the page's scope is `ScopeSwitcher`.",
750
- behaviour: "Opens instantly, a third of the way down, on glass at 12 px corners. Arrows move the highlight, Enter goes, Esc closes and returns focus to the trigger. An empty query shows useful defaults; no match says so.",
751
- words: "Groups are plural nouns (*Agents*, *Go to*). Items are names, or verb and noun for a command (*New Agent*). No match quotes the query.",
752
- access: "A dialog with a screen-reader title and description; the field is a combobox over a listbox; the highlighted item is `aria-selected`.",
753
- },
754
- {
755
- name: "DashboardShell",
756
- module: "dashboard-shell",
757
- family: "Navigation",
758
- summary: "A web product's frame: the sidebar, the top bar with the page's path, and the content.",
759
- when: "The Portal's frame around every signed-in page: its sidebar, the top bar and the scrolling content, with an optional inspector.",
760
- instead: "A desktop window is `DesktopAppShell`. A page's own title and actions are `PageHeader` inside the content.",
761
- behaviour: "The sidebar can be resized and collapsed to its rail; the content scrolls under the top bar. On a narrow window the sidebar opens as a sheet.",
762
- words: "None of its own: the sidebar and the header carry the product's words.",
763
- access: "The content is the page's `main`, labelled by `contentLabel`; the inspector is a labelled `aside`; the narrow window's navigation button and the sidebar rail are named.",
836
+ when: '"Use CommandMenu for a global, keyboard-first command palette that finds resources and runs actions across the app." (https://vercel.com/geist/command-menu) Ours: finding anything across the product and going there: an Agent, a customer, a page, a creation page (*New Agent*). Opened by ⌘K or the sidebar\'s Find. It goes to places and never runs a command.',
837
+ instead: '"For a menu opened from a visible trigger on a single resource, use `Menu`; for right-click on a row, use `ContextMenu`." (https://vercel.com/geist/command-menu) "Group items into pages (`Projects`, `Team Settings`) when a single flat list would exceed roughly 30 items or span multiple resource types." (https://vercel.com/geist/command-menu) Ours: those are `DropdownMenu` and `ContextMenu`. One record\'s commands are its `ActionMenu`. Search inside one collection is `CollectionSearch`. Choosing the page\'s scope is `ScopeSwitcher`. `CommandDialog` has no pages: its list is groups.',
838
+ behaviour: '"Bind `⌘K` on macOS and `Ctrl+K` elsewhere. Don’t reuse the binding for any in-page filter input; it’s a global shortcut." (https://vercel.com/geist/command-menu) "Open into the root page; preserve the search query when navigating back from a sub-page so the user’s typing isn’t lost." (https://vercel.com/geist/command-menu) "Trap focus inside the overlay while it’s open and return focus to the previously active element on close." (https://vercel.com/geist/command-menu) "Show a recent or default item set when the input is empty so the menu is useful before typing." (https://vercel.com/geist/command-menu) Ours: opens instantly, 640 px wide and 15% down the window; on a phone it is a bottom sheet, 640 px high with 8 px top corners and the list filling it. The input row stands above the list with a line under it, the field in large type with no icon and an *Esc* key at its end that closes the dialog and is the next stop after the field on Tab; the list scrolls past 436 px. The first row is highlighted on open. No match says so. Every row is 36 px with the pointer cursor, a group\'s heading is a muted 36 px row, a divider bleeds across the list. A row\'s shortcut is a `CommandShortcut` and a word or icon at its end is a `CommandSuffix`. Colour carries nothing: the highlighted row is a faint neutral wash with its text unchanged, group headings, suffixes and the empty line are muted, and no row, key or divider takes a hue.',
839
+ words: '"`<CommandMenu.Item>` children are Title Case verb phrases (`Deploy Project`, `Invite Team Member`). Avoid navigation phrasing like `Go to project page`; CommandMenu commands act, not browse." (https://vercel.com/geist/command-menu) "`CommandMenuPage.label` is Title Case and names the scope (`Projects`, `Team Settings`)." (https://vercel.com/geist/command-menu) "`CommandMenuPage.placeholder` is sentence case, action-oriented, and ends with `…` (`Search projects…`, `Type a command or search…`). Bare `Search…` is wrong because it doesn’t name the scope." (https://vercel.com/geist/command-menu) "`<CommandMenu.Group heading>` is Title Case, 1–2 words (`Actions`, `Recent`)." (https://vercel.com/geist/command-menu) Ours: groups are plural nouns in sentence case (*Agents*, *Go to*), one or two words. Items are names, or verb and noun for a creation page (*New Agent*), because the dialog goes to places. The placeholder names what is searched, never a bare "Search". No match quotes the query.',
840
+ access: '"Use `aria-live="polite"` on the result count so screen readers hear how the list narrows as the user types." (https://vercel.com/geist/command-menu) "Up/Down arrows move highlight, Enter activates, Escape closes. Backspace at an empty input pops the page stack." (https://vercel.com/geist/command-menu) "Display each item’s `keybind` as a `Kbd` slot so the shortcut is discoverable to sighted users and announced as a label to screen readers." (https://vercel.com/geist/command-menu) Ours: a dialog with a screen-reader `title` and `description`, both required; the *Esc* key is a button named by `closeLabel`, which is required; the field is a combobox over a listbox; the highlighted item is `aria-selected`. A polite live region reads `resultCount` as the list narrows, and it is required. `CommandInput` requires its `placeholder`, `CommandGroup` its `heading` and `CommandItem` its `onSelect`, so no field, group or row is left unnamed or inert; a row with no group stands outside a `CommandGroup`. A shortcut is a `CommandShortcut`, drawn as `kbd` elements. With no pages there is no stack for Backspace to pop.',
764
841
  },
765
842
  {
766
843
  name: "DesktopAppShell",
@@ -768,7 +845,7 @@ export const USAGE = [
768
845
  family: "Navigation",
769
846
  summary: "A desktop window's frame: title bar, rails, resizable panes and status bar.",
770
847
  when: "A desktop product's whole window: Nadia's left column (the Agents list, then an Agent's sections behind *‹ Agents*), its workspace and its side panes.",
771
- instead: "The Portal's column is `PortalSidebarFrame` inside `DashboardShell`. Both draw their rows with the same `SidebarMenuButton` and end with the same `AccountMenu`.",
848
+ instead: "The Portal's column is `PortalSidebarFrame` inside its `Sidebar`. Both draw their rows with the same `SidebarMenuButton` and end with the same `AccountMenu`.",
772
849
  behaviour: "One 44 px band shares the window buttons and the window's commands. The workspace splits into resizable panes; the secondary pane and the inspector rail appear only when given. Each Agent row is her orb, name and one line.",
773
850
  words: "Every pane label is the product's word for what it holds. An Agent's line says her job in under six words: *Writes quotes from the price list*.",
774
851
  access: "Each pane and rail is a labelled region; the orb beside a name is decorative.",
@@ -784,38 +861,16 @@ export const USAGE = [
784
861
  words: "Link columns are nouns; the identity paragraph names the company as the glossary defines it.",
785
862
  access: "A `footer` landmark; each link group is a labelled `nav`.",
786
863
  },
787
- {
788
- name: "NavigationMenu",
789
- module: "navigation-menu",
790
- family: "Navigation",
791
- summary: "A website header's top-level links, some opening a panel of links.",
792
- when: "The website's header: a few top-level destinations, where one may open a panel of related pages.",
793
- instead: "A product's navigation is its `Sidebar`. Commands are a menu; sibling views are tabs.",
794
- behaviour: "Opens on hover and on click; one panel at a time, positioned under its trigger. Escape closes it and returns focus.",
795
- words: "Nouns, one or two words, sentence case. A panel's links name their page and may carry one short line.",
796
- access: 'A labelled `nav`; triggers expose `aria-expanded`; links are real anchors with `aria-current="page"` on the current one.',
797
- },
798
- {
799
- name: "Pagination",
800
- module: "pagination",
801
- family: "Navigation",
802
- summary: "Numbered sibling pages with Previous and Next.",
803
- when: "`Pagination` for numbered sibling pages. A table of server pages takes `DataTable`'s own Previous and Next under its card; the card's footer strip holds the count or the paging.",
804
- instead: "Appending rows in place is a `Button` with `PendingLabel` (*Load more*). A fixed comparison passes `pagination={false}` and stays whole.",
805
- behaviour: "Boundaries are disabled. While the next page loads both buttons disable and the rows stay. The order and the page live in the URL. The labels hide under `sm`, leaving the chevrons.",
806
- words: "*Previous*, *Next*. The footer is a count with its noun or *Page 1 of 6*, never a help sentence.",
807
- access: 'A labelled `nav`; the current page has `aria-current="page"`; the arrows read *Go to previous page*; a pending button is `aria-busy`.',
808
- },
809
864
  {
810
865
  name: "PortalSidebarFrame",
811
866
  module: "portal-sidebar-frame",
812
867
  family: "Navigation",
813
- summary: "The Portal's navigation frame, without the Portal's routing: the wordmark and the section name over the collapse control, the Find control with its shortcut, the labelled nav bands, the band that leads out of the Portal, and the account at the foot.",
814
- when: "`PortalSidebarFrame` is the Portal's whole left column, and the website's sample of it.",
815
- instead: "Nadia's left column belongs to `DesktopAppShell`. Both draw their rows with the same `SidebarMenuButton` and end with the same `AccountMenu`; neither paints its own selected fill or its own account block.",
816
- behaviour: "Wordmark and product name over the collapse control, then Find, the bands, the way out (Get Nadia) and the account, split by 1 px guides.",
868
+ summary: "The website's sample of the Portal's navigation column, drawn from data and without the Portal's routing.",
869
+ when: "A static picture of the Portal's column on the website; all of `account`, `bands`, `findLabel`, `subtitle` and `toggleLabel` are required.",
870
+ instead: "A product's own navigation is a `Sidebar` filled by `SidebarFrame`, never this sample.",
871
+ behaviour: "Wordmark and product name beside the collapse control, then Find with ⌘K, the groups, the way out (Get Nadia) and the account; 1 px rules sit under the header and above the way out and the account, none between groups. The selected row takes the selection fill; every other row stays neutral.",
817
872
  words: "Find is one word beside ⌘K. Bands are short nouns; items are nouns, one or two words, sentence case.",
818
- access: 'The column is a labelled `nav`. Find is a button named with its shortcut. The wordmark is `role="img"` named nadicode.',
873
+ access: "The collapse control is named by `toggleLabel` and Find by `findLabel`; the wordmark is decorative beside the product name.",
819
874
  },
820
875
  {
821
876
  name: "ScopeSwitcher",
@@ -833,33 +888,77 @@ export const USAGE = [
833
888
  module: "sidebar",
834
889
  family: "Navigation",
835
890
  summary: "A product's navigation column: where a person can go, with the account at its foot.",
836
- when: "The Portal's and Nadia's left column: the groups of places, an object's sections when the column swaps, the update offer and the account menu at the foot.",
837
- instead: "An object's sections are SidebarSections inside this column, never a second column or a tab row; a page's own views are Tabs.",
838
- behaviour: "One row is selected and marked as the current page. Collapsed or narrow, rows keep their icon and show their name in a tooltip; under the mobile breakpoint the column becomes a sheet. A row's count uses SidebarMenuBadge, whose attention tone is for a failure that needs the person.",
839
- words: 'Each row is a place\'s noun: "Agents", "Tasks", "Customers". A count is a bare number.',
840
- access: 'The column is a landmark; each row is a link or a button with its name, and the current one carries aria-current="page". The badge\'s number is read with its row.',
891
+ when: "The one left column of the Portal and Nadia: a header, Find, groups of places, an object's sections when the column swaps, a band for the update or the way out, and the account. 256 px wide; the Portal's can be dragged between 224 and 384 px or collapsed to icons.",
892
+ instead: "Never a second column or a tab row for an object's sections: `SidebarSections` swaps this column. A page's own views are Tabs. A row that opens an area ends in `SidebarMenuChevron`.",
893
+ behaviour: 'Fills the cell its host gives it and stays in the page flow. `collapsible="icon"` (the default) collapses to 48 px icons with a tooltip name and becomes a sheet under 768 px; with `resize` a handle on its trailing edge drags it, steps it with the arrow keys, resets it on a double-click and collapses it on Enter. `collapsible="none"` is always open and never a sheet. A group is told apart by its label and its padding, never by a rule.',
894
+ words: 'Each row is a place\'s noun: "Agents", "Tasks", "Customers"; a count is a bare number. A sheet\'s title, description and Close label (`mobileTitle`, `mobileDescription`, `mobileCloseLabel`) are required; `resize`, when given, names the handle and comes from the catalogue.',
895
+ access: 'The column is a landmark; each row is a link or a button with its name, and the current one carries aria-current="page". The handle is a focusable separator with its value in px. The badge\'s number is read with its row.',
896
+ },
897
+ {
898
+ name: "SidebarBrand",
899
+ module: "sidebar",
900
+ family: "Navigation",
901
+ summary: "The company's wordmark over the product's name: what the header of a product's column says.",
902
+ when: "The `header` of a `SidebarFrame` in the Portal and its sample. Nadia's header is her company row instead.",
903
+ instead: "A switcher between companies or teams is a menu in the header, not this mark.",
904
+ behaviour: "A 112 px wordmark above one muted line, truncated. Inside a link, the link is the row.",
905
+ words: 'The line names the product the column belongs to: "Portal", "Business".',
906
+ access: "The wordmark is decorative beside the name; the link or button around it carries the accessible name.",
907
+ },
908
+ {
909
+ name: "SidebarFind",
910
+ module: "sidebar",
911
+ family: "Navigation",
912
+ summary: "The row that opens the product's Find: a field-shaped button under the header, with its key hint.",
913
+ when: "The one Find of a sidebar, directly under its header; it opens the product's command dialog or search popover.",
914
+ instead: "Never a real text field in the column; filtering a list is the page's own search.",
915
+ behaviour: "An outlined row of the column's width: a search icon, the label muted, and the key hint at the end. Collapsed, only the icon stays, with the label as its tooltip; the hint shows only above 768 px.",
916
+ words: 'One word naming what it finds ("Find"); the hint is the product\'s own key chord (⌘K in the Portal), never a sentence.',
917
+ access: "A button named by `label`, so the name survives the collapsed icon; the hint is read after it.",
918
+ },
919
+ {
920
+ name: "SidebarFrame",
921
+ module: "sidebar",
922
+ family: "Navigation",
923
+ summary: "The regions of a product's `Sidebar`, in order: header, Find, groups, band, account.",
924
+ when: "The content of every product's `Sidebar`: the Portal's brand and Nadia's company row go in `header`, their Find in `find`, their places in the children.",
925
+ instead: "A product lays no region of its own: no grid of rows, no hand-drawn rule between regions.",
926
+ behaviour: "A 56 px header with the collapse control under a 1 px rule, Find under it, the groups scrolling in the middle, then the band and the account pinned at the foot above 1 px rules. `header`, `find` and `account` are required; an absent band leaves no rule. Collapsed, the header shows only the collapse control.",
927
+ words: "None of its own; every word arrives in the regions.",
928
+ access: "The regions are plain containers; the landmark is the `Sidebar` around them and the nav inside each group.",
929
+ },
930
+ {
931
+ name: "SidebarMenuChevron",
932
+ module: "sidebar",
933
+ family: "Navigation",
934
+ summary: "Vercel's ›: the mark at the end of a row that swaps the column to that area's sub-menu: the Portal's Settings and Company, Nadia's company.",
935
+ when: "The last child of a `SidebarMenuButton` whose press replaces the places with the area's sections (`SidebarSections`), never of a row that only goes to a page or to a destination whose views are tabs (the Portal's Analytics and Library).",
936
+ instead: "A count is `SidebarMenuBadge`; an external link says so in its name.",
937
+ behaviour: 'A 16 px muted chevron pushed to the row\'s end, mirrored in right-to-left. A `Sidebar` with `collapsible="icon"` hides it when collapsed; a column that is narrowed by its host (`collapsible="none"`) leaves it out when narrow: `{narrow ? null : <SidebarMenuChevron />}`.',
938
+ words: "None.",
939
+ access: "Decorative: the row's name and its link say where it goes.",
841
940
  },
842
941
  {
843
942
  name: "SidebarSections",
844
943
  module: "sidebar-sections",
845
944
  family: "Navigation",
846
- summary: "The sidebar swapped to one object: the way back, what you are in, and its sections.",
847
- when: "Inside an object, the account or Platform settings, the same sidebar column shows these instead of the places: an Agent's or a Customer's sections in the Portal, Nadia's Edit and her company's sections, the Portal's Account and Platform settings.",
848
- instead: "The places a person can go are the sidebar's own groups; a page's views are Tabs; a settings page that does not swap the column is a SettingsPage. Never a second column beside the first.",
849
- behaviour: "The back row returns to where the person came from and is disabled while leaving is blocked. One section is current. Narrow, the identity and the heading leave, and each row keeps its icon with its name in a tooltip.",
850
- words: 'The back row names where it goes ("Agents", "Back to workspace"); the identity is the object\'s own name; the heading names the kind of settings ("Account"); sections are nouns ("Skills", "Members").',
851
- access: 'The sections are a nav named by `label`; the current one carries aria-current="page". The identity\'s mark is decorative beside its name.',
945
+ summary: "The sidebar swapped to one object or one area: the way back, what you are in, and its sections.",
946
+ when: "Inside an object, the same sidebar column shows these instead of the places: an Agent's or a Customer's sections in the Portal, Nadia's Edit and her company's sections. Inside an area whose pages are siblings a person moves between, the area's sub-menu without `identity`, as Vercel's Settings: the Portal's Settings, Company and Account. The row that opens an area ends in `SidebarMenuChevron`. Required: the way back and the nav's `label`; every section has an icon.",
947
+ instead: "The places a person can go are the sidebar's own groups; a page's views are Tabs (Analytics, Library); a settings page that does not swap the column is a SettingsPage. Never a second column beside the first.",
948
+ behaviour: "With `identity`: a back arrow row, then what you are in, then the sections. Without it, as Vercel's sub-menu: one row with a ‹ at its start and the area's name centred, then the sections as words alone. The back row returns to where the person came from and is disabled while leaving is blocked. One section is current and takes the selection fill; the rest stay neutral. A section that sets `group` opens a labelled group that holds the sections after it until another names its own; the label and the gap tell groups apart, never a rule. Narrow, the identity, the area's name and the labels leave, and each row keeps its icon with its name in a tooltip.",
949
+ words: 'The back row names where it goes ("Agents", "Back to workspace"); the identity is the object\'s own name; an area\'s `label` is its sidebar row\'s word ("Settings", "Company"); a group heading names the kind of settings ("Account"), in sentence case; sections are nouns ("Skills", "Members").',
950
+ access: 'The sections are a nav named by `label`; the current one carries aria-current="page". Without `identity` the back row is named by its `label` alone and the centred area name is read as text after it. The identity\'s mark is decorative beside its name.',
852
951
  },
853
952
  {
854
953
  name: "Tabs",
855
954
  module: "tabs",
856
955
  family: "Navigation",
857
956
  summary: "Panels of one surface that switch in place.",
858
- when: "Sibling views of one object that switch in place inside one surface.",
859
- instead: "Views with their own URL are `TabsNav`. A choice that changes a value is a `RadioGroup` or `ToggleGroup`. A stable destination belongs in the `Sidebar`. A tab never opens a new scope.",
860
- behaviour: "One treatment among siblings, chosen through `variant`, never a page border. Arrow keys move, one tab stop. The underline fades on `confirm`, 180 ms.",
861
- words: "Nouns, one or two words: *Overview*, *Installations*. Italian (*Installazioni*) scrolls, never wraps or truncates.",
862
- access: "`tablist`, `tab`, `tabpanel` with `aria-selected`.",
957
+ when: '"Use Tabs to switch between sibling views inside a single page (`Overview`, `Logs`, `Settings`)." (https://vercel.com/geist/tabs) "Cap a Tabs row at 5–7 entries on desktop and 3–4 on mobile. Past that, consolidate or move secondary views behind a `Menu`." (https://vercel.com/geist/tabs) Ours: the cap is 7 on a desktop and 4 on a phone.',
958
+ instead: '"For navigation between unrelated pages, use a sub-menu, not Tabs." (https://vercel.com/geist/tabs) "Tabs imply the views share scope, URL parent, and data model." (https://vercel.com/geist/tabs) Ours: views with their own URL are `TabsNav`. A choice that changes a value is a `RadioGroup` or `ToggleGroup`. A stable destination belongs in the `Sidebar`.',
959
+ behaviour: '"Selecting a tab is instant; don’t trigger network confirmation or toast on tab change." (https://vercel.com/geist/tabs) "Reflect the active tab in the URL (query param or path) so deep-links and refresh restore state." (https://vercel.com/geist/tabs) "Disable individual tabs only for permission or empty-state reasons; pair the disabled tab with a `tooltip` that names the constraint." (https://vercel.com/geist/tabs) Ours: the `tooltip` is `disabledReason`, shown on hover and focus and kept as the tab\'s description. One treatment among siblings, chosen through `variant`, never a page border: `line` (50 px, a 2 px ink underline over the row\'s 1 px rule) for a page\'s collections, `default` (a 32 px pill on the selection fill) for an object\'s sections. The page owns the chosen tab: `value` and `onValueChange` are required. There is no sliding underline. Colour is neutral only: the chosen tab is ink, the rest are muted until hover, and a disabled tab is never dimmed: in `line` it reads as an unchosen one, in `default` it sits on the selection fill with muted text, both with a not-allowed cursor.',
960
+ words: '"`tabs[].title` is Title Case, 1–2 words, and names the destination noun (`Overview`, `Logs`, `Settings`). Verbs belong on buttons; `View Logs` is wrong on a tab." (https://vercel.com/geist/tabs) "`tabs[].tooltip` is sentence case and explains the constraint (`Only visible to project owners.`), not the tab’s purpose." (https://vercel.com/geist/tabs) "Don’t append a count to the title (`Logs (12)`); use a badge slot instead and drop the badge at zero." (https://vercel.com/geist/tabs) Ours: the title is sentence case (*Overview*, *Installations*), not Title Case. There is no badge slot: a count belongs to the list\'s footer. A reason is *Only owners see billing*. Italian (*Installazioni*) scrolls, never wraps or truncates.',
961
+ access: '"Keep Left/Right arrows moving focus across tabs and Enter/Space activating; don’t hijack with global shortcuts." (https://vercel.com/geist/tabs) "Label the tablist with `aria-label` when no visible heading sits above it (`aria-label="Sections"`)." (https://vercel.com/geist/tabs) "Maintain a visible focus ring on the active tab; don’t suppress focus styles for visual polish." (https://vercel.com/geist/tabs) Ours: the arrow keys move with one tab stop. `tablist`, `tab`, `tabpanel` with `aria-selected`. The row is named by `aria-label` or `aria-labelledby`, which the type requires; a disabled tab is `aria-disabled`.',
863
962
  },
864
963
  {
865
964
  name: "TabsNav",
@@ -868,9 +967,9 @@ export const USAGE = [
868
967
  summary: "Sibling views of one object, each with its own URL, under the top bar.",
869
968
  when: "Sibling views of one object under the top bar, when each view has its own URL (an object's sections), with `TabsLink` for each.",
870
969
  instead: "Panels that switch in place are `Tabs`. A stable destination belongs in the `Sidebar`. An object's ancestry is the `Breadcrumb`.",
871
- behaviour: "Overflow scrolls sideways and brings the current link into view. The current link takes the underline.",
872
- words: "Nouns, one or two words: *Overview*, *Installations*. Italian scrolls, never wraps or truncates.",
873
- access: 'A labelled `nav` of links with `aria-current="page"` on the current one.',
970
+ behaviour: "\"Reflect the active tab in the URL (query param or path) so deep-links and refresh restore state.\" (https://vercel.com/geist/tabs) Ours: each view is its own URL, so a link carries it. Overflow scrolls sideways and brings the current link into view, and the row fades out over 32 px at each end where links stand beyond it. The current link takes the selection fill, or in `line` an ink underline over the row's 1 px rule; the other links are muted until hover, with no hue. Sizes and spacing follow `Tabs`. The sub-pages of one object stand as the filled-pill row in a `band` under the top bar, as Vercel draws a deployment's tabs: the band is this component's, with `above` for the object's identity, so no page draws the border, the fill or the gutters around the row. The band keeps the page's own column and gutter, and the first link's words start at the page title's edge, its fill reaching into the gutter as Vercel's tabs do. The `line` row has its own rule and takes no band.",
971
+ words: '"`tabs[].title` is Title Case, 1–2 words, and names the destination noun (`Overview`, `Logs`, `Settings`). Verbs belong on buttons; `View Logs` is wrong on a tab." (https://vercel.com/geist/tabs) "Don’t append a count to the title (`Logs (12)`); use a badge slot instead and drop the badge at zero." (https://vercel.com/geist/tabs) Ours: the title is sentence case (*Overview*, *Installations*), not Title Case. There is no badge slot: a count belongs to the list\'s footer. Italian scrolls, never wraps or truncates.',
972
+ access: '"Label the tablist with `aria-label` when no visible heading sits above it (`aria-label="Sections"`)." (https://vercel.com/geist/tabs) "Maintain a visible focus ring on the active tab; don’t suppress focus styles for visual polish." (https://vercel.com/geist/tabs) Ours: a `nav` of links named by `aria-label` or `aria-labelledby`, which the type requires, with `aria-current="page"` on the current one.',
874
973
  },
875
974
  {
876
975
  name: "textLinkVariants",
@@ -879,7 +978,7 @@ export const USAGE = [
879
978
  summary: "The classes of a link that reads as text: `inline` inside a sentence, `plain` on its own, `quiet` for a muted way out.",
880
979
  when: "A link inside a sentence (`inline`, blue and underlined), a link that stands alone as text (`plain`), or a muted secondary way to somewhere such as a footer's *Learn more* (`quiet`).",
881
980
  instead: "A command is a `Button`; a destination drawn as a button is a link with `buttonVariants`. Going to a record from its row is the record's name in `RecordIdentity`.",
882
- behaviour: "Classes only: the caller renders the real anchor or router link. `inline` thickens its underline on hover; `plain` and `quiet` underline on hover. Paint changes ride the shared paint transition.",
981
+ behaviour: "Classes only: the caller renders the real anchor or router link. `inline` thickens its underline on hover; `plain` and `quiet` underline on hover. Paint changes ride the shared paint transition. Colour: only `inline` takes the link blue, as Vercel puts blue on links and nowhere else in running text; `plain` keeps the colour of the text around it, and `quiet` is muted and turns ink on hover, which is how a card footer's or a list note's *Learn more* reads. A link that leaves the product ends in the arrow icon after its text, which `quiet` sizes.",
883
982
  words: "The link's text names where it goes: *Open Billing*, *Read the privacy notice*; never *Click here* or a bare URL.",
884
983
  access: "A real `a` with an `href`. A link that leaves the product says so in its text or carries the arrow with a name. Focus is the base layer's 2 px outline.",
885
984
  },
@@ -887,33 +986,22 @@ export const USAGE = [
887
986
  name: "AgentCard",
888
987
  module: "agent-card",
889
988
  family: "Data",
890
- summary: "One Agent, or another pictured object, as a card in a gallery.",
989
+ summary: "One Agent, or another pictured object, as a card in a gallery, as Vercel's Templates gallery draws a template.",
891
990
  when: "A gallery of Agents or templates where each one is chosen by its picture and its job: the Agents to create from, the templates of a hub.",
892
- instead: "A list of Agents to open is `AgentListRow`. An Agent's own page opens with `AgentProfileCard`. A Library entry is `CatalogEntry`.",
893
- behaviour: "The picture (her orb at 96) on the leading side, the name and job, an optional status under them, and a footer with the meta and one action. A long name wraps inside the card.",
894
- words: "The name as the Agent calls herself; the job in one line (*Writes quotes from the price list*); the action verb and noun (*Use template*).",
991
+ instead: "A list of Agents to open is `AgentListRow`. An Agent's own page opens with `AgentProfileCard`. A Library skill or plugin is a row of its collection (`CollectionPage`).",
992
+ behaviour: "Vercel's template card: the name over its job, cut after two lines, then the small meta and an optional status, and under them the preview tile, inset 32 px from the card's leading edge and running off its trailing and bottom edges, the picture (her orb at 96) in its middle on the recessed ground. The one action stands at the name's right, as a card's one header action does. A long name wraps inside the card. A text object, with no picture, ends after its words.",
993
+ words: "The name as the Agent calls herself; the job in one line (*Writes quotes from the price list*); the meta as short facts joined by a middle dot (*6 skills · v1.2.0*); the action verb and noun (*Create Agent*).",
895
994
  access: "The name is the card's heading text; the orb is decorative beside it; the action is a real button or link.",
896
995
  },
897
- {
898
- name: "ApprovalRow",
899
- module: "approval-row",
900
- family: "Data",
901
- summary: "One thing waiting for a person's yes: a mark for what is asking, the request in one sentence, what happened underneath it, and the single action that clears it.",
902
- when: "Each request in a list of approvals: a mark for what is asking, the request in one sentence, what happened, and the one action that clears it.",
903
- instead: "A record's state is a `StatusBadge`. A notice about a condition is `NoticeBanner`. An Agent's question inside a conversation belongs to the conversation.",
904
- behaviour: "An outline `Item` at the small size; several sit in an `ItemGroup`. A `failed` description is announced.",
905
- words: "The title is the request as a sentence; the action is its yes as verb and noun: *Approve payment*.",
906
- access: 'A `listitem`; a failed description is `role="alert"`.',
907
- },
908
996
  {
909
997
  name: "Card",
910
998
  module: "card",
911
999
  family: "Data",
912
- summary: "A resting surface that holds one thing: a header, content and a recessed footer.",
913
- when: "A bounded piece of a page that stands on its own: a settings section, a KPI, a chart, a table. Usually reached through the composition that owns that job.",
914
- instead: "A page's own header is never in a card. A row of a list is `Item`. A plane of brand colour is `BrandField`.",
915
- behaviour: 'The resting material: 6 px corners and the rest shadow, no border of its own. `CardHeader` lays out title, description and one `CardAction`; `CardFooter` is the recessed strip behind one seam. `size="sm"` for a compact card in a side column.',
916
- words: "The title is a noun phrase naming what the card holds; the description one sentence, only when it adds something.",
1000
+ summary: "The surface of every card; on its own only a container inside a composition.",
1001
+ when: '"For in-product cards and dashboard tiles, use `Card`; Book is too decorative for repeated UI rows." (https://vercel.com/geist/book) Ours: only inside a composition that owns the job: the object card of an `ObjectPage`, the setting card of a `SettingsSection`, a KPI, a chart, a table. A new kind of card is a new composition, never a `Card` assembled at a call site.',
1002
+ instead: "\"Groups related form controls inside a bordered card with optional footer actions.\" (https://vercel.com/geist/fieldset) Ours: that card, a title and description, one control and a footer with help at the start and one action at the end, is `SettingsSection`. An object's card (a header bar with its name and commands, a body of facts, rows that open, a footer hint with one link elsewhere) is `ObjectPage`. The page's title row is never in a card. A row of a list is `Item`. A plane of brand colour is `BrandField`.",
1003
+ behaviour: '"One Note per concept." (https://vercel.com/geist/note) "Stacking three Notes on a card means the page architecture, not the Note copy, is wrong." (https://vercel.com/geist/note) "Use the Error component as a block surface when a section or page-level resource failed to load: a panel, a dashboard card, a route boundary." (https://vercel.com/geist/error) Ours: the corner and shadow come with `surface`: a raised card takes the larger corner. Never set a corner at the call site. `CardHeader` lays out title, description and one `CardAction`, which stays at the title\'s right at every width when it is one control (a button, a badge, the three dots) and moves under the description in a header narrower than 28rem when it holds several; `CardFooter` is the recessed strip behind one seam: on the left one short muted line (a hint, a constraint, the plan a feature needs), on the right at most one button or link. It is 56 px high whether or not it holds a button, and as high as its lines in a `size="sm"` card, the compact card of a side column or a figure. The two kinds differ in what their header and footer hold: an object card\'s header holds the object\'s commands and its footer only a link, so it never saves; a setting card\'s header holds a title, a description and at most a switch that applies on change, and its footer holds the Save. A card is neutral; colour enters only through a status inside it or a section\'s `tone`.',
1004
+ words: "The title is a noun phrase naming what the card holds; the description one sentence directly under it, only when it adds something.",
917
1005
  access: "A plain container; give it `render={<section />}` with a heading when it is a region of the page.",
918
1006
  },
919
1007
  {
@@ -921,9 +1009,9 @@ export const USAGE = [
921
1009
  module: "card-row",
922
1010
  family: "Data",
923
1011
  summary: "The row sibling cards sit in.",
924
- when: "Two to four sibling cards side by side: the KPIs of an overview, a pair of charts.",
925
- instead: "A list of records is `Item` or a table. A single card needs no row.",
926
- behaviour: "One column below `from`, then `columns` columns; `auto` gives each card an equal share. Each card fills its cell, so the bottom edge is even.",
1012
+ when: "Two to five sibling cards side by side: the KPIs of an overview, a pair of charts.",
1013
+ instead: 'A list of records is `Item` or a table. A single card needs no row. Geist\'s ruled `Grid`, with visible guide lines, crosses and cell padding, is not built: "Use Grid for two-dimensional cell-and-guide layouts in marketing pages, docs landing pages, and feature breakdowns where the rule lines and cell borders are part of the design." (https://vercel.com/geist/grid) "For plain n-column responsive content (cards, lists), use Tailwind `grid grid-cols-*` utilities directly; Grid is overkill when guides aren’t visible." (https://vercel.com/geist/grid) Ours: this is that plain n-column case.',
1014
+ behaviour: '"Set `columns` and `rows` at all three breakpoints so cells reflow predictably between mobile, tablet, and desktop." (https://vercel.com/geist/grid) Ours: one column below `from`, then `columns` columns, 16 px apart, with no `rows`; `auto` gives each card an equal share, and `5` sets two by two with the fifth across, then three over two from `lg`, then all five in one row from `xl` (1280 px), as OpenRouter\'s Activity sets its five tiles, never with a hole beside the last card. Each card fills its cell, so the bottom edge is even. The row is neutral; it tints nothing.',
927
1015
  words: "None of its own.",
928
1016
  access: "A layout container; reading order is source order.",
929
1017
  },
@@ -931,23 +1019,23 @@ export const USAGE = [
931
1019
  name: "CodeBlock",
932
1020
  module: "code-block",
933
1021
  family: "Data",
934
- summary: "Several lines of exact text to read or copy.",
935
- when: "Several lines a person reads or copies exactly: a configuration, static JSON, a command sequence.",
936
- instead: "One short value to copy is a read-only input with a `CopyButton`. A before and after is `FileDiff`. Code in a conversation is her `MarkdownText`.",
937
- behaviour: "Read-only, no wrapping, scrolls in both directions up to 384 px. Copy takes the exact text, says Copied only on success and says when it failed. No syntax colour, no line numbers.",
938
- words: "The real filename, or an honest label. The language as its common name: *JSON*. Copy, Copied, Couldn't copy.",
939
- access: "The text area is named by its `label`; the copy button names its target (*Copy mcp.json*) and announces the result.",
1022
+ summary: "Several lines of exact text to read, copy or point at line by line.",
1023
+ when: "“Use `<CodeBlock>` for multi-line, syntax-highlighted source the reader will scan or paste.” (https://vercel.com/geist/code-block) Ours: several lines a person reads or copies exactly: a configuration, static JSON, a command sequence, a file with the lines under discussion marked. There is no syntax colour yet.",
1024
+ instead: "“For a single inline token (env var, function name, file path), use `Inline Code`.” (https://vercel.com/geist/code-block) “Pick `Snippet` for a copy-to-clipboard shell command or one-line key reveal; it ships with the prompt glyph and copy affordance built in.” (https://vercel.com/geist/code-block) Ours: a token inside a sentence is inline code. One short value to copy is `TextWithCopyButton`. A before and after to compute is `FileDiff`. Code in a conversation is Nadia's `MarkdownText`.",
1025
+ behaviour: "“Highlight only the lines under discussion. A block where every line is highlighted reads the same as no highlight at all.” (https://vercel.com/geist/code-block) “Mark added or removed lines with the `diff` syntax or the dedicated added/removed props. Faking them with `// added` comments breaks copy-paste.” (https://vercel.com/geist/code-block) “Show the filename header when the snippet has a paste destination (`app/page.tsx`, `vercel.json`). Omit it for ephemeral examples.” (https://vercel.com/geist/code-block) “You can link to lines. Just press on any line number.” (https://vercel.com/geist/code-block) “Use the `tabs` prop for a tabbed language switcher instead of the default select.” (https://vercel.com/geist/code-block) “Always pass a `language`/`syntax` (`tsx`, `bash`, `json`, `diff`). Highlighting is the primary reason to choose `<CodeBlock>` over a plain `<pre>`.” (https://vercel.com/geist/code-block) Ours: read-only, no wrapping, 20 px rows, each line numbered with a number that links to it. Colour sits on the row and its 2 px bar at the start, never on the text or the numbers: `highlightedLinesNumbers` (info), `addedLinesNumbers` (success) and `removedLinesNumbers` (destructive); a line carries one mark, and the line a link points at takes the warning fill. A mark is fill and bar with no glyph, announced by the line's name. `hideLineNumbers` is for a few lines where numbers add nothing. The header (40 px, muted fill, the file's icon and name) exists only with a `filename`, a `switcher` or `tabs`. `tabs` draw as a row above the header, a `switcher` (a select) sits in the header before the copy button; never both. In the header the filename and the copy mark are muted; without a header the copy button floats 15 px from the top right corner in the full text colour and shows while the block is hovered or something in it has focus (always on a touch screen). A line number links to its line for a pointer and is out of the tab order. The file icon follows the filename's extension. There is no syntax colour and no `language` until a highlighter is adopted, and no Open in v0 action.",
1026
+ words: "“Snippets stay runnable. Don’t paraphrase real code into pseudo-syntax, and don’t prepend `$` to shell commands. `<Snippet>` already renders the prompt, so `text=\"$ vercel deploy\"` doubles to `$ $ vercel deploy`.” (https://vercel.com/geist/code-block) “Keep prose around the block in sentence case and put backticks around CLI flags inline (`` `--prebuilt` ``).” (https://vercel.com/geist/code-block) Ours: the real filename, or none. A switcher's `label` names what it switches (*Language*), and its options are the languages by their common names (*JSON*). `label` names the code (*MCP configuration*). `lineLabel` says a marked line and its mark in the catalogue's words (*Line 3, added*, *Riga 3, aggiunta*); it is only called for a highlighted, added or removed line. `copyLabel` names what is copied (*Copy mcp.json*); `copiedLabel` (*Copied*); `copyFailedLabel` (*Couldn't copy*). `label` and the three copy words are required, and `lineLabel` with any marked line.",
1027
+ access: "Ours: the code is a focusable, scrollable region named by `label`. Line numbers are hidden from assistive technology and are not tab stops; a highlighted, added or removed line carries `lineLabel`'s words as hidden text, with the numbers shown or not. The copy button names its target and announces the result.",
940
1028
  },
941
1029
  {
942
1030
  name: "DataTable",
943
1031
  module: "data-table",
944
1032
  family: "Data",
945
- summary: "Records that share comparable facts, read across in rows inside their `TableCard`.",
946
- when: 'Records that share comparable facts: usage per Agent, statements, invoices, customers with their partner and count. 40 px rows for figures; `density="comfortable"`, the default, gives 48 px for entity rows.',
947
- instead: "An entity collection that is the page's whole job is `CollectionPage`, which draws its own 56 px rows. A descriptive list is `ItemGroup`. One object's facts are `ObjectFacts`.",
948
- behaviour: "Headings visible by default; numbers trail in tabular figures. Row hover, focus within and an open menu take `accent`; a selected row takes `muted`; headers and footers never highlight. Narrow cards move facts into a detail row, never scroll sideways; an `optional` column's empty value is left out of it. Empty sits outside the body.",
949
- words: 'Headings are nouns with their unit: "Cost (€)", "Tokens". The footer is a count or the paging; the summary row is a labelled total. Italian headings run a third longer and never wrap.',
950
- access: 'A native table with `aria-label`; `th scope="col"`; the record\'s name is the row\'s one link. A missing value is a hidden "—" with screen-reader text, never blank.',
1033
+ summary: "Records compared across columns: the same facts for every record, read down a column and across a row, inside their `TableCard`.",
1034
+ when: "“Use `<Table>` for tabular data where rows share the same shape and at least one column is sortable or comparable across rows.” (https://vercel.com/geist/table) Ours: the person compares: usage per Agent, statements, invoices, customers with their partner and count. The columns are fixed, each has a heading, and at least one is a figure or can be sorted. There are three kinds of list. Objects a person opens (a project, a deployment, a member, an integration) are entity rows: a `RecordList`. Values edited in place (a domain, an environment variable) are entity rows that expand into their form: a `RecordList`'s `editor`. Records compared across columns are this table.",
1035
+ instead: "“For a row of descriptive content paired with a single action (membership row, integration row), use `Entity` instead.” (https://vercel.com/geist/table) “For a key/value metadata block on a detail page, use `Description`, not a two-column table.” (https://vercel.com/geist/table) Ours: Geist's `Entity` is a `RecordList` row. A registry of entities whose columns `RecordList` cannot hold, more than three facts beside a filter bar the product owns, as Vercel's Deployments list, is this table with `rows=\"entities\"`, which draws the same rows: headings for assistive technology only, 72 px rows, a `RecordIdentity` in the identity column. Its `Description` is `ObjectFacts`. A list on a page of its own is a `CollectionPage`. A descriptive list is `ItemGroup`.",
1036
+ behaviour: "Every column has a `header`; a column without one does not compile. The headings are always visible, medium weight in the cells' body grey, 36 px high above 12 px of room and the rule, so the first row starts at the rule like every other; rows are 40 px. Each column is as wide as its definition's `size`, in pixels, and the columns without one share the rest, so a table gives a size to every column but the one that takes the spare width; a value too long for its column ends in an ellipsis, whole in its title (a record's name carries its own), unless the column `wraps`; a column marked `identifier` holds ids or paths and is cut in the middle instead. The last column trails unless it `wraps` or is `centered`; a `centered` column's heading and values sit in the middle of it. Records are divided by a 1 px rule, none after the last. Records of several kinds stand in one table under `group` headings (Vercel's Usage page: *Delivery network*, *Functions*): a heading row that spans the columns, in ink, medium text, 36 px high with the same rule under it, one per group in the order the groups first appear, never a second table; a heading is the group's name and carries no figure of its own. A row that opens a record takes `accent` on hover, and any row on focus within and while its menu is open; a row that opens nothing never reacts to hover. A selected row takes `muted`; headers and footers never highlight and carry no fill. A row never scrolls sideways: on a card narrower than its columns (the sized ones and 160 px for each without a size) the row keeps its name and its `primaryColumns`, and every other column leaves it, its value joining the one muted line under the name, joined by a middle dot and ending in an ellipsis (a `Tag` reads as its text there, and a `wraps` column's prose wraps the line whole), or `dropped` when its `compact` says so, as Vercel's lists read on a phone: no labels, no second line; an `optional` column's empty value is left out of the line. A table whose every column is in `primaryColumns`, a settings matrix, lays out by its content there instead, so the name wraps beside the columns. A bounded page's paging stands in the card's footer strip at its trailing edge, opposite `footer`, which stands at the leading edge: its `status` (\"Page 2 of 7\") before Previous and Next, which are the product's router links (`previousLink`, `nextLink`) when each page is its own address, and buttons otherwise; the one that leads nowhere stays, disabled. A note about the whole table is `footerNote`, in a strip of its own under that one: one muted line, an optional *Learn more* after it at the left and one secondary action at the right (Vercel's deployments list ends \"Deployment retention is enabled. Learn more\" with *View recently deleted*); it is the one place a table says a sentence. Colour is neutral: headings and cells in the body grey, no hue, except a status badge or a tone the cell itself carries. “When the underlying list is empty (filter cleared, never created), render `Empty State` outside the table rather than an empty `<Table.Body>`.” (https://vercel.com/geist/table) “After an async filter change, wrap the region in `aria-live=\"polite\"` so screen readers announce the new state.” (https://vercel.com/geist/empty-state) Ours: empty sits outside the body, in a polite live region that stays in place so a filter that empties the table is announced. “Sortable column headers are buttons. The visible label stays Title Case; the sort-direction arrow is decorative and the button announces the next sort state to assistive tech.” (https://vercel.com/geist/table) Ours: the order has one owner, the app's `sorting`, which the URL holds, so no heading is a button and every page of a paged list follows one order. “Apply `tabular-nums` (or Geist Mono) to numeric columns so digits align across rows for comparison.” (https://vercel.com/geist/table) Ours: a numeric column trails in tabular figures.",
1037
+ words: '“Column headers (`<Table.Head>`) are Title Case nouns or noun phrases: `Last Used`, `Requests (7d)`, `Created`, `Status`. Never sentences.” (https://vercel.com/geist/table) “Render `—` in cells where a value is unknown or not applicable. Don’t substitute `N/A`, `null`, or an empty string.” (https://vercel.com/geist/table) “Use the canonical short relative-time form in cells (`2m ago`, `5h ago`); switch to `Mar 14, 2026` past 7 days. See `Relative Time Card`.” (https://vercel.com/geist/table) “Pagination labels are `Previous` and `Next`. Page-count copy reads `Page 2 of 7` or `21–40 of 142` with an en-dash inside the range.” (https://vercel.com/geist/table) Ours: headings are sentence case nouns with their unit, "Cost (€)", "Tokens", where Geist writes Title Case. The words of a time and of Previous and Next are the product\'s catalogue, in both languages. The footer is a count or the paging; the summary row is a labelled total. Italian headings run a third longer and never wrap.',
1038
+ access: 'A native table named by `tableLabel`, which is required; `th scope="col"`; the record\'s name is the row\'s one link. A sorted column\'s heading carries `aria-sort`. Ours: a missing value is a hidden "—" with screen-reader text, never blank.',
951
1039
  },
952
1040
  {
953
1041
  name: "DiffCount",
@@ -966,7 +1054,7 @@ export const USAGE = [
966
1054
  family: "Data",
967
1055
  summary: "A row whose label opens what it holds, with an action and a trailing value beside it.",
968
1056
  when: "A line in a dense list that opens to show its detail: a tool call, a group of steps.",
969
- instead: "Supporting detail on a page is `Disclosure`. A long block cut short is `ExpandableBlock`.",
1057
+ instead: "Supporting detail on a page is `Disclosure`.",
970
1058
  behaviour: "The chevron shows on hover or focus and while open. Without `onToggle` the row is static and draws no chevron.",
971
1059
  words: "The label names what is inside; the trailing value is a short figure.",
972
1060
  access: "The label is a `button` with `aria-expanded` when it toggles.",
@@ -982,49 +1070,60 @@ export const USAGE = [
982
1070
  words: "The added and removed labels and the folded-lines count come from the product's catalogue.",
983
1071
  access: "Each changed line carries its added or removed label in screen-reader text; the plus and minus marks are hidden.",
984
1072
  },
1073
+ {
1074
+ name: "Gauge",
1075
+ module: "gauge",
1076
+ family: "Data",
1077
+ summary: "A ring that shows one ratio out of 100.",
1078
+ when: '"A 0–100 ratio against a fixed maximum where the comparison is the point, like quota usage, build cache hit rate, uptime, or billing-period consumption." (https://vercel.com/geist/gauge) Ours: `size` follows the weight of the figure beside it.',
1079
+ instead: '"For determinate task progress with a known total (uploads, multi-step setup), use `Progress`." (https://vercel.com/geist/gauge) "For binary or enumerated state, use `Status Dot` for deployments or `Badge` for everything else." (https://vercel.com/geist/gauge) Ours: those are `StatusDot` and `StatusBadge`. A single figure on its own is a `KpiCard`. A wait of unknown length is the waiting line; the gauge has no indeterminate form. Two shares of one whole are a chart.',
1080
+ behaviour: '"Use `arc="equal"` for true ratios so 50% reads as exactly half." (https://vercel.com/geist/gauge) "Use the default `primary` arc for single-percentage usage where the filled portion is the story." (https://vercel.com/geist/gauge) "When using the gauge to display a ratio, use the `equal` arc priority to make both arcs are equally sized." (https://vercel.com/geist/gauge) Ours: there is no `arc` prop; the gauge always draws the primary arc, from the top, clockwise, with round ends and a small gap before the neutral track; at 0 or 100 there is no gap. Colour sits on the arc alone: the track stays the neutral line colour, and the number and icon stay neutral. "Threshold colors should match the same numeric breakpoints used elsewhere in the product (`>=80%` warning, `>=95%` error). Don’t invent gauge-only thresholds." (https://vercel.com/geist/gauge) Ours: `tone` is that threshold, never derived from the value. "Pair `indeterminate` with explanatory copy nearby (`Calculating usage…`) so the user knows the value is loading, not zero." (https://vercel.com/geist/gauge) Ours: there is no `indeterminate`; a value still loading is the waiting line. The smallest size has room only for an icon. The arc moves on `open`, still under reduced motion.',
1081
+ words: '"Always pair the gauge with an adjacent label or `Tooltip` naming what the number represents (`Build Cache Hit Rate`). The gauge alone is not self-describing." (https://vercel.com/geist/gauge) "Don’t put units inside `children`; the label carries the unit (`Uptime · 99.97%`). `children` is reserved for an icon overlay." (https://vercel.com/geist/gauge) "When `showValue` is on, the rendered number is the value only. Never inject a `%` or unit string into the prop." (https://vercel.com/geist/gauge) Ours: sentence case, *Cache hit rate · 99.97%*. `showValue` prints the number; `children` is only an icon.',
1082
+ access: '"The component sets `role="progressbar"` with `aria-valuemin`, `aria-valuemax`, and `aria-valuenow`. Don’t override those." (https://vercel.com/geist/gauge) Ours: the role is `meter`, with the same three values. "The adjacent label is the gauge’s accessible name. Tie it with `aria-labelledby` on the gauge wrapper so screen readers read “Uptime, 99 percent.”" (https://vercel.com/geist/gauge) Ours: it always has a name, `aria-label` or `aria-labelledby` pointing at the adjacent label, and the type asks for one. "Don’t use color to encode the threshold without redundant text; pair the warning tint with copy below or in the Tooltip." (https://vercel.com/geist/gauge)',
1083
+ },
985
1084
  {
986
1085
  name: "Item",
987
1086
  module: "item",
988
1087
  family: "Data",
989
1088
  summary: "A row said in a sentence: media, a title, one line of description, trailing actions.",
990
- when: "Things best said in a sentence: an activity feed, a Library shelf, connected accounts. Media, a title, one line of description, trailing actions.",
991
- instead: "Facts that compare across records are a `DataTable`. A page whose job is one collection is `CollectionPage`. Agents in a gallery are `AgentListRow`; Library entries `CatalogEntry`.",
992
- behaviour: "Inside a card the group is `divided`: no gaps, one hairline between items. An item that is an `a` or `button` takes the `accent` hover and is one hit target; nested controls stay independent. The title holds one line.",
993
- words: "Title: the name, or what happened as a short sentence (*Nadia filed 12 invoices*). Description: one sentence. One primary action as a button; the rest in the three dots.",
994
- access: "The group is a `list` and each item a list item. A linked item's text is its name; image media is decorative beside the title.",
1089
+ when: "“Use `<Entity>` for a row of descriptive content paired with one or two controls (member rows, integration rows, domain rows).” (https://vercel.com/geist/entity) Ours: things best said in a sentence, with one or two controls and no columns: a connected account, a session, a notification preference, a Library shelf row, an activity row. Media, a title over its description, trailing controls.",
1090
+ instead: "“For tabular data with sortable columns and shared row shape, use `Table` instead.” (https://vercel.com/geist/entity) “For a static key/value metadata block on a detail page, use `Description`.” (https://vercel.com/geist/entity) Ours: Geist's `Table` is a `DataTable` and its `Description` is `ObjectFacts`. An object a person opens, with a mark, facts and a status, is a `RecordList` row; the same row edited in place is a `RecordList` with an `editor`. A page whose job is one collection is `CollectionPage`. Agents in a gallery are `AgentListRow`; Library entries `CatalogEntry`.",
1091
+ behaviour: 'Every row has a title over its one muted description line; only a message row read whole, or a second content beside the first, may be description only. Media, content and actions stand 16 px apart, centred on one another, in a 72 px row of 16 px padding; the title and the description each hold one line and end in an ellipsis (`whole` lets a message wrap). Inside a card the group is `divided`: no gaps, one hairline between items. An item that is an `a`, a `button` or a `label` takes the `accent` hover and is one hit target; a plain item never reacts to hover; nested controls stay independent. A button in the row is `size="sm"`. A row a person picks starts with `ItemSelect` and is drawn `render={<label />}`, so the whole row ticks its box and has no other control. A second content beside the first takes only the room its words need. Colour is neutral: the title in ink, the description and any trailing words muted, no hue. “The right column holds at most one or two controls. If the row needs more, move secondary actions into a `Dots Menu`.” (https://vercel.com/geist/entity) Ours: the third control goes in the row\'s three dots. “Render the Skeleton variant (`entity-with-skeleton`) during load instead of an empty row, and swap to real content once data resolves.” (https://vercel.com/geist/entity) Ours: `ItemSkeleton` stands in a `SkeletonRegion`, still, with no shimmer.',
1092
+ words: "“Lead the left column with a scannable identifier: an `Avatar` or icon, a Title Case label, then sentence-case secondary metadata (`Member since Mar 14, 2026`).” (https://vercel.com/geist/entity) “Keep right-column buttons Verb + Noun (`Remove Member`, `Resend Invite`). Bare verbs like `Remove` or `Confirm` lose context once the row scrolls offscreen.” (https://vercel.com/geist/entity) Ours: the title is the name, or what happened as a short sentence (*Nadia filed 12 invoices*), and the description one sentence, in sentence case: *Remove member*, never *Remove*.",
1093
+ access: "Draw each row of a group `render={<li />}`, so the group is a list of list items. A linked item's text is its name; image media is decorative beside the title. “For multi-select rows, the leading `Checkbox` carries `aria-label=\"Select {entity name}\"` so the row is selectable without relying on the visual label.” (https://vercel.com/geist/entity) Ours: the box of `ItemSelect` is named *Select {name}* in the product's words.",
995
1094
  },
996
1095
  {
997
1096
  name: "Kbd",
998
1097
  module: "kbd",
999
1098
  family: "Data",
1000
- summary: "One key of a shortcut, beside the command it triggers.",
1001
- when: "A shortcut hint next to the command it triggers: Find with ⌘K, the sidebar with ⌘B. 20 px, 2 px corners, mono 12 on the recessed fill.",
1002
- instead: "A key a person types into is an `Input`; a code a person types is `InputOTP`. In a command list, `CommandShortcut`.",
1003
- behaviour: "Not interactive. Only shortcuts actually bound are shown. The caller maps the platform: ⌘ on macOS, Ctrl on Windows and Linux.",
1004
- words: "One key per `Kbd`; symbols for macOS modifiers (⌘ ⇧ ⌥), words elsewhere (*Ctrl*). Surrounding prose stays outside the key.",
1005
- access: "A `kbd` element, read in order; the command's own name carries the meaning.",
1099
+ summary: "One key, or a modifier with its key, of a shortcut beside the command it triggers.",
1100
+ when: '"Use `<Kbd>` for shortcut hints inside prose, menu items, and button suffixes." "Use `small` inside dense surfaces like menu rows, command-bar items, or table cells where the default size crowds adjacent text." (https://vercel.com/geist/keyboard-input) Ours: `small` is `size="sm"`. Find with ⌘K, the sidebar with ⌘B. Only shortcuts actually bound are shown.',
1101
+ instead: '"In long-form docs that narrate a shortcut, write `the ⌘ K shortcut` directly so the page renders the same when copied to plain text." (https://vercel.com/geist/keyboard-input) Ours: a key a person types into is an `Input`; a code a person types is `InputOTP`.',
1102
+ behaviour: '"Pass modifiers via boolean props (`meta`, `shift`, `alt`, `ctrl`)." "The component swaps `⌘` for `Ctrl` on Windows and Linux, so hard-coding `<Kbd>Cmd+K</Kbd>` ships the wrong glyph to half your users." (https://vercel.com/geist/keyboard-input) Ours: not interactive. ⌘ ⇧ ⌥ ⌃ on Apple systems, *Ctrl* *Shift* *Alt* elsewhere, with `meta` becoming *Ctrl*. The server draws the Apple form and the browser corrects it after hydration. Several flags make one key: `meta shift`. It is 24 px high (`size="sm"`: 20 px) in the interface\'s sans type, 14 px (12 px), with no gap between Apple glyphs. Colour: one neutral key in the primary ink on the page surface with a hairline ring, no hue; inside a tooltip it drops the ring and takes a fill one step off the tooltip\'s inverted surface, at 20 px.',
1103
+ words: '"`children` is one key, digit, or named key (`K`, `7`, `Enter`, `Esc`)." "Don’t lowercase it, don’t spell out modifiers inside, and don’t pack a sentence into the element." "Punctuation lives outside the element: `Press <Kbd meta>K</Kbd> to open the command menu.`" "Period, comma, and `or` separators stay in surrounding prose so screen readers don’t announce them as keys." (https://vercel.com/geist/keyboard-input) Ours: children is a capital letter, a digit, a mark or a named key from `KBD_KEYS`.',
1104
+ access: "Ours: a `kbd` element, read in order; the command's own name carries the meaning.",
1006
1105
  },
1007
1106
  {
1008
- name: "MemoryItem",
1009
- module: "memory-item",
1107
+ name: "MiddleTruncate",
1108
+ module: "middle-truncate",
1010
1109
  family: "Data",
1011
- summary: "One fact an Agent keeps, under its muted label.",
1012
- when: "A fact from an Agent's memory shown in her conversation or her profile.",
1013
- instead: "A fact about a record is `ObjectFacts`. A note a person writes is a `Textarea`.",
1014
- behaviour: "A short label over the fact; `children` replace the plain `fact` when it needs rich content.",
1015
- words: "The label is the product's word for memory from the catalogue; the fact is in the Agent's own words.",
1016
- access: "Read in place as text.",
1110
+ summary: "A technical string cut in the middle, so its start and its end both stay readable.",
1111
+ when: "“Use Middle Truncate for strings whose head and tail both carry information: file paths (`apps/…/page.tsx`), URLs, deployment IDs (`dpl_…abc123`), commit hashes, branch names with prefixes.” (https://vercel.com/geist/middle-truncate) Ours: an environment variable is one of these too.",
1112
+ instead: "“For prose, descriptions, and headings, use end-truncation with `…` instead; cutting the middle of a sentence destroys meaning.” (https://vercel.com/geist/middle-truncate) “For any truncated value the user might need verbatim, pair with a `Tooltip` showing the full string or a copy-on-click affordance.” (https://vercel.com/geist/middle-truncate) Ours: a value someone may need exactly is `TextWithCopyButton` or sits beside a `CopyButton`; while the middle is cut, the whole value is also the element's `title`.",
1113
+ behaviour: "“Middle Truncate renders a single ellipsis glyph (`…`) rather than three periods. This keeps monospace values, including environment variable keys, IDs, hashes, and paths, from reserving three character cells for the truncation marker.” (https://vercel.com/geist/middle-truncate) “The component listens to its container width, so layouts that change width on hover (expanding cards, animated rows) cause the truncation point to jitter. Lock the width during interaction.” (https://vercel.com/geist/middle-truncate) “Copying truncated text yields the full original string from the underlying value, not the visible ellipsis form. Confirm this stays true if wrapping with custom `onCopy`.” (https://vercel.com/geist/middle-truncate) “Don’t wrap Middle Truncate in another `text-overflow: ellipsis` container; the two strategies fight each other and the inner ellipsis wins inconsistently.” (https://vercel.com/geist/middle-truncate) “Keep the visible string long enough on small viewports that the head still identifies the resource (path segment, ID prefix).” (https://vercel.com/geist/middle-truncate) Ours: it fills the width its parent gives and shows the whole value when it fits. Too narrow, the last third of the value, at most 16 characters, stays whole. While the middle is cut, copying a selection made inside it puts the whole value on the clipboard; a value shown in full copies only what is selected. A custom `onCopy` runs first, and `preventDefault` in it keeps the value as selected.",
1114
+ words: "Ours: the value as the system writes it, unchanged; the ellipsis comes from the layout, never from the text.",
1115
+ access: "“Expose the full string to assistive tech via the wrapping element’s accessible name (the component already keeps the full value in the DOM for copy).” (https://vercel.com/geist/middle-truncate) “Avoid Middle Truncate inside focusable controls without an explicit `aria-label`; the ellipsis on its own gives screen readers nothing to announce.” (https://vercel.com/geist/middle-truncate) Ours: the whole value is the element's text, read in full; inside a link or a button, the control carries the `aria-label` that names the target.",
1017
1116
  },
1018
1117
  {
1019
1118
  name: "ObjectFacts",
1020
1119
  module: "object-workspace",
1021
1120
  family: "Data",
1022
1121
  summary: "An object's facts as labelled values in a grid, inside one of its sections.",
1023
- when: "Read-only values that describe an object, inside an ObjectSection: a company's legal name, VAT number and address.",
1122
+ when: "“Use `<Description>` for definition-list metadata: a short Title Case key paired with a single value (`Last Deployed`, `Region`, `Plan`). For inline help under a form field, use the input’s helper-text slot instead.” (https://vercel.com/geist/description) Ours: read-only values that describe an object, inside an ObjectSection: a company's legal name, VAT number and address. One fact alone is a description: a short label over its value.",
1024
1123
  instead: "Facts under the object's name belong to ObjectPage's `facts`; values a person changes are SettingsFields in a SettingsSection; figures compared across records are a DataTable.",
1025
- behaviour: "Two columns from `sm`, one below; `compact` tightens the gaps for a dense card; a `block` fact takes the full width and keeps its line breaks.",
1026
- words: 'A label is a noun ("Billing email"); a value is the fact itself, with no unit the label already says.',
1027
- access: "A description list: each label is its value's term and names it.",
1124
+ behaviour: "Two columns from `sm`, one below; `compact` tightens the gaps for a dense card; a `block` fact takes the full width and keeps its line breaks. A fact is its label (14/14 px, muted) over its value (14/16 px, medium, ink) 8 px apart in a 16 px label line, 40 px in all; every fact has both. `right` aligns a fact to the trailing edge, and `ellipsis` cuts the label and the value to one line, the value whole in its title. Neutral throughout. “Pass `tooltip` only when the title alone is ambiguous and a one-sentence definition resolves it. The tooltip text is sentence case and ends with a period.” (https://vercel.com/geist/description) Ours: the `tooltip` stands behind an info icon after the label that a keyboard reaches; the value never carries it.",
1125
+ words: '“Title slot is Title Case noun (`Last Deployed`, `Build Duration`); content is sentence case unless the value is a literal identifier, ID, or timestamp that should be preserved verbatim.” (https://vercel.com/geist/description) Ours: a label is a short sentence case noun ("Billing email"), not Geist\'s Title Case; a value carries no unit the label already says.',
1126
+ access: "“Geist renders `<dl>`/`<dt>`/`<dd>` so screen readers announce each key/value pair as a definition. Don’t wrap the component in extra paragraphs that break the list semantics.” (https://vercel.com/geist/description) “Don’t put interactive controls in the title slot. Buttons, menus, and links belong in the content (`<dd>`) or in the parent layout, not the label (`<dt>`).” (https://vercel.com/geist/description) Ours: each label is its value's term and names it; only the tooltip's own trigger sits in a label.",
1028
1127
  },
1029
1128
  {
1030
1129
  name: "PairingCode",
@@ -1042,11 +1141,11 @@ export const USAGE = [
1042
1141
  module: "progress",
1043
1142
  family: "Data",
1044
1143
  summary: "A known amount against a real maximum, with its label and value.",
1045
- when: "A known amount against a real maximum, with its label and value: included runs used against the plan's allowance.",
1046
- instead: "An unknown wait is the waiting line, never a bar with an invented percentage. A run's consumption in a dense spot is a `UsageMeter`. A single figure is a `KpiCard`.",
1047
- behaviour: "A 4 px track in the `line` colour. The fill moves only when the reading changes, on `open`; still under reduced motion. `tone` is the product's threshold, never derived from the value. Past the maximum the bar fills and turns red, whatever the tone.",
1048
- words: 'Print the work and its unit in `ProgressValue`: "620 of 1,000".',
1049
- access: "A `progressbar` named by its `ProgressLabel` or `aria-label`, with its value.",
1144
+ when: '"Determinate work whose total is knowable, like file uploads, multi-step setup, build steps, or batch deletions." (https://vercel.com/geist/progress) "Use `max` for the real ceiling (`max={files.length}`), not a hardcoded `100`. The component computes the percentage from `value / max`." (https://vercel.com/geist/progress) Ours: it always has a `value` and a name, as `aria-label` or as `aria-labelledby` pointing at its `ProgressLabel`, and the type asks for both. `stops` mark genuine stages, each named. `size` is `md` (10 px), or `sm` (4 px) inside a table cell or a row.',
1145
+ instead: '"For short indeterminate waits (~1–3s), use `Spinner`; for inline copy like `Saving`, use `Loading Dots`." (https://vercel.com/geist/progress) Ours: neither exists; an unknown wait is the waiting line, never a bar with an invented percentage. "For usage against a quota or ratio, use `Gauge`; the circle reads as health, the bar reads as progress." (https://vercel.com/geist/progress) Ours: a run\'s consumption in a dense spot is a `UsageMeter`. A single figure is a `KpiCard`.',
1146
+ behaviour: '"Threshold colors via `dynamic colors` should mirror the same breakpoints used elsewhere (warning at the same threshold a quota note fires)." (https://vercel.com/geist/progress) Ours: colour sits on the fill alone: the track is always the neutral line colour and the words beside the bar stay neutral. The fill is neutral by default; `tone` is that threshold (amber where the note fires, red past the cap), never derived from the value. Past the maximum the bar fills and turns red, whatever the tone. The fill\'s end is rounded, and square when `stops` cut the bar. The fill moves only when the reading changes, on `open`; still under reduced motion. The printed value is always the live reading. "Use stops for genuine multi-stage work and label the stage next to the bar (`Step 2 of 4 · Building`); a stop with no label is decorative noise." (https://vercel.com/geist/progress)',
1147
+ words: '"Pair the bar with text naming the work and units: `Uploading 12 of 30 files`, `Building · 1.2 GB / 4 GB`. The bar alone doesn’t say what’s progressing." (https://vercel.com/geist/progress) "Don’t append `successfully` or `complete` once the bar fills; swap to a completion state (toast, success row, redirect)." (https://vercel.com/geist/progress) "For long operations, name the work in the surrounding copy (`Building deployment…`) instead of leaving a bare percentage." (https://vercel.com/geist/progress) Ours: print the work and its unit in `ProgressValue`.',
1148
+ access: '"Component sets `role="progressbar"` with `aria-valuemin`, `aria-valuemax`, and `aria-valuenow`; pass an accessible name through `aria-label` on the wrapper or a sibling `<label>` tied with `aria-labelledby`." (https://vercel.com/geist/progress) "Throttle `aria-valuenow` updates to roughly once a second so screen readers don’t announce every increment of a fast upload." (https://vercel.com/geist/progress) Ours: the printed value is live, and the value it announces follows it at most once a second. "Stops accept their own `ariaLabel`; name each one (`Build complete`, `Tests complete`) so the bar is navigable without sight." (https://vercel.com/geist/progress) Ours: a stop\'s name is its `label`. With `stops`, the stage reached leads the spoken value; a stop is not focusable, because a progressbar\'s children are presentational.',
1050
1149
  },
1051
1150
  {
1052
1151
  name: "QrCode",
@@ -1064,11 +1163,11 @@ export const USAGE = [
1064
1163
  module: "record-time",
1065
1164
  family: "Data",
1066
1165
  summary: "When something happened to a record: a relative time in the sentence that says what, or the time of day in a feed's time column.",
1067
- when: 'The moment a record was made, changed or last ran, in its row or its facts ("Updated 3 hours ago"), and the time of each event in an ActivityFeed or a RunTimeline step (`mode="clock"`).',
1068
- instead: 'A date a person picks is a Calendar; a span of days a page reads is a DateRangeControl; a run\'s duration is a fact written by the product ("Took 4 min").',
1069
- behaviour: "Relative within a week, then the day and month, with the year only when it is another year; `mode=\"clock\"` prints the time of day in the viewer's zone in tabular figures. Hovering either shows the exact moment in UTC and in the viewer's zone. A page rendered on a server passes the viewer's `timeZone` when it knows it, so the server and the browser write the same time; otherwise the server and the hydrating page write UTC and the page then rewrites it in the browser's zone. A time that names no moment draws nothing.",
1070
- words: 'The product writes the sentence through `phrase` ("Updated {when}", "Ran {when}"); the clock needs no words. The package formats every date with the person\'s `locale`.',
1071
- access: "A `<time>` element with the machine-readable moment; assistive technology reads the exact local moment after the visible text.",
1166
+ when: '"Use `<RelativeTimeCard>` anywhere a recent timestamp appears in scannable surfaces: `Table` cells, `Entity` rows, deploy lists, activity feeds." (https://vercel.com/geist/relative-time-card) Ours: `RelativeTimeCard` is `RecordTime`: the moment a record was made, changed or last ran, in its row or its facts, and the time of each event in an ActivityFeed or a RunTimeline step (`mode="clock"`).',
1167
+ instead: '"For a static date in body prose past 7 days, render `Mar 14, 2026` directly." (https://vercel.com/geist/relative-time-card) Ours: a date a person picks is a Calendar; a span of days a page reads is a DateRangeControl; a run\'s duration is a fact written by the product (*Took 4 min*).',
1168
+ behaviour: "\"The hover card already shows absolute UTC and local time, so don’t duplicate that copy elsewhere on the row.\" (https://vercel.com/geist/relative-time-card) Ours: relative within a week, then the day and month, with the year only when it is another year; `mode=\"clock\"` prints the time of day in the viewer's zone in tabular figures. Hovering either shows a card that opens above after 150 ms, 13 px from the time, with a stem: the relative reading first, then the exact date and time in UTC and in the viewer's zone, each behind a zone chip, the viewer's by its offset (*GMT+2*). A page rendered on a server passes the viewer's `timeZone` when it knows it, so the server and the browser write the same time; otherwise the server and the hydrating page write UTC and the page then rewrites it in the browser's zone. A time that names no moment draws `children`, a state such as *Pending*, or nothing.",
1169
+ words: '"Pass `date` as a number (Unix ms or epoch)." "Don’t pre-format the value, and don’t replace `children` with a formatted string." "The component’s short formatter is the canonical form (`2m`, `5h`, `Yesterday`)." "Don’t append `ago` after the component." "The formatter already produces `2m ago` / `5h ago`, so `<RelativeTimeCard /> ago` reads as `2m ago ago`." "Use `children` only for non-time labels (`Just now`, `Pending`, `Queued`) where the formatter can’t describe the state." "Pair with a leading label when the row is ambiguous on its own: `Last deploy <RelativeTimeCard date={ts} />`." (https://vercel.com/geist/relative-time-card) Ours: `at` takes a Date, epoch milliseconds or an ISO string, never a display string such as *3 hours ago*. The reading is the locale\'s long form (*3 hours ago*, *12 Sep*), not `2m`. The product writes the sentence through `phrase` (*Updated {when}*, *Ran {when}*), so *ago* is never added after it; the clock needs no words. The package formats every date with the person\'s `locale`.',
1170
+ access: "Ours: a `<time>` element with the machine-readable moment; assistive technology reads the exact local moment after the visible text.",
1072
1171
  },
1073
1172
  {
1074
1173
  name: "RunTimeline",
@@ -1085,44 +1184,44 @@ export const USAGE = [
1085
1184
  name: "SettingsFactsSection",
1086
1185
  module: "settings-facts",
1087
1186
  family: "Data",
1088
- summary: "Read-only facts about one object, framed as a card with a title and one action.",
1187
+ summary: "Read-only facts about one object, framed as a card with a title, a line, one action and a footer.",
1089
1188
  when: "Read-only facts about one object: who it is, where it belongs, its identifiers. `SettingsFactsSection` frames them as a card with a title and one action; `ObjectFacts` is the list alone.",
1090
- instead: "Facts a person changes are a `SettingsSection` with its own Save. Facts compared across records are a table. A relation is its identity or a `Tag`, never its own card.",
1091
- behaviour: 'Label above value, two equal columns, one column on a phone. Every label stays when its value is missing, as *—*. A long value spans both columns. No fact gets a box of its own. In a side column, `presentation="summary"` drops the card.',
1092
- words: "Labels are nouns, sentence case, no colon: *VAT number*. Values are exact: identifiers in mono with a copy button, dates in the person's locale.",
1093
- access: "A `dl` of `dt` and `dd`, each value named by its label. The section is labelled by its heading, which also takes focus when an editor closes. A dash carries screen-reader text.",
1189
+ instead: "Facts a person changes are a `SettingsSection` with its own Save. One server-given value, an ID, is a `SettingsSection` with the value in a read-only mono `InputGroup` and its copy button, no Save. Facts compared across records are a table. A relation is its identity or a `Tag`, never its own card.",
1190
+ behaviour: '"Use `<Description>` for definition-list metadata: a short Title Case key paired with a single value (`Last Deployed`, `Region`, `Plan`)." (https://vercel.com/geist/description) Ours: required: the description and the footer\'s left line (`footerHint`), as on a `SettingsSection`; `footer` holds actions, never a Save. Label above value, two equal columns, one column on a phone. Every label stays when its value is missing, as *—*. A long value spans both columns. No fact gets a box of its own. In a side column, `presentation="summary"` drops the card, the description and the footer line.',
1191
+ words: '"Title slot is Title Case noun (`Last Deployed`, `Build Duration`); content is sentence case unless the value is a literal identifier, ID, or timestamp that should be preserved verbatim." (https://vercel.com/geist/description) Ours: labels are nouns in sentence case, no colon: *VAT number*. Identifiers in mono with a copy button, dates in the person\'s locale. The footer line says what the facts are for or links to the docs.',
1192
+ access: '"Geist renders `<dl>`/`<dt>`/`<dd>` so screen readers announce each key/value pair as a definition." (https://vercel.com/geist/description) "Don’t put interactive controls in the title slot." (https://vercel.com/geist/description) Ours: a `dl` of `dt` and `dd`, each value named by its label. The section is labelled by its heading, which also takes focus when an editor closes. A dash carries screen-reader text.',
1094
1193
  },
1095
1194
  {
1096
- name: "StatusRow",
1097
- module: "status-row",
1195
+ name: "Snippet",
1196
+ module: "snippet",
1098
1197
  family: "Data",
1099
- summary: "One line of a status panel: a mark, what it is, and controls that show on hover.",
1100
- when: "Lines inside a side panel that report what is running or connected, grouped under a collapsible `StatusSection`.",
1101
- instead: "A collection's row is `RecordList`'s. A row with a sentence and actions is `Item`.",
1102
- behaviour: "When `onActivate` is given the whole row is one button that takes the accent hover; the trailing controls show on hover or focus unless `trailingVisible`. A `StatusSection` starts collapsed.",
1103
- words: "The row names the thing; the section's label is a plural noun.",
1104
- access: 'An activatable row is `role="button"`, reached by Tab and run by Enter or Space. The section\'s toggle exposes `aria-expanded`.',
1198
+ summary: "One command a person copies and runs in a terminal.",
1199
+ when: "“Use Snippet for a runnable shell command the user is meant to copy. For inline tokens (env var names, paths) use `InlineCode`, and for multi-line source use `CodeBlock`.” (https://vercel.com/geist/snippet) Ours: a command or a short sequence of commands to paste into a terminal: an install, a sign-in, a pairing step.",
1200
+ instead: "Ours: a value that is not a command (an identifier, a URL, a checksum) is `TextWithCopyButton`. A script someone reads before running is a `CodeBlock`. A token inside a sentence is inline code.",
1201
+ behaviour: '“Pass the command as `text`. Authors never type a leading `$`; the component renders the prompt, so `text="$ vercel deploy"` displays as `$ $ vercel deploy`.” (https://vercel.com/geist/snippet) “Set `prompt={false}` for non-shell content (URLs, JSON, output you want copied verbatim) so the rendered string matches what gets copied.” (https://vercel.com/geist/snippet) “Keep one command per Snippet. Pass an array to `text` for a short multi-line block; for longer scripts, switch to `CodeBlock` so users can read before they copy.” (https://vercel.com/geist/snippet) “Use `copyText` only when `text` contains rich nodes (`<span>` highlights, conditional fragments) and the clipboard payload should be plain text. For a string `text`, `copyText` is redundant.” (https://vercel.com/geist/snippet) “Use the `copied` prop to control the copy button animation externally. This lets you copy different text (e.g. from a context card) while reusing the snippet\'s checkmark feedback.” (https://vercel.com/geist/snippet) “Use `copied` plus an `onCopy` callback when a parent surface (a card, a tooltip) needs to show the same checkmark feedback while copying different text.” (https://vercel.com/geist/snippet) Ours: an array of strings is copied joined by line breaks, one row each after its own prompt; the prompt is never copied. The shell is as wide as its text, up to its row (`className="w-full"` fills the row); a longer line scrolls sideways with no scrollbar. `text=""` with a `placeholder` shows the placeholder, copies nothing and has no button. `text` that is an element needs `copyText`; a string needs none. `onCopy` reports a copy once the clipboard confirms. `variant` puts one colour on the whole shell, its text and the copy mark together: `info`, `warning` or `destructive` for a command inside a note of that kind, `inverted` for a dark block on a light page; the default is neutral, which is what almost every snippet is. Hovering the copy mark dims it and draws a 1 px ring; it never takes a fill or leaves the shell\'s text colour.',
1202
+ words: '“Pair `placeholder` with `text=""` for an empty state. Sentence case, no trailing period, no `Please`: `Run vercel link to fetch env vars`. The placeholder is informational, not copied.” (https://vercel.com/geist/snippet) Ours: the command as typed. The placeholder example is *Run nadia login to connect this computer*. `copyLabel` names the command (*Copy install command*); `copiedLabel` and `copyFailedLabel` are required with it.',
1203
+ access: "Ours: the text is read as text; the prompt is hidden from assistive technology. The button is named by `copyLabel`, and the result is read once from its status.",
1105
1204
  },
1106
1205
  {
1107
1206
  name: "Table",
1108
1207
  module: "table",
1109
1208
  family: "Data",
1110
- summary: "The native table primitive every comparison table is drawn with.",
1111
- when: "A fixed table a composition draws itself: a summary inside a card, a list of plan limits. A table of records is `DataTable`, which draws this one.",
1112
- instead: "Records with sorting, paging and an empty state are `DataTable`. A descriptive list is `ItemGroup`. One object's facts are `ObjectFacts`.",
1113
- behaviour: "Headings visible; numbers trail in tabular figures. Row hover takes `accent`; headers and footers never highlight. `TableDetailRow` and `tableCompactLayouts` move facts into a detail row on a narrow card instead of scrolling sideways.",
1114
- words: "Headings are nouns with their unit: *Cost (€)*, *Tokens*. Italian headings run a third longer and never wrap.",
1115
- access: 'A native `table` with a caption or `aria-label`; `th scope="col"`. A missing value is `TableValue`\'s hidden dash, never blank.',
1209
+ summary: "The native table every comparison is drawn with: a head row, figure rows of one line, and cells padded the same at every width.",
1210
+ when: "“Use `<Table>` for tabular data where rows share the same shape and at least one column is sortable or comparable across rows.” (https://vercel.com/geist/table) Ours: a fixed table a composition draws itself: a summary inside a card, a list of plan limits, figures compared down a column. A table of records is `DataTable`, which draws this one.",
1211
+ instead: "“For a row of descriptive content paired with a single action (membership row, integration row), use `Entity` instead.” (https://vercel.com/geist/table) “For a key/value metadata block on a detail page, use `Description`, not a two-column table.” (https://vercel.com/geist/table) “When the underlying list is empty (filter cleared, never created), render `Empty State` outside the table rather than an empty `<Table.Body>`.” (https://vercel.com/geist/table) Ours: Geist's `Entity` is `Item`, or `RecordList` when the row names a record; its `Description` is `ObjectFacts`; its `Empty State` is `EmptyState`. Records with sorting, paging and an empty state are `DataTable`.",
1212
+ behaviour: "A head row whose labels fill 36 px, with 12 px of room and then a 1 px rule under them, so the first row starts at the rule like every other; body rows 40 px, with no rule between them unless the body is `bordered`, which draws it straight across the row, under the corners. The first and last cell of a row take a 6 px corner, so a stripe or a highlight reads as a rounded bar. The last column, its heading and its cells, aligns to the trailing edge. `striped`, `bordered` and `interactive` belong to `TableBody`: stripes fill the odd rows with `muted` (a selected row there takes `accent`), `bordered` draws a rule between rows (not after the last) and no frame, and an interactive row takes `accent` on hover, on focus within and while its menu is open, over a stripe; a row that opens nothing is not `interactive`. Headers and footers never highlight and carry no fill; a footer has a rule above it and ink, medium text. A column's width is a `TableCol` in a `TableColgroup`. `density=\"comfortable\"` is the entity row of a record that names itself. On a card narrower than its `compactDetails` layout a row never scrolls sideways: its secondary columns leave the row and their values stand in the `TableDetailRow` under it, one muted line joined by a middle dot that ends in an ellipsis, their labels read by assistive technology only, as Vercel's lists read on a phone; a `Tag` there reads as its plain text, and a fact that `wraps` lets the line wrap whole instead of ending in an ellipsis. An entity row (`density=\"comfortable\"`) keeps its record on one 72 px row instead: its facts join the line under the name that `RecordIdentity` draws (`TableCompactLine`), so the row is the name over one line. Colour is neutral throughout: headings and cells share the body grey and a heading differs by weight, the footer is ink, and the hue of a state lives in the `StatusBadge` of a cell, never in the row. “Sortable column headers are buttons. The visible label stays Title Case; the sort-direction arrow is decorative and the button announces the next sort state to assistive tech.” (https://vercel.com/geist/table) Ours: no heading sorts the rows; the order is the page's, through a `CollectionSort` above the list. “Apply `tabular-nums` (or Geist Mono) to numeric columns so digits align across rows for comparison.” (https://vercel.com/geist/table) Ours: `DataTable` sets it on a numeric column; in a `Table` a figure's cell takes `tabular-nums`.",
1213
+ words: "“Column headers (`<Table.Head>`) are Title Case nouns or noun phrases: `Last Used`, `Requests (7d)`, `Created`, `Status`. Never sentences.” (https://vercel.com/geist/table) “Render `—` in cells where a value is unknown or not applicable. Don’t substitute `N/A`, `null`, or an empty string.” (https://vercel.com/geist/table) “Use the canonical short relative-time form in cells (`2m ago`, `5h ago`); switch to `Mar 14, 2026` past 7 days. See `Relative Time Card`.” (https://vercel.com/geist/table) Ours: headings are sentence case nouns with their unit, *Cost (€)*, *Tokens*, where Geist writes Title Case. Italian headings run a third longer and never wrap. A missing value is `TableValue`'s dash, read as its `missingLabel`. The table is always named: its `aria-label` says what it lists (*Usage by Agent*).",
1214
+ access: 'A native `table` named by `aria-label` or `aria-labelledby`, which the types require, or `aria-hidden` when it is only the placeholder of a loading view; `th scope="col"`, with `aria-sort` on the column the rows are ordered by.',
1116
1215
  },
1117
1216
  {
1118
1217
  name: "TableCard",
1119
1218
  module: "table-card",
1120
1219
  family: "Data",
1121
- summary: "The frame a collection's table sits in: a heading, the collection's own toolbar under it, the table, a labelled total, and the recessed footer strip.",
1220
+ summary: "The frame a collection's table sits in: a heading with the collection's own toolbar at its trailing edge, the table, a labelled total, and the recessed footer strip.",
1122
1221
  when: "A table of records in a card: its heading, its own toolbar, the table, a labelled total and the recessed footer strip.",
1123
1222
  instead: "An entity collection that is the page's whole job is `CollectionPage`. A chart is `ChartCard`. One object's facts are `SettingsFactsSection`.",
1124
- behaviour: "The heading, the toolbar and a separator come first when given; the summary row is a labelled total under the table; the footer strip holds the hint on the left and the count or paging on the right.",
1125
- words: "The title names the collection. The footer is a count or the paging; the summary is a labelled total. Never a help sentence in the footer.",
1223
+ behaviour: "The heading and a separator come first when given, the toolbar on the heading's row at its trailing edge, title left and controls right, as Vercel heads a card's list; on a card under 576 px the toolbar takes its own line under the title. The table's first and last columns stand in from the card's edges by the card's spacing, so the first column lines up with the title. The summary row is a labelled total under the table, padded like a table footer so its label lines up with the first column and set in ink, medium text; the footer strip holds the hint on the left and the count or paging on the right. A `note` about the whole list stands in a strip of its own under that one: one muted line with an optional *Learn more* at the left and one secondary action at the right, as Vercel ends a deployments list (\"Deployment Retention is enabled\", *View Recently Deleted*).",
1224
+ words: "“Pagination labels are `Previous` and `Next`. Page-count copy reads `Page 2 of 7` or `21–40 of 142` with an en-dash inside the range.” (https://vercel.com/geist/table) Ours: the title names the collection. The footer is a count or the paging; the summary is a labelled total. Never a help sentence in the footer.",
1126
1225
  access: "A `section` whose title is an `h2`; the summary is a `dl`.",
1127
1226
  },
1128
1227
  {
@@ -1132,9 +1231,20 @@ export const USAGE = [
1132
1231
  summary: "One cell's value in a table, in each state a reading can be in.",
1133
1232
  when: "Every cell of a `DataTable` whose reading can be missing, zero, out of date, unreadable or still loading.",
1134
1233
  instead: 'A whole table that failed to read is `EmptyState kind="failed"`; a figure on its own is a `KpiCard`.',
1135
- behaviour: 'Five states that never collapse into a dash: unknown (a muted dash), zero (the figure "0"), stale (the reading, then the stale word), failed (the warning mark and its words) and loading (a still 64 × 12 placeholder). A real value, `0` included, is shown as it is.',
1136
- words: '`missingLabel` says what the absence means ("Not reported", never "N/A" or "None"); `staleLabel` is one word ("Stale"); `failedLabel` says the read failed ("Couldn\'t read").',
1137
- access: "The dash is hidden and `missingLabel` is read in its place; the warning mark is hidden beside its words; the placeholder is decorative, and the table is `aria-busy` while it loads.",
1234
+ behaviour: '“Render `—` in cells where a value is unknown or not applicable. Don’t substitute `N/A`, `null`, or an empty string.” (https://vercel.com/geist/table) Ours: five states that never collapse into a dash: unknown (a muted dash), zero (the figure "0"), stale (the reading, then the stale word), failed (the error mark and its words, in the error colour) and loading (a still 64 × 12 placeholder). A real value, `0` included, is shown as it is.',
1235
+ words: 'Ours: `missingLabel` says what the absence means ("Not reported", never "N/A" or "None"); `staleLabel` is one word ("Stale"); `failedLabel` says the read failed ("Couldn\'t read").',
1236
+ access: "The dash is hidden and `missingLabel` is read in its place; the error mark is hidden beside its words; the placeholder is decorative, and its cell is `aria-busy` while it loads.",
1237
+ },
1238
+ {
1239
+ name: "TextWithCopyButton",
1240
+ module: "text-with-copy-button",
1241
+ family: "Data",
1242
+ summary: "A short exact value with the button that copies it.",
1243
+ when: "A short value someone reads and pastes elsewhere: an identifier, a checksum, a slug, a server address.",
1244
+ instead: "Several lines are a `CodeBlock`; a command is a `Snippet`. A value that needs no copying is plain text. The button alone, beside text that is not the value, is `CopyButton`.",
1245
+ behaviour: "Shows the value in the body type, with a 24 px `CopyButton` whose 16 px mark sits 6 px after it. The value is slightly softer than the full text colour and the mark softer still; hovering either takes both to the full text colour, with no fill, border or shadow. Only the button copies: the value stays selectable, so a copy the clipboard refuses can be done by hand. A value too long for its row wraps at any character; `ellipsis` instead cuts it in the middle in one line (`MiddleTruncate`) and the whole value is still what copies. The result shows as the button's check mark, with no toast.",
1246
+ words: "`copyLabel` names what is copied (*Copy config digest*), never a bare *Copy*; `copiedLabel` says what was copied (*Config digest copied*); `copyFailedLabel` says it failed (*Couldn't copy*). All three are required.",
1247
+ access: "The value is text, read in full. The button is named by `copyLabel`; the result is read once from its status.",
1138
1248
  },
1139
1249
  {
1140
1250
  name: "UsageMeter",
@@ -1162,12 +1272,12 @@ export const USAGE = [
1162
1272
  name: "EmptyState",
1163
1273
  module: "empty-state",
1164
1274
  family: "Feedback",
1165
- summary: "What a place with nothing in it says: a first use shows its section's picture, a title, one line and the action that fills it; an empty list, a search that found nothing and a read that failed stay quiet.",
1166
- when: "A collection, a section or a page has nothing to show: it never had a record (`firstUse`), it is empty right now and fills by itself (`nothingYet`), the search or a filter found nothing (`noResults`), or its read failed (`failed`).",
1167
- instead: "A list's rows still loading are its skeleton rows, never an empty state. A failure of one record's own action is that record's row message. A failure that blocks the whole page, or a notice about the account, is a `NoticeBanner`.",
1168
- behaviour: "The frame follows the container: a page or a region stands on a resting card with its own edge, inside a card it takes none, and a collection always passes `card`. A first use's picture comes from the one approved set and arrives once, from faint and soft to sharp, after it loads, with no animation under reduced motion; nothing else moves. Only `firstUse` has a picture or a title; only `failed` has a mark, the warning, with its message as an alert and its actions, Retry among them, as outline buttons.",
1169
- words: 'A first use\'s title names what is missing ("No mailboxes yet") and its line says what will live there and how it fills, in one sentence. A quiet line is one sentence. Clear says what it clears: "Clear search", "Clear filters". A failure is the catalogue\'s message, never a string of the component\'s own.',
1170
- access: "A first use's title is a heading, level 2 or, inside a titled section, 3. The picture is decorative. A failure is announced as an alert.",
1275
+ summary: "What a place with nothing in it says, by its cause: never created, empty at the moment, nothing matched, finished, not allowed, or failed to load.",
1276
+ when: '"Fill spaces when no content has been added yet, or is temporarily empty due to the nature of the feature and should be designed to prevent confusion." (https://vercel.com/geist/empty-state) "A card used to communicate an error state with a title and message." (https://vercel.com/geist/error-card) "Pick the variant by what the user needs: no-results for a filtered list that returned zero rows, blank slate or informational for a resource the user hasn’t created, cleared for completed work, permission for role or tier denials, error for a failed load." (https://vercel.com/geist/empty-state) "Render the permission and tier-denial variants full-page when the user lands on a route they can’t view. Use a `Note` only when one tile inside an otherwise-accessible page is gated." (https://vercel.com/geist/empty-state) "Use the Error component as a block surface when a section or page-level resource failed to load: a panel, a dashboard card, a route boundary." (https://vercel.com/geist/error) Ours: pick the `kind` by the cause. `firstUse` is the blank slate and the informational variant: no record was ever made. `nothingYet`: empty now and it fills by itself. `noResults`: the search or filter found nothing. `cleared`: the work is done. `denied`: this viewer\'s role or plan does not open it. `failed`: the read failed; it is the Error and the Error Card, a failure inside a card with `scope="card"` and one that stands alone as a block with `page` or `region`. There is no educational kind.',
1277
+ instead: '"Don’t put critical persistent warnings here. Empty states vanish when the list populates; persistent warnings belong in `Note` or the page header." (https://vercel.com/geist/empty-state) "Pick `toasts.error()` for transient action failures (`Couldn’t save settings. Try again.`) and the `error` prop on `Input` for field-level validation. Don’t replace either with this block." (https://vercel.com/geist/error) Ours: `Note` is `MessageNotice` or `NoticeBanner`, `toasts.error()` is `showMessageToast` with the tone `error`, and the `error` prop on `Input` is `FieldError`. Rows still loading are skeleton rows. A failure of one record\'s action is that record\'s row message. One gated tile on an open page is a `MessageNotice`, so `denied` takes `page` or `region`, never `card`.',
1278
+ behaviour: '"The CTA must be a real `Button` or `Link`, not an `onClick` div, so it joins the tab order and exposes a role." (https://vercel.com/geist/empty-state) "Cap at one primary CTA, plus one secondary when the first action could legitimately be one of two paths (`Import Repository` and `Deploy Template`). Three CTAs is a smell." (https://vercel.com/geist/empty-state) "Don’t auto-launch a tour from the educational variant; pair `Start Tour` with `Skip`." (https://vercel.com/geist/empty-state) "The recovery action must do something concrete: a `Try Again` button when the operation is retry-safe, a named verb (`Reconnect GitHub`, `Update Payment Method`) when it isn’t." (https://vercel.com/geist/error) "Don’t auto-retry in the background; the user came to this surface to decide." (https://vercel.com/geist/error) "For full-page route errors (`error.tsx`), return focus to the `Try Again` button on appearance so a keyboard user can retry without hunting for it." (https://vercel.com/geist/error) "Always pair platform or system errors with a stable identifier (request ID `x-vercel-id`, deployment ID `dpl_…`, run ID, trace ID). Validation and permission denials are user-state, not system, and don’t need an ID." (https://vercel.com/geist/error) "Render the stable ID on a monospace sub-line under a collapsed `<details>` so the user can copy-paste it into a support thread." (https://vercel.com/geist/error) Ours: `firstUse` has the one picture, one action and at most one docs link under it; there is no educational kind, so no tour. A failure shows at most two of its message\'s actions, Retry first, and moves focus to the first one when it fills a page (`scope="page"`); its `reference` is the collapsed Disclosure holding the monospace identifier and a copy button. A page or a region stands on a resting card, inside a card it takes none, and a collection always passes `card`; a card that lists a few items, as a collection\'s rail does, passes `line`, where `nothingYet` and `cleared` are one 44 px row like an item: the message at the left, after the cleared check at 16 px. Every kind that carries a title, `failed` excepted, draws it the same way: a mark or picture, a 16 px medium title, one or two muted 14 px lines, 24 px apart, centred, the lines no wider than 340 px. `cleared` and `denied` carry a bordered 54 px tile with a muted icon, never a hue; the picture is `firstUse`\'s alone. The action is the filled primary button, as Vercel\'s Connect draws Create Connector in its empty card; the docs link is muted. A page or a region is padded 48 px above and below and 70 px at the sides (a first use narrows to 24 px under 640 px for its picture). `failed` is the one hued kind: on a page or a region its ground, line, mark and text are all in the error colour, with a 16 px regular title, 16 px padding and 8 px between its parts, and the buttons stay neutral; inside a card it takes the text and mark colour alone, because the card owns the edge and a second one would double it. Required: `firstUse` and `denied` take a title and a line, `firstUse` its picture, `failed` the catalogue message whose title is always drawn.',
1279
+ words: '"`title` is Title Case (`No Logs Match Your Filter`); `description` is sentence case and adds new information instead of restating the title." (https://vercel.com/geist/empty-state) Ours: a title is sentence case and names what is missing ("No mailboxes yet"), not Title Case; a quiet line is one sentence. "Quote a single typed query verbatim with curly quotes: `No logs match “${query}”. Clear the filter to see all logs.` For multi-facet filters use the plural template `No {Items} Match Your Filters` and suggest widening or clearing." (https://vercel.com/geist/empty-state) "Onboarding bodies name the next action that creates the first item: `Push to your Git repository to create your first one.` Tier-gated bodies follow `{Feature value} with the {Plan} plan.`" (https://vercel.com/geist/empty-state) "Error variant pairs the body with a copyable request ID and a `Try Again` button." (https://vercel.com/geist/empty-state) "CTA labels are Title Case `Verb + Noun`. Never `Get Started`, `Continue`, or `OK`." (https://vercel.com/geist/empty-state) Ours: an action label is sentence case, verb and noun ("Clear filters", "New Agent"), never "Get started" or "OK". "State what happened and what to do next, in that order. Cut apologetic preambles (`Unfortunately`, `Oops`, `We’re sorry`)." (https://vercel.com/geist/error) "Use `Couldn’t` or `Can’t` for user-state errors (`Couldn’t verify your passkey. Try again.`); use `Failed to` for system or infra errors that mirror CLI output (`Build failed. Bundle exceeds 50 MB.`). `Unable to` is banned." (https://vercel.com/geist/error) "Don’t fall back to `Something Went Wrong` as a title; name the resource that failed (`Couldn’t Load Page`, `Couldn’t Load Deployments`)." (https://vercel.com/geist/error) "Never humor an error. Users hitting an error are frustrated; insincere copy makes it worse." (https://vercel.com/geist/error) Ours: a failure is the catalogue\'s message, never a string of the component\'s own, and its title names what failed to load.',
1280
+ access: '"After an async filter change, wrap the region in `aria-live="polite"` so screen readers announce the new state." (https://vercel.com/geist/empty-state) "When the error appears asynchronously (after a failed fetch), wrap the region in `aria-live="polite"` so it’s announced. Reserve `aria-live="assertive"` for true blocking errors that interrupt input." (https://vercel.com/geist/error) Ours: a title, a failure\'s included, is a heading, level 2 or, inside a titled section, 3. The picture is decorative. A failure\'s message is a `role="status"` line, polite, never an assertive alert, because it arrives after a read, not in the middle of input. `nothingYet`, `noResults` and `cleared` are plain lines with no live role of their own: the wrapper that stays mounted while rows and absence swap (the `aria-live="polite"` region, such as `SkeletonRegion`) announces them, once.',
1171
1281
  },
1172
1282
  {
1173
1283
  name: "EntryProgress",
@@ -1195,12 +1305,12 @@ export const USAGE = [
1195
1305
  name: "HoverCard",
1196
1306
  module: "hover-card",
1197
1307
  family: "Feedback",
1198
- summary: "A preview of a few facts about something named in a dense row.",
1199
- when: "A preview of a few facts about something named in a dense row: the exact moment behind a relative time, a record's summary.",
1200
- instead: "For one phrase use `Tooltip`. For anything to edit or keep open use a `Sheet` or the record's page. Never the only way to an action.",
1201
- behaviour: "Opens on hover and on focus, below by default, 4 px off; stays while the pointer is on it. Solid floating surface, 12 px padding.",
1202
- words: "Facts, not prose: the date in words, the time with its zone, the machine timestamp. The trigger keeps its verb sentence: *Updated 3 hours ago*.",
1203
- access: "The trigger is a real element with its own name (a `time` with its `datetime` for a moment), so the facts never depend on hover. Keyboard opening is tested, not assumed.",
1308
+ summary: "A card of a few facts about something named in a dense row, shown on hover or focus.",
1309
+ when: '"Use Context Card to reveal entity metadata on hover or focus: a user, a deployment, a project, an API key." "The trigger is usually a name link or avatar in dense data." (https://vercel.com/geist/context-card) Ours: `Context Card` is `HoverCard`. The exact moment behind a relative time is also a card. An entity\'s card is a `HoverCardSummary`.',
1310
+ instead: '"For a one-line "why" without metadata rows, use `Tooltip`." "For long-form content, edit forms, or persistent navigation, route to a `Drawer` or a detail page." "Don’t use Context Card to surface destructive actions; the panel can dismiss on cursor exit before the user commits." "Don’t nest a Context Card inside a Tooltip or another Context Card; the second layer steals focus and traps keyboard users." (https://vercel.com/geist/context-card) Ours: `Drawer` is `Sheet`. A card is never the only way to an action.',
1311
+ behaviour: '"Context Card opens on hover and keyboard focus and closes on cursor exit or blur." "Preserve the ~150ms entry delay so it doesn’t flash on a fast mouse sweep." "Cap interactive content at one primary action (`View Project`, `Open Settings`)." "More than one CTA reads as a menu and belongs in `Menu`." "Use `align` to position the card at the start, center, or end of the trigger." "Use `render` to set the trigger as a custom element, such as a link, button, or badge." (https://vercel.com/geist/context-card) Ours: the card stays open while the pointer is on it and opens above by default with a stem pointing at the trigger. `HoverCardContent` requires its facts as children. Colour: one neutral surface with no hue; the only accent a card carries is its link, in the outline button.',
1312
+ words: '"Lead with the entity name as a Title Case heading and one identifying line in sentence case underneath (team slug, owner, deployment URL)." "Follow with 2–4 metadata rows of `Label: value`." "Keys are Title Case noun phrases (`Last Active`, `Created`, `Plan`); values follow the same formatting rules as table cells." "Use the em-dash character `—` for unknown values, never `N/A` or `null`." "Don’t restate what the trigger already showed." "If the row already renders the deployment URL, don’t repeat it as the first card line." (https://vercel.com/geist/context-card) Ours: the name and the keys are in sentence case (*Last active*). A time\'s card leads with the relative label, and its trigger keeps its verb sentence: *Updated 3 hours ago*.',
1313
+ access: '"The trigger keeps its own accessible name; the card is supplementary and shouldn’t replace it." "Card content is reachable by keyboard once the trigger has focus; Escape closes the card and returns focus to the trigger." (https://vercel.com/geist/context-card) Ours: the trigger is a real element (a `time` with its `datetime` for a moment), so the facts never depend on hover. Keyboard opening is tested, not assumed.',
1204
1314
  },
1205
1315
  {
1206
1316
  name: "LabelledPicture",
@@ -1209,7 +1319,7 @@ export const USAGE = [
1209
1319
  summary: "A failure page's picture with its label, the way a museum labels a work.",
1210
1320
  when: "A page that cannot show what was asked for: an address or record that is not there (absence), or a request the product stopped (fault). `PictureLabel` is its label.",
1211
1321
  instead: "For one region that failed inside a working page use `EmptyState failed`; the rest of the page stays. Never turn the whole page into an error because one card failed.",
1212
- behaviour: "Picture 256 px, 320 from sm, 384 from lg. The label moves beside the base at lg with an empty column of the same width opposite, so the picture keeps the centre line. Still.",
1322
+ behaviour: "Picture 256 px, 320 from sm, 384 from lg. The label stands centred under the picture at every width, on the same centre line as the words below. Still.",
1213
1323
  words: "Never apologise, never name the mechanism. The label's title says what the picture is doing; the second line gives its materials and year.",
1214
1324
  access: "The picture is decorative; the label is a `figcaption`, read once. The page's heading is its `h1`; its actions are real links.",
1215
1325
  },
@@ -1218,31 +1328,31 @@ export const USAGE = [
1218
1328
  module: "message-notice",
1219
1329
  family: "Feedback",
1220
1330
  summary: "A catalogue message about one thing on the screen.",
1221
- when: "A catalogue message about one thing on the screen: under the control whose write failed (`action`), or atop the section whose condition lasts (`section`).",
1222
- instead: "For a whole page or the account use `NoticeBanner`. For a finished action use a toast. For a field's wrong value use `FieldError`. For a failed read use `EmptyState failed`.",
1223
- behaviour: "Tone `destructive` by default, `warning` or `info`. Its actions are outline buttons. Long words wrap, nothing truncates. A successful retry removes it. A `section` message with no action is drawn as the passage under a control, because a banner always carries its fix.",
1224
- words: "From the catalogue only. Title says what happened to which object; body only when it adds something. Action labelled with its result: *Try again*, *Sign in*.",
1225
- access: '`action`: `role="alert"` (`status` for info). `section`: `role="status"`, `aria-live="polite"`, atomic. Focus stays where the person was.',
1331
+ when: '"Display text that requires attention or provides additional information." (https://vercel.com/geist/note) "Use a Note for inline contextual feedback next to the field, card, or section it describes: a region change warning above a region picker, a rate-limit notice next to a usage gauge." (https://vercel.com/geist/note) "Choose `variant` by meaning: `error` for a problem the user must fix, `warning` for a consequence to acknowledge, `success` for a passed check, `secondary` for neutral information." (https://vercel.com/geist/note) Ours: a message beside the field, card or section it describes, under the control whose write failed (`action`), or atop the section whose condition lasts (`section`). The `tone` is the `variant`: `destructive` is `error`, `warning` is `warning`, `info` is `success` (a passed check or a state the product manages for the reader, drawn in the accent blue) and `neutral` is `secondary`; there is no violet or cyan.',
1332
+ instead: '"Pick `Banner` when the message is page-level or system-wide and needs a CTA, `Toast` for transient acknowledgments, `Modal` for destructive confirmations." (https://vercel.com/geist/note) Ours: `Banner` is `NoticeBanner`, `Toast` is `showMessageToast` and `Modal` is `ConfirmDialog`. For a field\'s wrong value use `FieldError`. For a failed read use `EmptyState failed`.',
1333
+ behaviour: '"A Note is persistent until the underlying state changes; don’t add an ad hoc dismiss control because it competes with the message." (https://vercel.com/geist/note) "One Note per concept. Stacking three Notes on a card means the page architecture, not the Note copy, is wrong." (https://vercel.com/geist/note) "The optional `NoteAction` holds a single inline CTA. Don’t pair it with a second button." (https://vercel.com/geist/note) Ours: tone `destructive` by default, `warning`, `info` or `neutral`. Colour sits on the line, the text and the mark; the ground stays clear, and the action stays neutral. `section` is the framed note, a line 36 px high with its mark and sentence on one row and the action at its trailing edge (under 768 px the action takes its own row, full width), with or without an action; `action` is the unframed passage, its mark and sentence on one row with the action link inside the sentence, after a space. The title and the body run on as one passage, the title closed by a full stop when it has none of its own, so the two never read as one sentence; the title leads in bold (semibold in a note, medium in a passage) only when a body follows it, and stands at regular weight alone. A message worded for a notice has one action, a small default button in a section and an underlined 14 px link in the passage, and at most a second one after it (Nadia\'s *Ask Nadia* after *Try again*), an outline button in a section and a second link in the passage; a third is not shown. Long words wrap, nothing truncates. A successful retry removes it. Required: the catalogue `message`, so the sentence and its mark are always drawn.',
1334
+ words: '"`NoteLabel` is a 1–2 word Title Case prefix that names the topic: `Region Change`, `Rate Limit`, `Plan Limit`. Cut hedges like `Heads Up`, `FYI`, and `Note`." (https://vercel.com/geist/note) "`NoteContent` is one sentence in active voice that names the impact: `Changing this region restarts all functions.`" (https://vercel.com/geist/note) "No `variant="info"` exists; omit `variant` for the default info icon or use `variant="secondary"` for neutral copy." (https://vercel.com/geist/note) "Single-fragment labels take no period; full descriptive sentences in the body do." (https://vercel.com/geist/note) Ours: words come from the catalogue only. The message\'s title stands in for `NoteLabel` and `NoteContent`, in sentence case, and `info` exists as Geist\'s blue `success`. The action is labelled with its result: *Try again*, *Sign in*. When the message carries an action, its body does not repeat it: the link is the step, so the body ends before "Try again".',
1335
+ access: '`action`: `role="alert"` (`status` for info and neutral). `section`: `role="status"`, `aria-live="polite"`, atomic. The mark is hidden. Focus stays where the person was.',
1226
1336
  },
1227
1337
  {
1228
1338
  name: "NoticeBanner",
1229
1339
  module: "notice-banner",
1230
1340
  family: "Feedback",
1231
1341
  summary: "A condition of the whole page, the account or the service, with its fix.",
1232
- when: "A condition of the whole page, the account or the service that the page itself can resolve, with that resolving action on the banner. One at a time, above the page's content, under its title.",
1233
- instead: 'For one section use `MessageNotice placement="section"`. For a measured level use a `KpiCard` with its tone; crossing a threshold is a notification in the inbox, not a banner.',
1234
- behaviour: "Tone `neutral` (the plain card fill), `info`, `warning`, `destructive`: a soft ground, deep text, the 1 px line edge, 6 px corners; neutral and info take the Info mark, warning and destructive the TriangleAlert. No close button on an unresolved condition. No action, no banner: `action` is required.",
1235
- words: 'Title names the condition, description what it stops, the action the fix as verb and noun: "Open Slack settings", "Sign in". Sentence case, no "Please". The words come from the product\'s catalogue code.',
1236
- access: '`liveRegion="polite"` gives `role="status"`, announced once when it appears; `"off"` gives `role="note"` for one already there at load. The mark is hidden.',
1342
+ when: '"A prominent message that spans the full width of its container to announce important information." (https://vercel.com/geist/banner) "Used for temporary, project-wide notifications that require resolution" (https://vercel.com/geist/project-banner) "Use Project Banner for project-wide states that need resolution: overdue billing, an active rollback, attack mitigation, an expiring trial blocking deploys." (https://vercel.com/geist/project-banner) "Match `variant` to severity: `error` for critical downtime or payment-blocking states, `warning` for an exceptional state with non-immediate action, `success` for a positive temporary mitigation, `gray` for routine project-wide notices." (https://vercel.com/geist/project-banner) Ours: a condition of the whole page, the account or the service that the page itself can resolve, with that resolving action on the banner. The `tone` is the `variant`: `destructive` is `error`, `warning` is `warning`, `info` is `success` and `neutral` is `gray`. One sits above the page\'s content, under its title.',
1343
+ instead: '"Pick `Note` for inline contextual messages tied to a single field or card, `Toast` for transient acknowledgments, `Modal` for confirmations." (https://vercel.com/geist/project-banner) Ours: `Note` is `MessageNotice` (`placement="section"`), `Toast` is `showMessageToast` and `Modal` is `ConfirmDialog`. For a measured level use a `KpiCard` with its tone; crossing a threshold is a notification in the inbox, not a banner.',
1344
+ behaviour: '"Project Banner is non-dismissible by design. If the message can be dismissed without resolving the underlying state, it isn’t banner-worthy; move it to a `Note`." (https://vercel.com/geist/project-banner) "Show one Project Banner at a time. Stacking competing banners drowns the most urgent state." (https://vercel.com/geist/project-banner) "Always pass a `callToAction` that resolves the state. A banner with no route is a dead end." (https://vercel.com/geist/project-banner) Ours: a strip as wide as its container: a filled ground with a line on its top and bottom edge only, no corners, 40 px high and centred, with the mark, the sentence and the fix on one row; under 768 px the fix drops under the sentence, aligned with its text. Colour sits on the whole strip (ground, both lines, mark and text); the fix is an underlined link in the darkest step of the same hue (neutral: `text-foreground`) and returns to the strip\'s text on hover. Neutral and info take the Info mark, warning and destructive the TriangleAlert. `action` is the `callToAction`: one command, never two, never a ready-made button, and there is no close button. Required: `action`, `title`, `tone` and `liveRegion`; the mark is always drawn.',
1345
+ words: '"`label` is one sentence in sentence case that names the impact: `Your Pro trial expires in 3 days.` Don’t open with `Heads up` or apologetic preambles." (https://vercel.com/geist/project-banner) "`callToAction.label` is Title Case `Verb + Noun` and points at the resolver: `Update Payment Method`, `Reactivate Project`, `Review Tokens`." (https://vercel.com/geist/project-banner) "Name the affected entity when the project context isn’t obvious from the surrounding chrome (`Production deployments are paused on my-project`)." (https://vercel.com/geist/project-banner) "Don’t encode severity in the copy with emoji or interjections; the variant carries that signal." (https://vercel.com/geist/project-banner) "`NoteLabel` is a 1–2 word Title Case prefix that names the topic: `Region Change`, `Rate Limit`, `Plan Limit`. Cut hedges like `Heads Up`, `FYI`, and `Note`." (https://vercel.com/geist/note) Ours: the `title` is the `label`: the one sentence, naming the condition, its impact and the object. A `description` only adds what the title leaves out, in one more sentence. Our `label` is the `NoteLabel` topic prefix. The action is sentence case, verb and noun, pointing at the resolver: "Open Slack settings", "Update card". No "Please". The words come from the product\'s catalogue code.',
1346
+ access: '`liveRegion="polite"` gives `role="status"`, announced once when it appears; `"off"` gives `role="note"` for one already there at load. The mark, given or not, is hidden.',
1237
1347
  },
1238
1348
  {
1239
1349
  name: "PendingLabel",
1240
1350
  module: "pending-label",
1241
1351
  family: "Feedback",
1242
1352
  summary: "A control's own label while the system works on what it was asked to do.",
1243
- when: "Every control that starts work the system does: from the press until the result, whatever the wait. Under about a second it is the only sign. Every save in a footer goes through it.",
1353
+ when: "A control that is not a button and starts work the system does, such as a menu row: from the press until the result, whatever the wait. A `Button` or `ButtonLink` never uses it by hand: `loading` and `loadingLabel` draw it.",
1244
1354
  instead: "Past a second add one `WaitingLine` beside the control. An Agent's work is never this: it is her orb and a sentence. A queued or paused request is a status.",
1245
- behaviour: "Swaps the word, nothing else: same place, size and icon. The caller sets `disabled` (50% opacity) and `aria-busy` and blocks a second submit, because only the caller knows which element the wait belongs to. Failure returns the rest label.",
1355
+ behaviour: "Swaps the word, nothing else: same place, size and icon; a text label keeps the width of the longer of its two words. The caller sets `aria-busy` and blocks a second press, because only the caller knows which element the wait belongs to. Failure returns the rest label.",
1246
1356
  words: "The rest label's verb in its -ing form with the same noun: *Remove mailbox* to *Removing mailbox*. No ellipsis, no *Please wait*.",
1247
1357
  access: "`aria-busy` on the control; its name changes to the pending word and is read once. Focus stays on it.",
1248
1358
  },
@@ -1251,33 +1361,44 @@ export const USAGE = [
1251
1361
  module: "skeleton",
1252
1362
  family: "Feedback",
1253
1363
  summary: "A still placeholder in the shape of content whose layout is known.",
1254
- when: "A page or region whose layout is known while its data loads.",
1255
- instead: "For a wait on something the person pressed use `PendingLabel` and `WaitingLine`. Unknown layout shows nothing until data. Never both a skeleton and a line.",
1256
- behaviour: "The `line` fill, which stays visible on the card and on the workspace ground in both modes; 6 px corners or a circle, in the final shape so nothing shifts. Never pulses. Resolves to data, an absence or a failure.",
1257
- words: 'None. No "Loading" text beside it.',
1258
- access: "The region is `aria-busy`; each placeholder is hidden and never focusable.",
1364
+ when: '"Show a Skeleton when async data fills a known layout: table rows, card grids, profile blocks, sidebars." (https://vercel.com/geist/skeleton) Ours: a group of placeholders sits in the `SkeletonRegion` that later holds the data.',
1365
+ instead: '"For a single in-flight action, use `Spinner`; for an indeterminate inline wait, use `LoadingDots`; for known progress, use `Progress`." "Don’t use Skeleton as permanent decoration or as a placeholder for empty states." "When there’s no data to load, render an `EmptyState`." (https://vercel.com/geist/skeleton) Ours: there is no spinner and no loading dots: a wait on something the person pressed is `PendingLabel` and `WaitingLine`. An unknown layout shows nothing until the data arrives.',
1366
+ behaviour: '"Set `width` and `height` to match the final content so the layout doesn’t shift when data resolves." "A 200×20 block becoming an 80×16 string reads as a glitch." "Pick `pill`, `rounded`, or `squared` to mirror the eventual element’s shape (avatars `pill`, buttons and chips `rounded`, image tiles `squared`)." "When the skeleton wraps children, keep dimensions stable so the reveal swap doesn’t reflow surrounding content." "If you do not pass a fixed size, it will be calculated automatically." "The skeleton will hide when children are not null, but the size is retained." (https://vercel.com/geist/skeleton) Ours: `pill` is `circle` and `rounded` is `rectangle`. A skeleton with neither a size nor a child does not compile: `height` with `width`, or `className` for a size a token sets (`h-4 w-full`; the type cannot check that it holds one), or `boxHeight` with `height` for a text line, where the bar sits at the top of the box and the rest is space below it. Given a size, pass the content as children, `null` until it arrives: the bar shows, then the content replaces it in the same box. Without a size, wrap the real control and `show` covers it, `show={false}` reveals it, rather than drawing a twin of it. Still: it never pulses or shimmers. It resolves to data, an absence or a failure.',
1367
+ words: 'Ours: none. No "Loading" text beside it.',
1368
+ access: '"Skeletons are decorative; avoid placing focusable controls inside them while loading." "Disable the shimmer with the `no animation` variant on low-power surfaces and respect `prefers-reduced-motion`." (https://vercel.com/geist/skeleton) Ours: hidden from assistive technology and never focusable; an unsized wrapper covers its children inert and is `aria-busy` while it covers them. There is no shimmer to disable, so the placeholder is the same under `prefers-reduced-motion`. A group of bare placeholders needs a `SkeletonRegion`.',
1369
+ },
1370
+ {
1371
+ name: "SkeletonRegion",
1372
+ module: "skeleton",
1373
+ family: "Feedback",
1374
+ summary: "The container that shows placeholders and then the data, announced as busy while it loads.",
1375
+ when: "Ours: around the placeholders of a page or a card and the data that replaces them. In a component that renders both, it announces the data when it arrives. In a route's `loading` view it only marks the wait, since that view is unmounted when the page streams in; the page's own region announces the arrival.",
1376
+ instead: "Ours: a single `Skeleton` that wraps its content marks itself busy and needs no region.",
1377
+ behaviour: "Ours: `busy` is required: true while placeholders show, false in the same render that puts the data in. The same element goes from busy to not busy, so the change is announced once.",
1378
+ words: "Ours: none.",
1379
+ access: '"Wrap the loading region in `aria-busy="true"` and announce completion with `aria-live="polite"` on the destination container, not the skeleton itself." (https://vercel.com/geist/skeleton) Ours: `aria-busy` and a polite live region while loading, so the data that arrives is announced from the container that holds it; once loaded it is no live region, so a later change in the data, such as a row\'s status, is not read out wherever the reader is.',
1259
1380
  },
1260
1381
  {
1261
1382
  name: "Toaster",
1262
1383
  module: "sonner",
1263
1384
  family: "Feedback",
1264
1385
  summary: "The one place toasts appear, mounted once per product.",
1265
- when: "One acknowledgement of an action the person took, when the result is not already visible where they are looking; and the Undo of a reversible removal. Mount it once; raise toasts through `toast`.",
1266
- instead: "For a failure that must stay, use `MessageNotice` by the control. For unsaved work, failed payment or lost access never a toast alone. For a condition use a banner.",
1267
- behaviour: "Tone `success`, `info`, `warning`, `error`; mark in the deep step. The first action with a handler becomes its one button. Undo only where a safe rollback exists. A toast that stays up shows the waiting line, not a spinner.",
1268
- words: "Completion is noun, past participle: *Mailbox removed*, *Agent created*, paired one to one with its button *Remove mailbox*. No *successfully*, no exclamation.",
1269
- access: "Announced by the toaster's live region. Never the only route to recovery: the record's own row keeps its controls. Undo is reachable by keyboard (Alt+T moves focus to the toasts) while it shows.",
1386
+ when: '"A succinct message that is displayed temporarily." (https://vercel.com/geist/toast) "Use a toast for non-blocking acknowledgments of user-initiated actions: `Domain added`, `Project archived`, `Deployment canceled`." (https://vercel.com/geist/toast) "Pick the method by how the user experienced the event, not by HTTP status. A user-canceled deploy is `toasts.message(\'Deployment canceled\')`, not `success`. A partial deploy with skipped routes is `toasts.warning(...)`." (https://vercel.com/geist/toast) Ours: mount it once and raise toasts through `showMessageToast`. `toasts.message` is the tone `info`, and `success`, `warning` and `error` keep their names.',
1387
+ instead: '"Don’t use a toast alone for billing failures, permission denials, or build failures the user has to triage. Pair a ≤6-word toast (`Build failed`) with a persistent row carrying the recovery step and a stable identifier." (https://vercel.com/geist/toast) "Field-level validation belongs on the `Input`, not in a toast. Persistent configuration warnings belong in `Note` or `Banner`." (https://vercel.com/geist/toast) Ours: the persistent row is a `MessageNotice`, a field\'s wrong value stays on the field (`FieldError`), and a lasting condition is a `NoticeBanner`.',
1388
+ behaviour: "\"Default toasts auto-dismiss; pass `preserve` only when the user must read or act on the message before it disappears.\" (https://vercel.com/geist/toast) \"Undo snackbars stay on screen for 5–10 seconds and pair the past-tense toast with a single `Undo` action button.\" (https://vercel.com/geist/toast) \"Don’t stack toasts to narrate one async flow; emit the success or error toast at the terminal step.\" (https://vercel.com/geist/toast) Ours: `showMessageToast` takes no `preserve`, so every toast leaves after 6 s, which is also the Undo window; the first action with a handler becomes the toast's one button. Colour sits on the whole toast: `success` fills it with the accent blue (Geist's blue success, the same hue as an `info` note), `warning` and `error` with their solid colour, each with that colour's text; `info` stays the plain surface. The tone's mark stays, because a tone must read without colour and the hidden tone word Geist uses would need a word in the product's language that `showMessageToast` does not take. A toast is 420 px wide at most, 16 px padded, with a close button of 32 px at its trailing edge. A toast that stays up shows the waiting line, not a spinner. Required: `closeLabel` and `regionLabel`, because the close button and the region need a name in the product's language.",
1389
+ words: '"One sentence, sentence case, no trailing period when the toast is a single sentence." (https://vercel.com/geist/toast) "Completion toasts follow `{Noun} {past-participle}`: `Blob deleted`, `Domain added`, `Environment variable saved`. Never include `successfully`; the action name implies it." (https://vercel.com/geist/toast) "Error toasts are two sentences with periods and end with a recovery step: `Couldn’t verify domain. Try again.` Use `Couldn’t` for user-state errors and `Failed to` for system or infra errors; match adjacent shipped copy and don’t flip mid-flow." (https://vercel.com/geist/toast) "Pair the toast verb 1:1 with the destructive button verb (`Delete Project` then `Project deleted`, never `Project removed`)." (https://vercel.com/geist/toast) "Undo snackbars use the literal label `Undo`, never `Restore`, `Bring Back`, or `Cancel`. Only use the pattern when the rollback is safe." (https://vercel.com/geist/toast) Ours: the words come from the catalogue; *Mailbox removed* pairs with its button *Remove mailbox*.',
1390
+ access: '"The toast region announces with `aria-live="polite"`; reserve `assertive` for blocking errors that interrupt a flow." (https://vercel.com/geist/toast) "Don’t put primary navigation inside a toast; transient surfaces vanish before keyboard users can reach them." (https://vercel.com/geist/toast) Ours: the toaster\'s live region is named by `regionLabel` and the close button by `closeLabel`. Navigation is never the only route to recovery: the record\'s own row keeps its controls. Undo is reachable by keyboard (Alt+T moves focus to the toasts) while it shows.',
1270
1391
  },
1271
1392
  {
1272
1393
  name: "Tooltip",
1273
1394
  module: "tooltip",
1274
1395
  family: "Feedback",
1275
1396
  summary: "A short, optional fact about its trigger.",
1276
- when: "A short, optional fact the visible label cannot hold: why a value looks as it does, or an icon-only button's name, a shortcut only when one is bound.",
1277
- instead: "For a preview with several facts use `HoverCard`. Help with a link uses a popover. A disabled command's reason is written under it, never only here.",
1278
- behaviour: "Opens on hover and focus with no delay, above by default, 4 px off. Escape closes it. Never interactive; never the only home of a warning, a price or a reason.",
1279
- words: "One phrase, sentence case, no full stop: *Resets on 1 November*. Never repeats the visible label (*Save* on Save).",
1280
- access: "The trigger keeps its own name; the tooltip describes it. An icon-only button carries `aria-label` whether or not the tooltip is open.",
1397
+ when: '"Use a Tooltip to explain **why** something exists, not **what** it is." "The visible label names the thing; the tooltip adds the constraint, scope, or limit." (https://vercel.com/geist/tooltip) "Use lifecycle Tooltips (`Alpha`, `Experimental`, `Beta`, `Early Access`) to name the limits that apply: API stability, SLA, support, pricing, retention." (https://vercel.com/geist/tooltip) Ours: a shortcut is shown in a tooltip only when one is bound.',
1398
+ instead: '"For an entity preview with metadata rows (avatar, identifier, 2–4 facts, optional action), use `Context Card`." "For long-form content or actions that need persistence, route to a `Drawer` or page navigation." (https://vercel.com/geist/tooltip) Ours: `Context Card` is `HoverCard` and `Drawer` is `Sheet`. A disabled command\'s reason is written under it, never only in a tooltip.',
1399
+ behaviour: '"Tooltips open on hover and on keyboard focus." "Keep the default ~150ms entry delay so the tooltip doesn’t flicker on a sweeping mouse." "Don’t wrap a labelled `Input` in a Tooltip." "The trigger lands on the `<label>`, not the field, and the tooltip text becomes a second invisible label for screen readers." "Put help on a sibling icon button." "Keep primary actions outside the Tooltip; touch users can’t reach a hover-revealed control." (https://vercel.com/geist/tooltip) Ours: the pointer wait is 150 ms and keyboard focus opens it at once; `delay={0}` on the trigger or a `TooltipProvider` removes the wait. Above by default, 10 px from the trigger, at most 250 px wide, 13 px text balanced over its lines. Never interactive. `arrow={false}` drops the stem; `centerText={false}` left-aligns a tooltip that wraps. `TooltipContent` requires its text as children. Colour: one inverted neutral, the page\'s ink as the fill and its surface as the text, with a stem of the same fill and no hue, in both themes; a shortcut is a `Kbd` after the text, drawn 20 px high.',
1400
+ words: '"One sentence or fragment in `text`." "Sentence case, no period for a single fragment." "Skip tooltips that repeat the visible label (`text="Rate Limit"` on a `Rate Limit` button) or describe the interaction (`text="Click to override"`)." "Lifecycle Tooltips follow `{Label}: {one-line meaning}. {Specific limit}.`" "For a paid Beta feature, combine the lifecycle and pricing in one Tooltip rather than stacking two badges." (https://vercel.com/geist/tooltip) Ours: the text is `TooltipContent`\'s children, for example *Resets on 1 November*.',
1401
+ access: '"An icon-only trigger needs an `aria-label` that names the action; the Tooltip body adds context, it doesn’t replace the label." "Escape closes the tooltip and returns focus to the trigger." (https://vercel.com/geist/tooltip)',
1281
1402
  },
1282
1403
  {
1283
1404
  name: "UpdateCard",
@@ -1317,77 +1438,77 @@ export const USAGE = [
1317
1438
  module: "action-menu",
1318
1439
  family: "Menus",
1319
1440
  summary: "A record's menu opened from its row by a right-click: the mirror of its `ActionMenu`.",
1320
- when: "A right-click or long-press convenience on a record that already has a visible menu with the same commands.",
1321
- instead: "For anything a person must find use the visible three dots or a button: a context menu is never the only way. For global search use `CommandDialog`.",
1322
- behaviour: "The dropdown's surface and rows exactly, opening at the pointer. The same commands in the same order, the same disabled reasons, destructive last after a divider. With no command the row renders alone.",
1323
- words: "Identical to the visible menu's, in the same order; a command reached two ways reads the same both ways.",
1324
- access: "The Menu key and Shift+F10 open it on the focused row. The menu is named by `label`; Escape closes it and returns focus to the row.",
1441
+ when: '"ContextMenu must never be the only entry point for an action. Mirror every item in a visible `Menu` trigger or row button so mouse-only and keyboard-only users have parity." (https://vercel.com/geist/context-menu) Ours: a right-click or long-press convenience on a record that already has a visible menu with the same commands.',
1442
+ instead: '"For a global command palette, use `CommandMenu`; for a dropdown opened from a visible trigger, use `Menu`." (https://vercel.com/geist/context-menu) Ours: for anything a person must find use the visible three dots or a button. For global search use `CommandDialog`.',
1443
+ behaviour: '"Bind to right-click on desktop and long-press on touch. Suppress the native browser menu only on the trigger area, not the whole page." (https://vercel.com/geist/context-menu) "Position the menu at the pointer; if it would overflow the viewport, flip horizontally then vertically before clipping." (https://vercel.com/geist/context-menu) "Close on activation, Escape, and outside-click. Don’t close on hover-out." (https://vercel.com/geist/context-menu) Ours: the dropdown\'s surface and rows exactly. The same commands in the same order, the same muted rows and reasons, destructive last after a divider. With no command the row renders alone.',
1444
+ words: '"Items follow the same rules as `Menu`: Title Case `Verb + Noun` (`Open in New Tab`, `Copy URL`, `Delete Deployment`). Bare verbs are wrong." (https://vercel.com/geist/context-menu) Ours: sentence case, "Copy address". Identical to the visible menu\'s, in the same order; a command reached two ways reads the same both ways.',
1445
+ access: '"Trigger the keyboard menu key (`Shift+F10` on Windows/Linux, the menu key, or platform equivalent) to open the same menu without right-click." (https://vercel.com/geist/context-menu) "Up/Down arrows move focus, Enter/Space activates, Escape closes and returns focus to the row." (https://vercel.com/geist/context-menu) Ours: the menu is named by `label`, which is required.',
1325
1446
  },
1326
1447
  {
1327
1448
  name: "ActionMenu",
1328
1449
  module: "action-menu",
1329
1450
  family: "Menus",
1330
1451
  summary: "A record's three dots: the menu of its secondary commands, headed by the record's name.",
1331
- when: "The secondary commands of one record from a visible trigger: the row's three dots, or the page's, which comes last in the title row.",
1332
- instead: "For a record's main task use a visible button (Edit opens the row). For finding across everything use `CommandDialog`. For a value to choose use `Select`. For right-click use the mirror, `ActionContextMenu`.",
1333
- behaviour: "Ghost 36 px trigger that keeps its fill while open. The menu opens aligned to the trigger's end, 6 px below: the record's name, the commands with their icons first and the value one acts on in mono under it, then the destructive ones after a divider in the error colour. Typeahead, arrows, Escape returns focus. A disabled command stays, greyed, with its reason under it. Nothing renders with no command.",
1334
- words: 'The trigger reads "More for {record}"; the header is the record\'s name. Commands are verb and noun in sentence case, "Remove mailbox", never a bare "Delete". About ten at most; rows wrap, never truncate.',
1335
- access: 'The trigger is a ghost icon button named by `label`, `aria-expanded` while open. `role="menu"`, items `menuitem` in a group named by the record; the value a command acts on and its reason are the item\'s description; icons hidden.',
1452
+ when: '"An overflow menu triggered by a three-dot icon that reveals additional actions in a dropdown." (https://vercel.com/geist/dots-menu) "Use Menu for a discoverable trigger that opens a list of actions on a single resource (a dots menu on a row, a dropdown on a primary entity)." (https://vercel.com/geist/menu) Ours: the secondary commands of one record from a visible trigger: the row\'s three dots, or the page\'s, which comes last in the title row. A record\'s one main task is a visible button beside the dots; one non-destructive command that can run, alone, is drawn as a button by `RecordList` and `PageActions`, never as a menu; one that cannot run or is locked stays in the menu so its reason is read.',
1453
+ instead: '"For right-click or long-press on a row, use `ContextMenu`. For global commands behind `⌘K`, use `CommandMenu`. For two related primary actions, use a split button rather than burying the secondary action." (https://vercel.com/geist/menu) Ours: for a record\'s main task use a visible button (Edit opens the row). For finding across everything use `CommandDialog`. For a value to choose use `Select`. For right-click use the mirror, `ActionContextMenu`.',
1454
+ behaviour: '"Open on click, not hover; hover-open menus collide with screen readers and trackpad scrolls." (https://vercel.com/geist/menu) "Position auto-flips based on window bounds; don’t hardcode a side that clips on narrow viewports." (https://vercel.com/geist/menu) "Close on item activation, Escape, and outside-click. Don’t auto-close on a hover-out." (https://vercel.com/geist/menu) "Use `MenuItemLocked` for permission-gated actions so the lock icon and disabled state explain why the row is inert." (https://vercel.com/geist/menu) Ours: that is `locked` with its `reason`, in the same tooltip. The trigger keeps its fill while open. The menu opens 10 px under the trigger, aligned to its end: the record\'s name, the commands with their icons first and the value one acts on in mono under it, then the destructive ones after a divider in the error colour. A command that cannot run now takes `disabledReason`: it stays in place, 36 px like the rest, with its text muted, never dimmed, and says why in a tooltip on the row; a destructive one is muted, not red; a running one takes `pending` and `pendingLabel`. Nothing renders with no command.',
1455
+ words: '"Item children are Title Case `Verb + Noun` (`Rename Project`, `Duplicate Deployment`). Bare verbs like `Rename` or `Edit` are wrong outside obvious single-object context." (https://vercel.com/geist/menu) "End an item with `…` only when activating it opens a follow-up dialog (`Rename…`, `Transfer to Team…`)." (https://vercel.com/geist/menu) "Group destructive items at the bottom, separated by a divider, and keep the destructive copy as `Verb + Noun` (`Delete Project`, never bare `Delete`)." (https://vercel.com/geist/menu) "Cap a Menu around 10 items. Past that, group with `MenuSection` or move secondary actions to a settings page." (https://vercel.com/geist/menu) Ours: sentence case, "Remove mailbox", not Title Case. The trigger reads "More for {record}"; the header is the record\'s name. Rows wrap, never truncate.',
1456
+ access: '"Typeahead jumps to the next item whose label starts with the typed character; keep the visible label first so typeahead matches what the user sees." (https://vercel.com/geist/menu) "Return focus to the trigger on close so keyboard users keep their place in the row." (https://vercel.com/geist/menu) Ours: the trigger is a ghost icon button named by `label`, which is required, `aria-expanded` while open. `role="menu"`, items `menuitem` in a group named by `record`, which is required; every command requires its `icon`. The value a command acts on and its reason are the item\'s description; icons hidden.',
1336
1457
  },
1337
1458
  {
1338
1459
  name: "AlertDialog",
1339
1460
  module: "alert-dialog",
1340
1461
  family: "Menus",
1341
1462
  summary: "The blocking dialog primitive under `ConfirmDialog`.",
1342
- when: "Before an action that stops or loses something, through `ConfirmDialog`, which owns the words and the pending state.",
1343
- instead: "A product confirms with `ConfirmDialog`, never a hand-built alert dialog. A reversible local change needs no confirmation; give it Undo in a toast.",
1344
- behaviour: "No backdrop dismissal; Escape cancels. `AlertDialogAction` closes on press; a confirm that can fail is a plain button so the dialog stays open with the error.",
1345
- words: "Title a statement naming the record, never a question. Body the consequence, once.",
1346
- access: '`role="alertdialog"`, named by the title and described by the consequence. Focus starts on Cancel, the safe choice.',
1463
+ when: '"Confirm destructive actions in a Modal. `Drawer` and `Sheet` don’t fully dim the page, so they read as too soft for a delete or revoke." (https://vercel.com/geist/modal) Ours: before an action that stops or loses something, through `ConfirmDialog`, which owns the words and the pending state.',
1464
+ instead: "Ours: a product confirms with `ConfirmDialog`, never a hand-built alert dialog. A reversible local change needs no confirmation; give it Undo in a toast.",
1465
+ behaviour: '"Default focus to `Cancel` on any destructive Modal. Enter must never trigger the destructive action without a typed confirmation." (https://vercel.com/geist/modal) "Allow Escape and outside-click to dismiss non-destructive Modals; gate dismissal on destructive ones with unsaved input." (https://vercel.com/geist/modal) Ours: a bottom sheet on a phone, centred from 640 px, 480 px wide with 20 px of padding and a 20 px column of title, description and body. The footer is a recessed bar that stays at the bottom while a tall confirmation scrolls, and holds 32 px buttons in one row, also on a phone: Cancel at the leading edge, the confirm at the trailing edge, a lone button filling the width. All neutral; red belongs to what `ConfirmDialog` puts inside. A press outside does not close it, because Base UI fixes that for an alert dialog and a stray click must not discard a typed phrase or an action in flight; Escape and Cancel close it. `AlertDialogAction` closes on press; a confirm that can fail is a plain button so the dialog stays open with the error.',
1466
+ words: '"`Modal.Title` is a Title Case statement, never a question. `Transfer Project` is correct; `Transfer Project?` is wrong." (https://vercel.com/geist/modal) "`Modal.P` body is sentence case, 1–3 sentences. State the consequence first, then any cascade." (https://vercel.com/geist/modal)',
1467
+ access: '"Set `aria-labelledby` to the `Modal.Title` id so screen readers announce the title on open." (https://vercel.com/geist/modal) "Keep the Cancel button literally `Cancel` so screen-reader users hear a stable dismissal label across destructive flows." (https://vercel.com/geist/modal) Ours: `role="alertdialog"`, named by the title and described by the consequence.',
1347
1468
  },
1348
1469
  {
1349
1470
  name: "ContextMenu",
1350
1471
  module: "context-menu",
1351
1472
  family: "Menus",
1352
- summary: "A menu that opens at the pointer on a right-click, from the dropdown's own parts.",
1353
- when: "A right-click on a canvas or a surface no record owns, whose commands are also reachable another way.",
1354
- instead: "A record's right-click is `ActionContextMenu`, from the same data as its `ActionMenu`. A context menu is never the only way to a command: give it a visible trigger too.",
1355
- behaviour: "The dropdown's surface and rows exactly, opening at the pointer on the right. Destructive last, after a divider. One level of submenu at most.",
1356
- words: "Identical to the visible menu's, in the same order; a command reached two ways reads the same both ways.",
1357
- access: "The Menu key and Shift+F10 open it on the focused element. Escape closes it and returns focus to that element.",
1473
+ summary: "A menu that opens at the pointer on a right-click or a long press, from the dropdown's own parts.",
1474
+ when: '"Use ContextMenu for power-user shortcuts on right-click or long-press over a row, file, or canvas object." (https://vercel.com/geist/context-menu) "ContextMenu must never be the only entry point for an action. Mirror every item in a visible `Menu` trigger or row button so mouse-only and keyboard-only users have parity." (https://vercel.com/geist/context-menu) Ours: a right-click on a canvas or a surface no record owns, whose commands are also reachable another way.',
1475
+ instead: '"For a global command palette, use `CommandMenu`; for a dropdown opened from a visible trigger, use `Menu`." (https://vercel.com/geist/context-menu) Ours: those are `CommandDialog` and `DropdownMenu`. A record\'s right-click is `ActionContextMenu`, from the same data as its `ActionMenu`.',
1476
+ behaviour: '"Bind to right-click on desktop and long-press on touch. Suppress the native browser menu only on the trigger area, not the whole page." (https://vercel.com/geist/context-menu) "Position the menu at the pointer; if it would overflow the viewport, flip horizontally then vertically before clipping." (https://vercel.com/geist/context-menu) "Close on activation, Escape, and outside-click. Don’t close on hover-out." (https://vercel.com/geist/context-menu) Ours: the dropdown\'s surface and rows exactly, 160 px wide at least, opening 2 px right of the pointer and appearing at once, with no entry motion. A row that cannot be chosen keeps its place with its text muted, never dimmed. Destructive last, after a divider.',
1477
+ words: '"Items follow the same rules as `Menu`: Title Case `Verb + Noun` (`Open in New Tab`, `Copy URL`, `Delete Deployment`). Bare verbs are wrong." (https://vercel.com/geist/context-menu) "End with `…` only when activating opens a follow-up dialog (`Rename…`, `Move to Folder…`)." (https://vercel.com/geist/context-menu) "Group destructive items at the bottom with a divider and keep the destructive label `Verb + Noun`. `Delete` alone never ships." (https://vercel.com/geist/context-menu) Ours: sentence case, "Copy address", not Title Case. Identical to the visible menu\'s, in the same order; a command reached two ways reads the same both ways.',
1478
+ access: '"Trigger the keyboard menu key (`Shift+F10` on Windows/Linux, the menu key, or platform equivalent) to open the same menu without right-click." (https://vercel.com/geist/context-menu) "Up/Down arrows move focus, Enter/Space activates, Escape closes and returns focus to the row." (https://vercel.com/geist/context-menu) "Don’t bury destructive items behind nested submenus. One level deep keeps keyboard navigation predictable." (https://vercel.com/geist/context-menu) Ours: the trigger must itself be focusable or hold something that is. The content requires an `aria-label` naming the menu, because nothing visible labels a menu opened at the pointer.',
1358
1479
  },
1359
1480
  {
1360
1481
  name: "Dialog",
1361
1482
  module: "dialog",
1362
1483
  family: "Menus",
1363
- summary: "A centred modal for a bounded job the person finishes or cancels.",
1364
- when: "A new record through a few fields, or one or two values changed from a command: a bounded job the person finishes or cancels. Usually reached through `FormDialog`.",
1365
- instead: "A record already in a list edits inside its row. A catalogue or a setup in steps is `FormDrawer`. A consequence is `ConfirmDialog`. A linkable workflow is its own page.",
1366
- behaviour: "Centred, 384 px, 24 px padding, 16 px gaps, 12 px corners, modal height over the 50% scrim. `DialogBody` scrolls between a header and footer that stay. Footer recessed behind a hairline, Cancel then the primary at the trailing edge.",
1367
- words: "Title is verb and noun (*Add MCP server*); the primary repeats it (*Add server*); the pending word is its -ing form. The failure comes from the catalogue, above the footer.",
1368
- access: '`role="dialog"` named by its title. Focus starts on the first field, is held inside, returns to the opener. Escape and the named close button dismiss when nothing is running.',
1484
+ summary: "A modal for a bounded job the person finishes or cancels.",
1485
+ when: '"Use Modal when a decision must block the rest of the page. For persistent associated context where the underlying page stays readable, use `Sheet` on desktop or `Drawer` on mobile." (https://vercel.com/geist/modal) "Skip Modal for routine create flows that have a dedicated page; route to the page instead." (https://vercel.com/geist/modal) Ours: `Dialog` is Geist\'s Modal, usually reached through `FormDialog`: a new record through a few fields, or one or two values changed from a command.',
1486
+ instead: '"Confirm destructive actions in a Modal. `Drawer` and `Sheet` don’t fully dim the page, so they read as too soft for a delete or revoke." (https://vercel.com/geist/modal) Ours: a consequence is `ConfirmDialog`. A catalogue or a setup in steps is `FormDrawer`. A record that already has a page, or a linkable workflow, is that page.',
1487
+ behaviour: '"Allow Escape and outside-click to dismiss non-destructive Modals; gate dismissal on destructive ones with unsaved input." (https://vercel.com/geist/modal) Ours: centred over a scrim, 540 px wide with 20 px of padding; on a phone it is a bottom sheet (`phoneLayout="centred"` keeps it centred). The title, its optional description and the fields scroll and the footer stays at the bottom; with `sticky` the header stays too, as a recessed bar. The footer is recessed and holds 32 px buttons in one row, also on a phone: Cancel at the leading edge, the primary at the trailing edge, a lone button across the full width. A step back (*Previous*) is wrapped with Cancel in one group at the leading edge, so the footer keeps two ends. A footer always holds an action: `children`, or `showCloseButton` with its `closeLabel`. All neutral: the footer and a `sticky` header are recessed behind a hairline, and no hue appears but the focus ring.',
1488
+ words: '"`Modal.Title` is a Title Case statement, never a question. `Transfer Project` is correct; `Transfer Project?` is wrong." (https://vercel.com/geist/modal) "`Modal.P` body is sentence case, 1–3 sentences. State the consequence first, then any cascade." (https://vercel.com/geist/modal) "Primary button is `Verb + Noun` and matches the title verb (`Transfer Project` title pairs with `Transfer Project` button). Never `Confirm`, `OK`, or a bare verb on a destructive primary." (https://vercel.com/geist/modal) "Cancel literal stays `Cancel`. Acknowledgment-only Modals (after a key reveal, after a one-time-show) use `Done`, never `OK` or `Close`." (https://vercel.com/geist/modal) "Pair the success toast verb 1:1 with the primary button: `Delete Project` button, `Project deleted` toast." (https://vercel.com/geist/modal) Ours: titles and buttons are sentence case where Geist writes Title Case: the title *Add MCP server*, the primary *Add server*, and its pending word is its -ing form. The failure comes from the catalogue, above the footer. The description is optional and runs to two lines.',
1489
+ access: '"Set `aria-labelledby` to the `Modal.Title` id so screen readers announce the title on open." (https://vercel.com/geist/modal) "Keep the Cancel button literally `Cancel` so screen-reader users hear a stable dismissal label across destructive flows." (https://vercel.com/geist/modal) "Trap focus inside the Modal while it’s open and return focus to the trigger after close. Restore body scroll on the same tick the Modal unmounts." (https://vercel.com/geist/modal) "After an error inside the Modal, keep focus inside so the user can retry; after success, return focus to the trigger." (https://vercel.com/geist/modal) Ours: `role="dialog"` named by its title. The corner Close is named by `closeLabel`. Focus starts on the first field or `initialFocus`.',
1369
1490
  },
1370
1491
  {
1371
1492
  name: "Drawer",
1372
1493
  module: "drawer",
1373
1494
  family: "Menus",
1374
- summary: "The low-level swipe drawer primitive.",
1375
- when: "A surface that slides from an edge and follows a swipe. No screen calls it today; screens use `FormDrawer`.",
1376
- instead: "A catalogue or a setup in steps is `FormDrawer`. Context beside a record is `Sheet`. A few fields are `FormDialog`.",
1377
- behaviour: "Opens from its swipe direction and follows the finger; nested drawers stack. The header and footer stay while the body scrolls, each drawing the one hairline that separates it.",
1378
- words: "Title is what it does to which record: *Connect Telegram*.",
1379
- access: "A modal dialog named by its title with a named close; focus returns to the trigger.",
1495
+ summary: "A bottom sheet for a short job on a phone.",
1496
+ when: '"Only use a Drawer on small viewports." (https://vercel.com/geist/drawer) "Drawer renders as a bottom sheet on small viewports only. On desktop render `Modal` (or `Sheet` for lateral context) directly instead of forcing Drawer." (https://vercel.com/geist/drawer) "A Drawer is good for short focused mobile actions: a single form, a filter sheet, a primary CTA paired with `Cancel`." (https://vercel.com/geist/drawer) Ours: no screen calls it today.',
1497
+ instead: '"Don’t use Drawer to confirm destructive actions. The lack of a fully blocking dim weakens the severity signal that delete and revoke flows need; route to `Modal`." (https://vercel.com/geist/drawer) Ours: on a desktop a blocking job is `Dialog` and context beside a record is `Sheet`; a catalogue or steps are `FormDrawer`.',
1498
+ behaviour: '"Tap-outside and swipe-down dismiss by default; preserve both unless the form has dirty input." (https://vercel.com/geist/drawer) "Use `verticalScroll` so the drawer body scrolls inside its frame instead of the page behind it." (https://vercel.com/geist/drawer) "Cap content height with `customHeight` only when the default height clips the primary action; the action and `Cancel` must stay above the fold." (https://vercel.com/geist/drawer) Ours: `unsaved.dirty` holds back the swipe, Escape and a press outside, and `unsaved.onDismissBlocked` runs. The body always scrolls inside the frame, so there is no `verticalScroll`; `height` is Geist\'s `customHeight`. It rises from the bottom edge and follows the finger, is at most 80% of the viewport tall, and is all neutral. Nested drawers stack.',
1499
+ words: '"`DrawerTitle` is a Title Case statement that names the entity (`Deployment Details`, `Filter Logs`)." (https://vercel.com/geist/drawer) "Body is sentence case prose. One primary `Verb + Noun` button and a literal `Cancel`; don’t cram a destructive cascade copy into the smaller frame." (https://vercel.com/geist/drawer) "Don’t restate the page heading as the drawer title; name what this view does." (https://vercel.com/geist/drawer) Ours: the title is sentence case (*Filter logs*), centred at 18/24 and semibold.',
1500
+ access: '"Trap focus inside the drawer while open and return focus to the trigger on close." (https://vercel.com/geist/drawer) "Escape closes the drawer; honor the system back gesture on mobile so users can dismiss without reaching for the close affordance." (https://vercel.com/geist/drawer) "Lock body scroll on open and restore it on close so iOS rubber-band scroll doesn’t leak through." (https://vercel.com/geist/drawer) Ours: a modal dialog named by its title. It draws no Close of its own; a footer that needs a visible one uses `DrawerClose`.',
1380
1501
  },
1381
1502
  {
1382
1503
  name: "DropdownMenu",
1383
1504
  module: "dropdown-menu",
1384
1505
  family: "Menus",
1385
- summary: "A menu that floats from a visible trigger, built from its parts: the surface, labelled groups, commands, choices and dividers.",
1386
- when: "A menu no composition draws: a column chooser, a sort, a product's own choices in a submenu.",
1387
- instead: "A record's or a page's commands are `ActionMenu` or `PageActions`' menu, from data. The account's rows are `AccountMenu`. A value to choose in a form is `Select`; finding across everything is `CommandDialog`.",
1388
- behaviour: "Solid floating surface, 12 px corners, 4 px inset, at least 224 px. Rows 36 px (44 on touch), 12 px padding, accent fill on hover and focus. Opens on click; typeahead, arrows, Escape returns focus to the trigger. A `DropdownMenuCommand` leads with its icon; one that cannot run stays, greyed, with its reason under it.",
1389
- words: 'Commands are verb and noun in sentence case, "Remove mailbox", never a bare "Delete". About ten at most. Italian runs a third longer; rows wrap, never truncate.',
1390
- access: '`role="menu"`, items `menuitem`, the trigger `aria-expanded` while open. A command\'s icon is hidden; the value it acts on and its reason are its description.',
1506
+ summary: "A menu that floats from a visible trigger, built from its parts: the surface, labelled groups, commands, links, choices and dividers.",
1507
+ when: '"Use Menu for a discoverable trigger that opens a list of actions on a single resource (a dots menu on a row, a dropdown on a primary entity)." (https://vercel.com/geist/menu) Ours: a menu no composition draws: a column chooser, a sort, a product\'s own choices in a submenu.',
1508
+ instead: "\"For right-click or long-press on a row, use `ContextMenu`. For global commands behind `⌘K`, use `CommandMenu`. For two related primary actions, use a split button rather than burying the secondary action.\" (https://vercel.com/geist/menu) Ours: those are `ContextMenu` and `CommandDialog`; two related primary actions stand side by side as buttons. A record's or a page's commands are `ActionMenu` or `PageActions`' menu, from data. The account's rows are `AccountMenu`. A value to choose in a form is `Select`.",
1509
+ behaviour: '"Dropdown menu opened via button. Supports typeahead and keyboard navigation." (https://vercel.com/geist/menu) "Open on click, not hover; hover-open menus collide with screen readers and trackpad scrolls." (https://vercel.com/geist/menu) "Position auto-flips based on window bounds; don’t hardcode a side that clips on narrow viewports." (https://vercel.com/geist/menu) "Close on item activation, Escape, and outside-click. Don’t auto-close on a hover-out." (https://vercel.com/geist/menu) "Use `MenuItemLocked` for permission-gated actions so the lock icon and disabled state explain why the row is inert." (https://vercel.com/geist/menu) Ours: that is `locked` with its `reason`. A solid floating surface, 200 px wide at least, 10 px from its trigger. A labelled trigger may end in a chevron; an icon trigger has none. Safe commands come first, each with its icon, groups split by a divider, the destructive command last. Colour sits on that command alone: its text is the error colour at rest and its row takes the soft error fill when highlighted; the panel, the other rows, a section title and the divider stay neutral, and a suffix or lock is muted, 12 px after the label. A command that cannot run stays in place with muted text, never dimmed, and says why in a muted line under its label, which a tap on a phone reads as well as a pointer, since a tooltip opens for the mouse only; a destructive one is muted, not red. Every row is 36 px (44 on touch) with the pointer cursor, or the default one when it cannot be chosen, a row with a reason growing by its line, and a section title is a muted row of that height. The highlight is the row\'s fill alone: no text inside a row changes colour with it, and a row that cannot be chosen takes no highlight.',
1510
+ words: '"Item children are Title Case `Verb + Noun` (`Rename Project`, `Duplicate Deployment`). Bare verbs like `Rename` or `Edit` are wrong outside obvious single-object context." (https://vercel.com/geist/menu) "End an item with `…` only when activating it opens a follow-up dialog (`Rename…`, `Transfer to Team…`)." (https://vercel.com/geist/menu) "Group destructive items at the bottom, separated by a divider, and keep the destructive copy as `Verb + Noun` (`Delete Project`, never bare `Delete`)." (https://vercel.com/geist/menu) "Section headers (`MenuSection title`) are Title Case, 1–2 words (`Workspace`, `Recent Projects`)." (https://vercel.com/geist/menu) "Cap a Menu around 10 items. Past that, group with `MenuSection` or move secondary actions to a settings page." (https://vercel.com/geist/menu) Ours: sentence case, not Title Case: "Remove mailbox", never a bare "Delete"; a section title is sentence case too. Italian runs a third longer; rows wrap, never truncate.',
1511
+ access: '"Up/Down arrows move focus through items, Home/End jump to first/last, Enter or Space activates." (https://vercel.com/geist/menu) "Typeahead jumps to the next item whose label starts with the typed character; keep the visible label first so typeahead matches what the user sees." (https://vercel.com/geist/menu) "Return focus to the trigger on close so keyboard users keep their place in the row." (https://vercel.com/geist/menu) Ours: `role="menu"` named by its trigger, items `menuitem`. Unlike Geist, which skips disabled rows, arrows land on a command that cannot run so its reason is read; it is still never highlighted and never runs. A command\'s icon is hidden; the value it acts on and its reason are its description. `DropdownMenuCommand` requires its `icon`; a command that cannot run takes `disabledReason`, and one a person may not run is `locked` with its `reason`, so nothing is greyed without saying why.',
1391
1512
  },
1392
1513
  {
1393
1514
  name: "GlassControl",
@@ -1411,17 +1532,6 @@ export const USAGE = [
1411
1532
  words: 'The label is a noun: "Theme", "Language". Each choice carries a full name even when it shows a glyph or a short code: "EN" is named "English".',
1412
1533
  access: "A radio group named by the label; each choice is a radio named by its `label`, with the same text as its tooltip.",
1413
1534
  },
1414
- {
1415
- name: "Menubar",
1416
- module: "menubar",
1417
- family: "Menus",
1418
- summary: "A row of menus, as a desktop application's menu bar.",
1419
- when: "A desktop surface whose commands are many and grouped, as an application's File and Edit menus are.",
1420
- instead: "A record's commands are its `ActionMenu`; a page's are `PageActions`. Finding anything is `CommandDialog`.",
1421
- behaviour: "Each menu has the dropdown's surface and rows. Arrow keys move between menus and through items; Escape closes and returns focus to the bar.",
1422
- words: "Menu names are single nouns; items are verb and noun in sentence case.",
1423
- access: '`role="menubar"`; each trigger exposes `aria-expanded`; items are `menuitem`s.',
1424
- },
1425
1535
  {
1426
1536
  name: "Popover",
1427
1537
  module: "popover",
@@ -1437,12 +1547,12 @@ export const USAGE = [
1437
1547
  name: "Sheet",
1438
1548
  module: "sheet",
1439
1549
  family: "Menus",
1440
- summary: "A panel beside a record, or the navigation on a narrow window.",
1441
- when: "Context that belongs beside a record: inspecting a row's details. And the product's navigation when the window is too narrow for the sidebar.",
1442
- instead: "For a catalogue or steps use `FormDrawer` (built on this). For editing use the row or the record's page. For a blocking decision use `ConfirmDialog`.",
1443
- behaviour: "From any side, 12 px from the edges, 12 px corners all round, modal over the scrim. Default 3/4 of the window up to 384 px; navigation 288 px. Header 24 px padding; body scrolls; footer recessed.",
1444
- words: "Title is the record's name, never the page's heading again. A footer link names where it goes: *Open Rossi Ottica*.",
1445
- access: "A modal dialog named by its title with a named close. Focus returns to the row it inspects, or to the navigation button.",
1550
+ summary: "A panel from the edge of the window, beside what it describes.",
1551
+ when: '"Use Sheet for persistent associated context like deployment details, log row inspection, or a member profile, where the underlying page stays useful." (https://vercel.com/geist/sheet) Ours: also the product\'s navigation when the window is too narrow for the sidebar.',
1552
+ instead: '"For a blocking decision, use `Modal`. For a mobile-only bottom sheet, use `Drawer`." (https://vercel.com/geist/sheet) "Don’t use Sheet to confirm destructive actions. The default `modal=false` keeps the page interactive, which weakens severity for a delete or revoke." (https://vercel.com/geist/sheet) Ours: a blocking decision is `ConfirmDialog` or `Dialog`. A catalogue or steps use `FormDrawer` (built on this). For editing use the row or the record\'s page.',
1553
+ behaviour: '"Sheet defaults to `modal=false` so toasts and other high-z elements stay reachable. Keep that default unless the sheet owns the screen." (https://vercel.com/geist/sheet) "Pick `side` from the trigger location: a row inspector slides from `right`, a global filter from `left`. Don’t change sides mid-session." (https://vercel.com/geist/sheet) "Outside-click does not auto-close, so always render an explicit close affordance and honor Escape." (https://vercel.com/geist/sheet) Ours: `modal` dims the page and blocks it, for a sheet that owns the screen, and `noOverlay` on its content drops the scrim again. An inset card with 16 px corners, 12 px from the window\'s edges; a side panel has one width that no caller sets. A given kind of sheet always opens on the same side. A press outside never closes it, Escape does, except the phone navigation, which `closesOnScrimPress` closes on a press on its scrim. The body scrolls between a header and a footer with its actions at the trailing edge, as Vercel\'s drawers (Production Checklist): the footer keeps the sheet\'s fill and stands under a 1 px rule, so its actions stay put while the body scrolls. Neutral only, with no hue but the focus ring; the primary is the only filled control. `closeLabel` is required.',
1554
+ words: '"Title is Title Case and names the entity (`Deployment Details`, `Member Profile`), not the page action." (https://vercel.com/geist/sheet) "Body is read-mostly: sentence case prose with Title Case sub-headings. Action buttons are optional; when present, follow `Verb + Noun`." (https://vercel.com/geist/sheet) "Don’t duplicate the page header inside the sheet; the sheet is the detail layer." (https://vercel.com/geist/sheet) Ours: titles and sub-headings are sentence case where Geist writes Title Case. A footer link names where it goes: *Open Rossi Ottica*.',
1555
+ access: '"Trap focus inside the sheet while it’s open and return focus to the trigger row on close so keyboard users keep their place in the list." (https://vercel.com/geist/sheet) "Provide a visible close button labeled `Close` (or an icon button with `aria-label="Close"`) since clicking outside doesn’t dismiss." (https://vercel.com/geist/sheet) "Announce the sheet with `aria-labelledby` pointing at the title; pair it with `aria-describedby` only when the body is short and load-bearing." (https://vercel.com/geist/sheet) Ours: a dialog named by its title, with a Close named in the product\'s words by `closeLabel`. Focus returns to the row or the navigation button that opened it.',
1446
1556
  },
1447
1557
  {
1448
1558
  name: "ThemeModeMenu",
@@ -1462,7 +1572,7 @@ export const USAGE = [
1462
1572
  summary: "On which days activity happened, as a year of cells.",
1463
1573
  when: "On which measured days activity happened, over the window the data covers, up to the last 365 days.",
1464
1574
  instead: "Never added to fill a row. Amounts over time are a bar or area chart. It follows the selected metric and names its own from-to window.",
1465
- behaviour: "One hue, chart-1, at four steps of opacity over an empty day; a legend from Less to More. Only measured history: unknown coverage stays unknown, never zero.",
1575
+ behaviour: "One hue, chart-1, at four steps of opacity over an empty day; a legend from Less to More. Only measured history: unknown coverage stays unknown, never zero. Its card spans the row like every chart card; a window of a quarter or less draws larger cells, so 30 days is never a thumbnail.",
1466
1576
  words: "The window as dates in the description. Each day's label: *8 October 2026: 42 requests*.",
1467
1577
  access: "Every day is a named cell for assistive technology; the legend steps are named too. Colour is never the only reading: the value is in each label.",
1468
1578
  },
@@ -1486,7 +1596,7 @@ export const USAGE = [
1486
1596
  instead: "Never encode a loss as a share. An unsigned ranking is the compact `Progress` ranking or a `DataTable`.",
1487
1597
  behaviour: "Horizontal bars from a 1.5 px zero line: favourable values to the right in success with right corners rounded, unfavourable to the left in destructive with left corners rounded.",
1488
1598
  words: "The money unit in the period or the axis. Losses stay negative, written with a true minus: −€360.",
1489
- access: "Each bar's sign is in its position and its tooltip, never only its colour. Category labels are text, readable without hue: every category keeps its label on one line, a long one cut with an ellipsis and whole in the tooltip.",
1599
+ access: "Each bar's sign is in its position and its tooltip, never only its colour. Category labels are text, readable without hue: the axis is as wide as the longest label up to 128 px, a longer one wraps to a second line and only past that is cut with an ellipsis, whole in the tooltip.",
1490
1600
  },
1491
1601
  {
1492
1602
  name: "ChartBarMultiple",
@@ -1517,8 +1627,8 @@ export const USAGE = [
1517
1627
  summary: "The card every chart in the kit renders: one header, the summary, the plot, its legend and the reading's time. The plots (`ChartBarStacked`, `ChartAreaStacked`, `ChartBarMultiple`, `ChartBarHorizontal`) hold only their own plot; a page never builds a chart card frame by hand.",
1518
1628
  when: "Every chart and every reading built around one: the frame a page never builds by hand. A chart is a named section of its page.",
1519
1629
  instead: "A single figure is a `KpiCard`, never nested inside a chart. A ranking beside the plot goes in the card's `aside`, not a second card.",
1520
- behaviour: "Header grid: title and period in the first column, controls in the second from a 28 rem card width, under the title below it. Summary, plot, legend, footer in that order. An empty dataset shows its `emptyState` in the plot's place. The footer is a recessed strip behind a hairline: the reading's time left, the link to its owner right.",
1521
- words: 'Title names the measure and its split: "Model spend by Customer". Period as dates. Footer: the reading\'s time, "Updated 4 minutes ago" or "Read at 12:42", and the way to its owner, "Open in Analytics". Sentence case throughout.',
1630
+ behaviour: "Header grid: title and period in the first column, controls and then the link to the reading's owner (a small ghost button ending in a chevron, as OpenRouter's \"Explore ›\") in the second from a 28 rem card width, under the title below it; a link with no controls stays at the trailing edge at every width. Summary, plot, legend, footer in that order. An empty dataset shows its `emptyState` in the plot's place. The footer is a recessed strip behind a hairline: the reading's time left, a command on the reading (Refresh) right.",
1631
+ words: 'Title names the measure and its split: "Model spend by Customer". Period as dates. The link: "Explore", or "Open in Analytics" where the owner is another page. Footer: the reading\'s time, "Updated 4 minutes ago" or "Read at 12:42". Sentence case throughout.',
1522
1632
  access: "The title is a heading at the page's level (default 2) and labels the plot. The plot is focusable and shows the 2 px focus outline only on keyboard focus.",
1523
1633
  },
1524
1634
  {
@@ -1536,10 +1646,10 @@ export const USAGE = [
1536
1646
  name: "ChartSparkline",
1537
1647
  module: "chart-sparkline",
1538
1648
  family: "Charts",
1539
- summary: "The stock linear line with nothing around it: no axes, no grid, no tooltip, no card.",
1649
+ summary: "The stock line with nothing around it: no axes, no grid, no tooltip, no card, drawn smooth and thin as OpenRouter's Activity tiles draw theirs, so a weekly rhythm reads as a wave and not as a row of spikes.",
1540
1650
  when: "What is trending: a row in a list, beside its name and its delta.",
1541
- instead: "When the reader needs values, axes or a period, use a full chart card. A KPI value carries its delta badge, not a sparkline, unless the card owns one.",
1542
- behaviour: "96 by 32 px, a 2 px linear line. Stroke is one chart token or the entity's colour.",
1651
+ instead: "When the reader needs values, axes or a period, use a full chart card. A KPI value carries its delta badge; a `KpiCard` draws its own sparkline from its `sparkline` values, never one placed beside it.",
1652
+ behaviour: "96 by 32 px, a 1.5 px monotone line through every value. Stroke is one chart token or the entity's colour.",
1543
1653
  words: 'None of its own. The row names the measure and the window: "Requests · past 8 weeks".',
1544
1654
  access: "Decorative by default, hidden from assistive technology, because the value and delta beside it carry the reading. Standalone it takes a `label` and becomes an image.",
1545
1655
  },
@@ -1547,21 +1657,21 @@ export const USAGE = [
1547
1657
  name: "DateRangeControl",
1548
1658
  module: "chart-date-range-control",
1549
1659
  family: "Charts",
1550
- summary: "The period a chart reads: a `Select` of presets with a custom window from a calendar.",
1551
- when: "The reading period of an overview or a chart: on the title row through `PageActions`' `period`, or in a `ChartCard`'s `controls` where the page has none.",
1552
- instead: "A choice of metric is a `ToggleGroup` in the card's controls. A date a person enters in a form is a date field.",
1553
- behaviour: "One select with the presets and a custom window; the value is encoded into the URL by the caller with `encodeDateRange`.",
1554
- words: 'Presets name the window: "Past 7 days", "Past 14 days", "Past 30 days", "This cycle". A custom window reads as its dates.',
1555
- access: "The select carries the control's accessible name and announces the current range.",
1660
+ summary: "The period a chart reads: a button that opens the common windows and a calendar for any other.",
1661
+ when: '"Pick `<Calendar>` for analytics ranges and any picker where day-of-week and month context matter." "Provide `<Calendar.Presets>` for the common ranges (`Last 7 Days`, `Month to Date`) so users land on the right window in one click." (https://vercel.com/geist/calendar) Ours: the reading period of an overview or a chart: on the title row through `PageActions`\' `period`, or in a `ChartCard`\'s `controls` where the page has none.',
1662
+ instead: '"For ISO dates pasted whole or relative shorthand like `7d`, use a free-form `Input`." (https://vercel.com/geist/calendar) Ours: a choice of metric is a `ToggleGroup` in the card\'s controls. A date a person enters in a form is a date field.',
1663
+ behaviour: '"Pair a horizontal layout with live results next to the calendar; in narrow surfaces like a sidebar, fall back to the stacked layout." "Set `min` and `max` to the data window so users can’t pick outside the retention range." "Keep the trigger label as the chosen range (`Apr 1 – Apr 28, 2026`); don’t fall back to `Pick a date` once a value is committed." "Persist the selected range when the popover closes and re-opens so users can tweak the end date without re-picking the start." (https://vercel.com/geist/calendar) Ours: one button that reads as the current window. It opens the presets as buttons beside a two-month calendar, stacked above it on a narrow surface. A preset applies at once and closes; a custom window applies on the second day. `min` and `max` are `minValue` and `maxValue`, the data\'s retention window. The value is encoded into the URL by the caller with `encodeDateRange`.',
1664
+ words: '"Each preset is a real button with a Title Case label (`Last 30 Days`); don’t mark presets as menu items without keyboard handling." (https://vercel.com/geist/calendar) Ours: presets name the window in sentence case: *Past 7 days*, *Past 14 days*, *Past 30 days*, *This cycle*. A custom window reads as its dates.',
1665
+ access: '"Trap focus inside the popover so Tab cycles day cells and presets instead of the page behind it." "Announce range changes through `aria-live="polite"` so a screen reader hears `From Apr 1 to Apr 28` after the second click." (https://vercel.com/geist/calendar) Ours: the button carries the control\'s name and the current window, and opens a dialog of preset buttons, the current one pressed, and the day grid. A polite status next to the button states the window: the preset\'s name, or the dates once the second day is chosen; it does not change after the first day.',
1556
1666
  },
1557
1667
  {
1558
1668
  name: "KpiCard",
1559
1669
  module: "kpi-card",
1560
1670
  family: "Charts",
1561
- summary: "One measured figure, the way the dashboard-01 section card shows it: the label above the value, the change top right as a badge, and the named comparison in the footer (`rule/kpi-card-trend-top-right`).",
1671
+ summary: "One measured figure, the way the dashboard-01 section card shows it: the label above the value, the change top right as a badge, and the named comparison in the footer (`rule/kpi-card-trend-top-right`). With its values over the period (`sparkline`) it reads as Vercel Observability's overview tile: the label, the value with the change badge at its right, the line across the card's width under it, and the comparison under the line, with no footer.",
1562
1672
  when: "One figure a person checks at a glance: Active Agents, Spend, Tasks done, Need attention, in an overview's row of figures.",
1563
1673
  instead: "A figure read over time is a `ChartCard`, and a single figure is never nested inside one. A ratio beside two money measures is a `KpiCard` next to the chart, never a second axis. A level against a cap is `Progress`.",
1564
- behaviour: "Label left and the change badge right on the first line, the value under them, the comparison in the footer. A figure with no comparison shows no badge and no empty footer. Direction crossed with whether up is good sets the badge icon's tone: success, blocked, or grey for a flat or neutral measure. `tone` marks the value itself as warning or critical when it crosses a threshold.",
1674
+ behaviour: "Label left and the change badge right on the first line, the value under them, the comparison in the footer. The footer is one line in every card of a row, so their dividers line up: the comparison and the note, joined, cut with an ellipsis when long and whole on hover. A figure with no comparison shows no badge and no empty footer. A figure that opens its owner (`render`) ends its label with a chevron. Colour sits only where it reads a fact: direction crossed with whether up is good sets the badge icon's tone (success, blocked, or grey for a flat or neutral measure), and `tone` marks the value itself as warning or critical when it crosses a threshold; the card, label and comparison stay neutral. With `sparkline`, Vercel Observability's overview tile: the label across the tile, the value under it with the change badge at its right, a 32 px line in `chart-1` across the card's width, then the comparison and the note in muted ink on up to two lines, which every tile of a row reserves, so the tiles' lines and comparisons sit at the same heights and nothing is cut at a five-tile row. A figure with no line or no comparison keeps the empty rows.",
1565
1675
  words: 'The label names the measure in sentence case: "Active Agents", "Spend". The change is signed with a true minus and its unit ("+12.4%", "−1"), the comparison names the period: "vs 16 last week", "vs August 2026". No icon beside the label.',
1566
1676
  access: "The trend icon is hidden and the direction is read as a word in the reader's language (`directionLabel`). Figures use tabular digits. The value is text, never only colour.",
1567
1677
  },
@@ -1592,11 +1702,11 @@ export const USAGE = [
1592
1702
  module: "accordion",
1593
1703
  family: "Other",
1594
1704
  summary: "Sections that open and close under their headings.",
1595
- when: "Optional or advanced content in named sections, such as the questions of a help page.",
1596
- instead: "One piece of supporting detail is `Disclosure`. A long block cut short is `ExpandableBlock`. The page's main answer is never hidden in one.",
1597
- behaviour: "Related sections may stay open together; exclusive ones open one at a time. The trigger's chevron turns when open.",
1598
- words: "Each trigger names its topic as a noun phrase or a question a person asks.",
1599
- access: "Each trigger is a `button` with `aria-expanded` controlling its panel; headings keep the page's outline.",
1705
+ when: "“Use Collapse for optional, advanced, or repetitive content that most users skip on most visits (FAQ, advanced settings, request payload preview).” (https://vercel.com/geist/collapse) “For top-level structure that every user reads, use page sections with regular headings; collapsing primary content hides what the page is about.” (https://vercel.com/geist/collapse) Ours: Geist's `CollapseGroup` is `Accordion`: named sections such as a help page's questions or advanced settings.",
1706
+ instead: "“Single Collapse for one optional block. `CollapseGroup` for a related set; switch to `Tabs` when the items are sibling views, not optional drill-downs.” (https://vercel.com/geist/collapse) Ours: one piece of supporting detail is `Disclosure`, and a single optional block is `Collapsible`.",
1707
+ behaviour: "“Default state is closed unless the first-time visitor must read the content to act.” (https://vercel.com/geist/collapse) “Inside a `CollapseGroup`, allow only one panel open at a time when items are mutually exclusive; allow multiple when items are independent.” (https://vercel.com/geist/collapse) “Animate the open/close transition; jump-cuts make the page feel like it teleported.” (https://vercel.com/geist/collapse) “Don’t nest Collapse more than one level deep. Two-level nesting hides too much and breaks the keyboard tab path.” (https://vercel.com/geist/collapse) Ours: a flat list divided by 1 px lines, closed at the top and under every section, with no fill, no card and no hover change. A section is a heading row with the title at the start and a 16 px chevron at the end that points right while closed and down while open. `size` is `default` (a 24 px semibold title, an 84 px row) or `small` (a 16 px medium title, a 48 px row). Colour is neutral only, in every state. Exclusive sections open one at a time and the open one can be closed; independent ones stay open together with `multiple`. The panel animates its height, and under reduced motion it jumps. An accordion never nests in another.",
1708
+ words: "“Heading is Title Case and names the topic, not the action (`Advanced Settings`, not `Show Advanced Settings`).” (https://vercel.com/geist/collapse) “Body is sentence case prose with normal section formatting; treat the panel as a small page, not a tooltip.” (https://vercel.com/geist/collapse) “Don’t put primary destructive actions inside a closed Collapse; the user has to click twice to reach the warning.” (https://vercel.com/geist/collapse) Ours: the trigger is required and names its topic as a noun phrase or a question a person asks, in sentence case, not Title Case: *Advanced settings*, never *Show advanced settings*.",
1709
+ access: "“The trigger is a `<button>` with `aria-expanded` that flips on toggle and `aria-controls` pointing at the panel id.” (https://vercel.com/geist/collapse) “Enter and Space toggle. Don’t bind any other key globally; arrow keys move focus inside the panel content.” (https://vercel.com/geist/collapse) “Render the panel content in the DOM when closed (with `hidden` or visibility) so search and find-in-page still hit it; lazy-render only for expensive content.” (https://vercel.com/geist/collapse) Ours: a closed panel stays in the page, hidden until found, so find-in-page opens it; pass `hiddenUntilFound={false}` on a heavy panel to mount it only when open. Headings keep the page's outline.",
1600
1710
  },
1601
1711
  {
1602
1712
  name: "AspectRatio",
@@ -1631,27 +1741,16 @@ export const USAGE = [
1631
1741
  words: "The buttons name their move: *Previous slide*, *Next slide*.",
1632
1742
  access: "A `region` described as a carousel; each slide is a `group` described as a slide.",
1633
1743
  },
1634
- {
1635
- name: "ChannelView",
1636
- module: "channel-view",
1637
- family: "Other",
1638
- summary: "A work-chat channel, as presentation only: the channel's header, the day it opens on, the messages in it, the replies gathered under a message, a file left in the conversation, and the card a voice call leaves behind.",
1639
- when: "Showing a channel of Alveare's work chat: its header, its days, messages, replies, files and calls.",
1640
- instead: "An Agent's conversation is the Agents interface (`@nadicodeai/ui/agents`). A list of records is a list or table.",
1641
- behaviour: "A 36 px member column, a 10 px gap to the text, the author and the time on one baseline; replies gather in a pill under their message, and cards sit at the resting fill inside the line: Alveare's channel geometry.",
1642
- words: "The product's members and messages; every label (members, replies, reactions) comes from its catalogue.",
1643
- access: 'A person\'s picture is `role="img"` named by their name; the member stack is named by `membersLabel`; a reaction is named by its `label`, its glyph hidden.',
1644
- },
1645
1744
  {
1646
1745
  name: "Collapsible",
1647
1746
  module: "collapsible",
1648
1747
  family: "Other",
1649
1748
  summary: "Content that a trigger shows and hides.",
1650
- when: "Building a disclosure the complete owners do not cover: nested files in a sidebar, a group of rows.",
1651
- instead: "Supporting detail on a page is `Disclosure`; sections are `Accordion`; a long block is `ExpandableBlock`.",
1652
- behaviour: "The trigger toggles the content; closed content is not rendered for the reader.",
1653
- words: "The trigger names what is inside.",
1654
- access: "The trigger exposes `aria-expanded` and controls its content.",
1749
+ when: "“Use Collapse for optional, advanced, or repetitive content that most users skip on most visits (FAQ, advanced settings, request payload preview).” (https://vercel.com/geist/collapse) “For top-level structure that every user reads, use page sections with regular headings; collapsing primary content hides what the page is about.” (https://vercel.com/geist/collapse) Ours: Geist's `Collapse` is `Collapsible`. It is for a disclosure the complete owners do not cover: nested files in a sidebar, a group of rows.",
1750
+ instead: "“Single Collapse for one optional block. `CollapseGroup` for a related set; switch to `Tabs` when the items are sibling views, not optional drill-downs.” (https://vercel.com/geist/collapse) Ours: `CollapseGroup` is `Accordion`. Supporting detail on a page is `Disclosure`.",
1751
+ behaviour: "“Default state is closed unless the first-time visitor must read the content to act.” (https://vercel.com/geist/collapse) “Animate the open/close transition; jump-cuts make the page feel like it teleported.” (https://vercel.com/geist/collapse) “Don’t nest Collapse more than one level deep. Two-level nesting hides too much and breaks the keyboard tab path.” (https://vercel.com/geist/collapse) Ours: the trigger toggles the content and has no chrome of its own: the caller draws it, neutral. The content animates its height, and under reduced motion it jumps.",
1752
+ words: "“Heading is Title Case and names the topic, not the action (`Advanced Settings`, not `Show Advanced Settings`).” (https://vercel.com/geist/collapse) “Body is sentence case prose with normal section formatting; treat the panel as a small page, not a tooltip.” (https://vercel.com/geist/collapse) “Don’t put primary destructive actions inside a closed Collapse; the user has to click twice to reach the warning.” (https://vercel.com/geist/collapse) Ours: the heading is sentence case, not Title Case (*Advanced settings*). The trigger is required and names what is inside, as a noun.",
1753
+ access: "“The trigger is a `<button>` with `aria-expanded` that flips on toggle and `aria-controls` pointing at the panel id.” (https://vercel.com/geist/collapse) “Enter and Space toggle. Don’t bind any other key globally; arrow keys move focus inside the panel content.” (https://vercel.com/geist/collapse) “Render the panel content in the DOM when closed (with `hidden` or visibility) so search and find-in-page still hit it; lazy-render only for expensive content.” (https://vercel.com/geist/collapse) Ours: closed content is not rendered by default; pass `hiddenUntilFound` on cheap content so find-in-page reaches and opens it.",
1655
1754
  },
1656
1755
  {
1657
1756
  name: "CompanyCard",
@@ -1670,22 +1769,11 @@ export const USAGE = [
1670
1769
  family: "Other",
1671
1770
  summary: "Supporting detail a person opens on demand, such as other download formats or a checksum.",
1672
1771
  when: "Supporting detail a person opens on demand, such as other download formats or a checksum.",
1673
- instead: "Named sections are `Accordion`. A long block cut short is `ExpandableBlock`. A warning is never hidden in one.",
1772
+ instead: "Named sections are `Accordion`. A warning is never hidden in one.",
1674
1773
  behaviour: "Opens without JavaScript and renders on the server; the chevron turns when open.",
1675
1774
  words: "The closed label names what is inside: *Other formats*, *Checksum*.",
1676
1775
  access: "A native `details` and `summary`, announced as a disclosure.",
1677
1776
  },
1678
- {
1679
- name: "ExpandableBlock",
1680
- module: "expandable-block",
1681
- family: "Other",
1682
- summary: "A tall block cut to its first lines, with a control to read the rest.",
1683
- when: "A long output inside a conversation or a run: a tool's result, a pasted log.",
1684
- instead: "Supporting detail behind a label is `Disclosure`. Exact text to copy is `CodeBlock`.",
1685
- behaviour: "Shows the control only when the content runs past 120 px; expanding shows it whole.",
1686
- words: "The expand and collapse labels come from the product's catalogue.",
1687
- access: "The control is a `button` with `aria-expanded` and the current label as its name.",
1688
- },
1689
1777
  {
1690
1778
  name: "HiveScene",
1691
1779
  module: "hive-scene",
@@ -1713,22 +1801,22 @@ export const USAGE = [
1713
1801
  module: "numbered-steps",
1714
1802
  family: "Other",
1715
1803
  summary: "Steps a person follows, each with its number, its words and its own action.",
1716
- when: "A short sequence the person does: installing Nadia from Downloads, connecting Teams, the Dashboard's setup guide; or a sequence whose real progress the page tracks, such as the company check.",
1804
+ when: "A short sequence the person does: installing Nadia from Downloads, connecting Teams, the Dashboard's setup guide; or a sequence whose real progress the page tracks, such as the company check; and, as `columns`, the few steps a page explains, such as the website's how it works. Vercel draws its Speed Insights *Get Started* the same way: three numbered steps, each with its words and its own action.",
1717
1805
  instead: 'Stages the system runs on an entry screen are the EntryStageList; the steps of an Agent\'s run are a RunTimeline; a setup with a wait is a FormDrawer with its "Step 2 of 3" line.',
1718
- behaviour: "Each step is a 28 px square number in label-mono, its title, one muted description and, under it, the step's own action. Instructions mark no step and every number stays muted, because the page cannot know which one the person is on. Where the page tracks real progress, `current` turns that step's number to ink and writes its word beside the title. `framed` draws one bordered list with a seam between steps. It is the one owner of a drawn step number.",
1806
+ behaviour: "Each step is a 28 px square number in label-mono, its title, one muted description and, under it, the step's own action. Instructions mark no step and every number stays muted, because the page cannot know which one the person is on. Where the page tracks real progress, `current` turns that step's number to ink and writes its word beside the title. `framed` draws one bordered list with a seam between steps; `columns` sets the steps side by side from 640 px, two across then three, each number above its words, and is never framed. Colour is neutral: the current number is ink on the page's background and the others are muted; no hue marks a step. It is the one owner of a drawn step number.",
1719
1807
  words: 'The title is what the person does, in the imperative ("Install Nadia"), or a finished step in the past tense ("Sofia looked up acme.it"). The current step\'s word is short: "Now". An action names its verb and carries its platform line ("macOS · 2.14.0 · Apple silicon").',
1720
1808
  access: 'An ordered list named by `label`, so assistive technology reads the count and position; the drawn numbers are hidden. The current step carries `aria-current="step"`. The titles are not headings, so the list never breaks the page\'s heading order.',
1721
1809
  },
1722
1810
  {
1723
- name: "PluginSlotBoundary",
1724
- module: "plugin-slot-boundary",
1811
+ name: "Prose",
1812
+ module: "prose",
1725
1813
  family: "Other",
1726
- summary: "The frame around content a plugin draws inside a product.",
1727
- when: "Any surface a plugin renders into: it names the plugin, its permissions and its state around the plugin's own content.",
1728
- instead: "The product's own sections are `SettingsSection` or `ObjectSection`.",
1729
- behaviour: "A title and description, the permissions with their levels, a warning or an error when given, then the plugin's content in a labelled region and its footer. `disabled` dims the content.",
1730
- words: "The plugin's title as the plugin names itself; permissions are short nouns with a sentence each.",
1731
- access: "The permissions are a labelled list and the plugin's content a region labelled `contentLabel`, so a reader knows where the plugin begins and ends.",
1814
+ summary: "Written text a renderer produced, set in the design system's type roles: headings, paragraphs, lists, links, code, quotes and tables, none of them given a class by the page.",
1815
+ when: '"Copy styles are for multiple lines and have a higher line-height than Label." (https://vercel.com/geist/typography) Ours: Markdown or other rich text turned into elements by the product\'s renderer: a legal document in the Portal, a company wiki page in Nadia.',
1816
+ instead: "A conversation's Markdown stays the conversation's own `MarkdownText`. One fact in a sentence is its own role (`text-body-sm`), a table the product builds is `DataTable`, and a code sample with a copy button is `CodeBlock`. Prose that is a column of settings is `SettingsSection`.",
1817
+ behaviour: '"Headings: Used to introduce pages or sections." (https://vercel.com/geist/typography) "`text-copy-13-mono`: Used for inline code mentions." (https://vercel.com/geist/typography) Ours: it styles the elements inside it by their tag, so the product chooses the renderer and keeps only what is its own: an anchor on a heading, a page link that opens in place, the level a heading takes below the page\'s own title. A heading\'s size follows its tag: `h1` 24 px, `h2` 20, `h3` 16, `h4` to `h6` 14 (`heading-md` to `heading-compact`), with 24 px above and none below, so a renderer that moves a heading down a level moves its size down with it; blocks stand 12 px apart; lists indent 20 px; a quote has a 2 px rule at its left; a code block is a bordered recessed box in mono 13 px, inline code a muted chip, with no syntax colour; a table has medium muted headings, a 1 px rule under every row and no frame. Colour: ink for the text, the link blue on links alone, muted for quotes and table headings; no other hue. It sets no width: the reading column does.',
1818
+ words: "None of its own: the text is the document's. A heading is a noun or a short sentence in the document's language; a link's text names where it goes.",
1819
+ access: "It adds no landmark and no role. The renderer keeps the document's heading order and each link's text; a table keeps its `th` cells and a code block its `pre`.",
1732
1820
  },
1733
1821
  {
1734
1822
  name: "ReadingColumn",
@@ -1746,33 +1834,33 @@ export const USAGE = [
1746
1834
  module: "resizable",
1747
1835
  family: "Other",
1748
1836
  summary: "Panels a person resizes by dragging the handle between them.",
1749
- when: "Side-by-side panes whose balance a person sets: a conversation and its side panel.",
1750
- instead: "The product frame's sidebar resizes through `DashboardShell` or `DesktopAppShell`.",
1751
- behaviour: "The handle moves on drag and on arrow keys; each panel keeps its minimum and maximum.",
1752
- words: "None of its own.",
1753
- access: "The handle is the resizable library's focusable separator.",
1837
+ when: "Side-by-side panes whose balance a person sets: a conversation and its side panel. Nadia's side panel is this, with the window's grid column as the group; it is not a hand-made separator.",
1838
+ instead: "The product frame's sidebar resizes through `Sidebar`'s `resize` handle or `DesktopAppShell`. A width a person never changes is a plain grid column.",
1839
+ behaviour: 'The handle moves on drag and on arrow keys, and returns the panels to their default size on a double click; each panel keeps its minimum and maximum. A number is pixels (`minSize={352}`), a string without a unit is a percentage (`defaultSize="60"`), and `"20rem"` and `"40vw"` are read as written, so a panel that must keep a readable width is set in pixels and the pane beside it takes the rest. The handle is a 1 px line with a 4 px reach on each side; it takes the ring colour while hovered, dragged or focused, and `withHandle` adds a grip for a handle that must be found. Colour: neutral until it is touched, then the ring colour, and nothing else.',
1840
+ words: "Required: the handle's name, a verb phrase from the product's catalogue: *Resize the side panel*. A panel has none of its own.",
1841
+ access: "The handle is the resizable library's focusable `separator` with its value, named by the `aria-label` the type requires; the arrow keys move it and Home and End take it to the ends of its range.",
1754
1842
  },
1755
1843
  {
1756
1844
  name: "ScrollArea",
1757
1845
  module: "scroll-area",
1758
1846
  family: "Other",
1759
- summary: "An area that scrolls with the product's own thin scrollbar.",
1760
- when: "A bounded area whose content overflows on one axis: a side panel's list, a menu's long body.",
1761
- instead: "A page scrolls as the document. A rail of slides is `Carousel`.",
1762
- behaviour: "The viewport scrolls natively, so wheel, touch and keyboard all reach it; the thin scrollbar is drawn over the content.",
1763
- words: "None of its own.",
1764
- access: "The content keeps its own semantics; the scrollbar is paint.",
1847
+ summary: "An area that scrolls along one axis, with a fade where more is waiting.",
1848
+ when: '“Use Scroller for an overflowing list of peer items along a single axis: chip rows, log streams, code snippets, command palettes.” (https://vercel.com/geist/scroller) “Pick `vertical` for stacked feeds, `horizontal` for chip and tile rails, `free` only when content genuinely scrolls in both directions (logs with very long lines).” (https://vercel.com/geist/scroller) “For paginated or virtualized data sets larger than a few hundred items, render with a virtualization library inside the Scroller rather than dumping every node into the DOM.” (https://vercel.com/geist/scroller) Ours: Geist\'s `Scroller` is `ScrollArea`, and `vertical`, `horizontal` and `free` are `overflow="y"`, `"x"` and `"both"`. A bounded area whose content overflows: a side panel\'s list, a menu\'s long body, a rail of chips or logos.',
1849
+ instead: "Ours: a page scrolls as the document. A sequence of slides on the website is `Carousel`.",
1850
+ behaviour: "“Buttons will automatically scroll to a given **direct** child.” (https://vercel.com/geist/scroller) “Auto-scroll buttons target direct children only. If items are wrapped in extra layout nodes the buttons won’t find their target.” (https://vercel.com/geist/scroller) “Show edge fade or shadow affordances on the clipped axis so users see there’s more content past the viewport.” (https://vercel.com/geist/scroller) “Keep item widths and gaps consistent in horizontal scrollers; ragged edges break the snap rhythm and make the rail feel broken.” (https://vercel.com/geist/scroller) Ours: `overflow` is required. Give the area its height or width with `className`, and a border or radius with `frameClassName`, which draws only the scrolling box and leaves the buttons outside it. The scrollbar is hidden; a 40 px fade of the surface at each clipped edge is the only sign there is more, and it is gone at an edge with nothing beyond it. The viewport scrolls natively, so wheel, touch and keyboard all reach it. `buttonLabels` adds two round 32 px outline buttons, 16 px from the area: above it and centred for `y`, below it and at the start for `x`. They sit disabled at the ends. Smooth scrolling gives way to an instant jump under reduced motion. Colour is neutral only.",
1851
+ words: "“Scroller buttons need `aria-label`s that name the direction and content (`Scroll customer logos left`), not bare `Previous`/`Next`.” (https://vercel.com/geist/scroller) Ours: *Scroll installations left*; both labels are required with the buttons.",
1852
+ access: "“Tab order follows DOM order, so place items in reading order regardless of visual scroll direction.” (https://vercel.com/geist/scroller) “Make sure focusing an off-screen item scrolls it into view; the default browser behavior covers this, but custom focus traps can break it.” (https://vercel.com/geist/scroller) Ours: the content keeps its own semantics. The viewport is a tab stop when it scrolls.",
1765
1853
  },
1766
1854
  {
1767
1855
  name: "Separator",
1768
1856
  module: "separator",
1769
1857
  family: "Other",
1770
1858
  summary: "A 1 px line between groups.",
1771
- when: "Separating groups inside one surface: menu sections, a card's header from its toolbar.",
1772
- instead: "A card's footer draws its own seam; a list in a card is a `divided` `ItemGroup`. Never a border around a region.",
1773
- behaviour: "Horizontal or vertical, in the line colour.",
1859
+ when: "Ours: separating groups inside one surface: menu sections, a card's header from its toolbar, inline items in a row. Horizontal between stacked groups, vertical between items in a row.",
1860
+ instead: "Ours: a card's footer draws its own seam; a list in a card is a `divided` `ItemGroup`. Never a border around a region.",
1861
+ behaviour: "“Use `inset` when a divider meets a 1px frame painted over the content. The separator reserves 1px at each end and uses solid `gray-400`. A physical parent border already reserves its edge space and does not need this inset.” (https://vercel.com/geist/separator) Ours: horizontal or vertical, exactly 1 px, in the `border` colour (not `gray-400`) and never any other: a separator has no hue, no state and no padding. A vertical one takes its height from the flex row that holds it, so give the row a height and `items-center`.",
1774
1862
  words: "None.",
1775
- access: "A `separator` from Base UI with no name of its own.",
1863
+ access: "Ours: hidden from assistive technology, since it only paints a line; pass `aria-hidden={false}` when the line divides content a reader should hear as divided.",
1776
1864
  },
1777
1865
  {
1778
1866
  name: "ToolIcon",