@twoux/ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/LICENSE.md +61 -0
  2. package/LICENSES/Apache-2.0.txt +202 -0
  3. package/NOTICE.md +64 -0
  4. package/SHADCN-DIFF.md +228 -0
  5. package/agent/AGENTS.md +358 -0
  6. package/agent/docs-md/accessibility.md +49 -0
  7. package/agent/docs-md/action-catalogue.md +81 -0
  8. package/agent/docs-md/actions.md +74 -0
  9. package/agent/docs-md/agent-activity-log.md +70 -0
  10. package/agent/docs-md/agent-permissions.md +82 -0
  11. package/agent/docs-md/agent-run.md +90 -0
  12. package/agent/docs-md/agents-on-behalf.md +74 -0
  13. package/agent/docs-md/ai-column.md +85 -0
  14. package/agent/docs-md/ai-origin-label.md +70 -0
  15. package/agent/docs-md/announcing-features.md +73 -0
  16. package/agent/docs-md/answer-actions.md +81 -0
  17. package/agent/docs-md/app-shell.md +114 -0
  18. package/agent/docs-md/artifact-card.md +89 -0
  19. package/agent/docs-md/ask-on-screen.md +72 -0
  20. package/agent/docs-md/assistant-page.md +100 -0
  21. package/agent/docs-md/assistant-panel.md +142 -0
  22. package/agent/docs-md/avatar.md +82 -0
  23. package/agent/docs-md/banner.md +106 -0
  24. package/agent/docs-md/board.md +163 -0
  25. package/agent/docs-md/breadcrumb.md +89 -0
  26. package/agent/docs-md/breakdown-table.md +97 -0
  27. package/agent/docs-md/bulk-action-bar.md +112 -0
  28. package/agent/docs-md/button.md +293 -0
  29. package/agent/docs-md/card.md +115 -0
  30. package/agent/docs-md/change-list.md +99 -0
  31. package/agent/docs-md/change-request.md +166 -0
  32. package/agent/docs-md/chart-area.md +87 -0
  33. package/agent/docs-md/chart-bar.md +100 -0
  34. package/agent/docs-md/chart-box-plot.md +88 -0
  35. package/agent/docs-md/chart-bullet.md +89 -0
  36. package/agent/docs-md/chart-candlestick.md +112 -0
  37. package/agent/docs-md/chart-card-line.md +120 -0
  38. package/agent/docs-md/chart-donut.md +90 -0
  39. package/agent/docs-md/chart-drill-down.md +72 -0
  40. package/agent/docs-md/chart-funnel.md +71 -0
  41. package/agent/docs-md/chart-heatmap.md +76 -0
  42. package/agent/docs-md/chart-histogram.md +81 -0
  43. package/agent/docs-md/chart-map.md +165 -0
  44. package/agent/docs-md/chart-mixed.md +88 -0
  45. package/agent/docs-md/chart-price.md +92 -0
  46. package/agent/docs-md/chart-radial-bars.md +109 -0
  47. package/agent/docs-md/chart-radial-lines.md +111 -0
  48. package/agent/docs-md/chart-sankey.md +104 -0
  49. package/agent/docs-md/chart-scatter.md +87 -0
  50. package/agent/docs-md/chart-share-bar.md +89 -0
  51. package/agent/docs-md/chart-treemap.md +98 -0
  52. package/agent/docs-md/chart-waterfall.md +93 -0
  53. package/agent/docs-md/checkbox.md +131 -0
  54. package/agent/docs-md/choosing-selection-control.md +71 -0
  55. package/agent/docs-md/citations-sources.md +79 -0
  56. package/agent/docs-md/citing-sources.md +63 -0
  57. package/agent/docs-md/clarify-before-acting.md +69 -0
  58. package/agent/docs-md/clarifying-question.md +92 -0
  59. package/agent/docs-md/code-block.md +174 -0
  60. package/agent/docs-md/colours.md +87 -0
  61. package/agent/docs-md/columns-menu.md +140 -0
  62. package/agent/docs-md/combobox.md +161 -0
  63. package/agent/docs-md/command-palette.md +126 -0
  64. package/agent/docs-md/confidence-note.md +87 -0
  65. package/agent/docs-md/configurable-dashboard.md +76 -0
  66. package/agent/docs-md/confirmation-dialog.md +93 -0
  67. package/agent/docs-md/connected-tools.md +80 -0
  68. package/agent/docs-md/connector-picker.md +127 -0
  69. package/agent/docs-md/consistency.md +53 -0
  70. package/agent/docs-md/content-density.md +61 -0
  71. package/agent/docs-md/conversation-history.md +78 -0
  72. package/agent/docs-md/create-record.md +80 -0
  73. package/agent/docs-md/csv-import.md +88 -0
  74. package/agent/docs-md/dashboard-items.md +78 -0
  75. package/agent/docs-md/data-export.md +61 -0
  76. package/agent/docs-md/data-grid.md +137 -0
  77. package/agent/docs-md/data-vis-colours.md +76 -0
  78. package/agent/docs-md/data-visualisation.md +76 -0
  79. package/agent/docs-md/date-picker.md +178 -0
  80. package/agent/docs-md/delete-record.md +75 -0
  81. package/agent/docs-md/dense-table.md +495 -0
  82. package/agent/docs-md/design-tokens.md +53 -0
  83. package/agent/docs-md/disabled-read-only.md +75 -0
  84. package/agent/docs-md/drag-and-drop.md +74 -0
  85. package/agent/docs-md/dropdown-menu.md +137 -0
  86. package/agent/docs-md/edit-record.md +73 -0
  87. package/agent/docs-md/empty-loading-error.md +78 -0
  88. package/agent/docs-md/empty-state.md +128 -0
  89. package/agent/docs-md/error-messages.md +75 -0
  90. package/agent/docs-md/error-state.md +123 -0
  91. package/agent/docs-md/export-menu.md +86 -0
  92. package/agent/docs-md/feedback-prompt.md +88 -0
  93. package/agent/docs-md/field-mapping.md +78 -0
  94. package/agent/docs-md/file-drop.md +110 -0
  95. package/agent/docs-md/filter-bar.md +138 -0
  96. package/agent/docs-md/filter-panel.md +103 -0
  97. package/agent/docs-md/filtering-search.md +79 -0
  98. package/agent/docs-md/follow-ups-starters.md +79 -0
  99. package/agent/docs-md/form-validation.md +95 -0
  100. package/agent/docs-md/generated-columns.md +65 -0
  101. package/agent/docs-md/generated-drafts-files.md +71 -0
  102. package/agent/docs-md/grouped-list.md +89 -0
  103. package/agent/docs-md/guide-cue.md +170 -0
  104. package/agent/docs-md/help-panel.md +103 -0
  105. package/agent/docs-md/help-system.md +74 -0
  106. package/agent/docs-md/iconography.md +49 -0
  107. package/agent/docs-md/inbox.md +108 -0
  108. package/agent/docs-md/inline-editing-validation.md +80 -0
  109. package/agent/docs-md/inline-input-in-answer.md +70 -0
  110. package/agent/docs-md/items-palette.md +116 -0
  111. package/agent/docs-md/keyboard-model.md +47 -0
  112. package/agent/docs-md/keyboard-navigation.md +68 -0
  113. package/agent/docs-md/keyboard-shortcut-hint.md +87 -0
  114. package/agent/docs-md/keyboard-shortcut-sheet.md +90 -0
  115. package/agent/docs-md/labelling-ai-output.md +66 -0
  116. package/agent/docs-md/layout.md +47 -0
  117. package/agent/docs-md/list-table.md +254 -0
  118. package/agent/docs-md/loading-skeleton.md +91 -0
  119. package/agent/docs-md/managing-agents.md +71 -0
  120. package/agent/docs-md/message-composer.md +105 -0
  121. package/agent/docs-md/migrating.md +49 -0
  122. package/agent/docs-md/motion.md +93 -0
  123. package/agent/docs-md/navigation.md +76 -0
  124. package/agent/docs-md/notification-channels.md +65 -0
  125. package/agent/docs-md/notifications-decision.md +100 -0
  126. package/agent/docs-md/onboarding-step.md +114 -0
  127. package/agent/docs-md/order-book.md +108 -0
  128. package/agent/docs-md/page-header.md +96 -0
  129. package/agent/docs-md/pagination.md +88 -0
  130. package/agent/docs-md/pane-layout.md +105 -0
  131. package/agent/docs-md/period-comparison.md +66 -0
  132. package/agent/docs-md/plan-card.md +120 -0
  133. package/agent/docs-md/popover-rules.md +88 -0
  134. package/agent/docs-md/prompt-editor.md +90 -0
  135. package/agent/docs-md/propose-apply.md +65 -0
  136. package/agent/docs-md/provider-logo.md +97 -0
  137. package/agent/docs-md/radio.md +83 -0
  138. package/agent/docs-md/ranked-list.md +102 -0
  139. package/agent/docs-md/reasoning-progress.md +72 -0
  140. package/agent/docs-md/record-detail.md +71 -0
  141. package/agent/docs-md/regenerating-answers.md +70 -0
  142. package/agent/docs-md/run-status-bar.md +94 -0
  143. package/agent/docs-md/saved-tasks-schedules.md +63 -0
  144. package/agent/docs-md/saved-views.md +149 -0
  145. package/agent/docs-md/saving-views.md +68 -0
  146. package/agent/docs-md/scheduler.md +258 -0
  147. package/agent/docs-md/secondary-panels.md +72 -0
  148. package/agent/docs-md/segmented-control.md +88 -0
  149. package/agent/docs-md/selection-bulk-actions.md +70 -0
  150. package/agent/docs-md/separator.md +75 -0
  151. package/agent/docs-md/setup-checklist.md +92 -0
  152. package/agent/docs-md/shape-elevation.md +55 -0
  153. package/agent/docs-md/sheet-grid.md +211 -0
  154. package/agent/docs-md/side-panel.md +134 -0
  155. package/agent/docs-md/sign-in.md +114 -0
  156. package/agent/docs-md/spacing.md +55 -0
  157. package/agent/docs-md/split-button.md +100 -0
  158. package/agent/docs-md/stat-compare.md +82 -0
  159. package/agent/docs-md/static-dashboard.md +75 -0
  160. package/agent/docs-md/status-badge.md +140 -0
  161. package/agent/docs-md/step-card.md +75 -0
  162. package/agent/docs-md/stepper.md +92 -0
  163. package/agent/docs-md/suggested-changes.md +91 -0
  164. package/agent/docs-md/switch.md +78 -0
  165. package/agent/docs-md/table-settings.md +81 -0
  166. package/agent/docs-md/tabs.md +85 -0
  167. package/agent/docs-md/tag-input.md +108 -0
  168. package/agent/docs-md/tag.md +79 -0
  169. package/agent/docs-md/task-builder.md +87 -0
  170. package/agent/docs-md/team-instructions.md +72 -0
  171. package/agent/docs-md/templates-gallery.md +85 -0
  172. package/agent/docs-md/text-input.md +160 -0
  173. package/agent/docs-md/thinking-steps.md +89 -0
  174. package/agent/docs-md/timeline.md +189 -0
  175. package/agent/docs-md/timestamp.md +84 -0
  176. package/agent/docs-md/timestamps.md +70 -0
  177. package/agent/docs-md/toast.md +117 -0
  178. package/agent/docs-md/tooltip.md +90 -0
  179. package/agent/docs-md/typography.md +62 -0
  180. package/agent/docs-md/undo-destructive.md +66 -0
  181. package/agent/docs-md/unsaved-changes.md +71 -0
  182. package/agent/docs-md/usage-meter.md +80 -0
  183. package/agent/docs-md/user-feedback.md +71 -0
  184. package/agent/docs-md/voice-and-tone.md +60 -0
  185. package/agent/docs-md/why-choice.md +93 -0
  186. package/agent/docs-md/workspace-onboarding.md +74 -0
  187. package/agent/docs-md/writing.md +48 -0
  188. package/agent/llms-full.txt +18369 -0
  189. package/agent/llms.txt +198 -0
  190. package/agent/skills/twoux-audit/SKILL.md +49 -0
  191. package/agent/skills/twoux-build/SKILL.md +39 -0
  192. package/agent/skills/twoux-design/SKILL.md +54 -0
  193. package/agent/skills/twoux-write/SKILL.md +32 -0
  194. package/bin/check.mjs +123 -0
  195. package/bin/docs.mjs +24 -0
  196. package/bin/guard.mjs +27 -0
  197. package/bin/init.mjs +113 -0
  198. package/bin/knowledge.mjs +87 -0
  199. package/bin/mcp.mjs +116 -0
  200. package/bin/theme.mjs +168 -0
  201. package/bin/twoux.mjs +25 -0
  202. package/package.json +80 -3
  203. package/registry/jobs.json +1028 -0
  204. package/registry/palette.json +770 -0
  205. package/registry/rules.json +913 -0
  206. package/src/components/agent-review.tsx +364 -0
  207. package/src/components/ai-basics.tsx +212 -0
  208. package/src/components/ai-work.tsx +304 -0
  209. package/src/components/ai.tsx +227 -0
  210. package/src/components/alert-dialog.tsx +203 -0
  211. package/src/components/alert.tsx +86 -0
  212. package/src/components/avatar.tsx +114 -0
  213. package/src/components/badge.tsx +59 -0
  214. package/src/components/board.tsx +397 -0
  215. package/src/components/breadcrumb.tsx +114 -0
  216. package/src/components/button-group.tsx +90 -0
  217. package/src/components/button.tsx +109 -0
  218. package/src/components/calendar.tsx +247 -0
  219. package/src/components/card.tsx +102 -0
  220. package/src/components/chart-map.tsx +336 -0
  221. package/src/components/charts-more.tsx +1331 -0
  222. package/src/components/charts-radial.tsx +328 -0
  223. package/src/components/charts.tsx +390 -0
  224. package/src/components/checkbox.tsx +40 -0
  225. package/src/components/code.tsx +139 -0
  226. package/src/components/collapsible.tsx +37 -0
  227. package/src/components/combobox.tsx +339 -0
  228. package/src/components/command-extras.tsx +11 -0
  229. package/src/components/command.tsx +196 -0
  230. package/src/components/data-grid.tsx +213 -0
  231. package/src/components/data-table.tsx +334 -0
  232. package/src/components/data-views.tsx +740 -0
  233. package/src/components/date-picker.tsx +686 -0
  234. package/src/components/dense-table.tsx +207 -0
  235. package/src/components/dialog.tsx +162 -0
  236. package/src/components/dropdown-menu.tsx +264 -0
  237. package/src/components/empty.tsx +114 -0
  238. package/src/components/feedback-prompt.tsx +80 -0
  239. package/src/components/field.tsx +254 -0
  240. package/src/components/fields.tsx +112 -0
  241. package/src/components/flow.tsx +840 -0
  242. package/src/components/guide-cue.tsx +273 -0
  243. package/src/components/inbox.tsx +113 -0
  244. package/src/components/input-group.tsx +176 -0
  245. package/src/components/input.tsx +26 -0
  246. package/src/components/kbd.tsx +34 -0
  247. package/src/components/label.tsx +27 -0
  248. package/src/components/labels.tsx +176 -0
  249. package/src/components/motion.tsx +136 -0
  250. package/src/components/onboarding.tsx +454 -0
  251. package/src/components/order-book.tsx +185 -0
  252. package/src/components/pane-layout.tsx +417 -0
  253. package/src/components/popover.tsx +96 -0
  254. package/src/components/provider-logos.data.ts +49 -0
  255. package/src/components/radio-group.tsx +52 -0
  256. package/src/components/scheduler.tsx +997 -0
  257. package/src/components/segmented-control.tsx +28 -0
  258. package/src/components/select.tsx +195 -0
  259. package/src/components/separator.tsx +31 -0
  260. package/src/components/sheet.tsx +148 -0
  261. package/src/components/shell.tsx +426 -0
  262. package/src/components/shortcut-sheet.tsx +83 -0
  263. package/src/components/skeleton.tsx +18 -0
  264. package/src/components/sonner.tsx +107 -0
  265. package/src/components/spinner.tsx +13 -0
  266. package/src/components/split-button.tsx +58 -0
  267. package/src/components/stepper.tsx +78 -0
  268. package/src/components/switch.tsx +43 -0
  269. package/src/components/table.tsx +126 -0
  270. package/src/components/tabs.tsx +118 -0
  271. package/src/components/tag-input.tsx +253 -0
  272. package/src/components/textarea.tsx +21 -0
  273. package/src/components/theme.tsx +80 -0
  274. package/src/components/timeline.tsx +408 -0
  275. package/src/components/toggle-group.tsx +87 -0
  276. package/src/components/toggle.tsx +54 -0
  277. package/src/components/tooltip.tsx +65 -0
  278. package/src/components/workspace-actions.tsx +294 -0
  279. package/src/components/workspace-import.tsx +465 -0
  280. package/src/components/workspace.tsx +1884 -0
  281. package/src/index.ts +80 -0
  282. package/src/lib/active-indicator.tsx +53 -0
  283. package/src/lib/chart-kit.tsx +72 -0
  284. package/src/lib/cn.ts +6 -0
  285. package/src/lib/shortcuts.tsx +61 -0
  286. package/src/lib/workspace.tsx +10 -0
  287. package/src/shadcn/alert-dialog.tsx +196 -0
  288. package/src/shadcn/alert.tsx +65 -0
  289. package/src/shadcn/avatar.tsx +108 -0
  290. package/src/shadcn/badge.tsx +47 -0
  291. package/src/shadcn/breadcrumb.tsx +108 -0
  292. package/src/shadcn/button-group.tsx +83 -0
  293. package/src/shadcn/button.tsx +63 -0
  294. package/src/shadcn/calendar.tsx +220 -0
  295. package/src/shadcn/card.tsx +91 -0
  296. package/src/shadcn/checkbox.tsx +31 -0
  297. package/src/shadcn/collapsible.tsx +33 -0
  298. package/src/shadcn/command.tsx +184 -0
  299. package/src/shadcn/dialog.tsx +158 -0
  300. package/src/shadcn/dropdown-menu.tsx +256 -0
  301. package/src/shadcn/empty.tsx +103 -0
  302. package/src/shadcn/field.tsx +248 -0
  303. package/src/shadcn/input-group.tsx +170 -0
  304. package/src/shadcn/input.tsx +20 -0
  305. package/src/shadcn/kbd.tsx +28 -0
  306. package/src/shadcn/label.tsx +23 -0
  307. package/src/shadcn/popover.tsx +88 -0
  308. package/src/shadcn/radio-group.tsx +44 -0
  309. package/src/shadcn/select.tsx +189 -0
  310. package/src/shadcn/separator.tsx +27 -0
  311. package/src/shadcn/sheet.tsx +142 -0
  312. package/src/shadcn/skeleton.tsx +13 -0
  313. package/src/shadcn/sonner.tsx +40 -0
  314. package/src/shadcn/switch.tsx +34 -0
  315. package/src/shadcn/table.tsx +115 -0
  316. package/src/shadcn/tabs.tsx +90 -0
  317. package/src/shadcn/textarea.tsx +17 -0
  318. package/src/shadcn/toggle-group.tsx +83 -0
  319. package/src/shadcn/toggle.tsx +46 -0
  320. package/src/shadcn/tooltip.tsx +56 -0
  321. package/src/styles/palette.css +127 -0
  322. package/src/styles/theme.css +35 -0
  323. package/src/styles/twoux.css +236 -0
  324. package/theme.json +14 -0
  325. package/README.md +0 -3
@@ -0,0 +1,495 @@
1
+ # Dense table
2
+
3
+ The List table at 28px rows for spreadsheet-like data, such as a life-cycle inventory or a product library, with group rows, row numbers with a row menu, a filter row, in-cell buttons and frozen first columns.
4
+
5
+ Kind: component · Group: Data · Status: built, import from @twoux/ui
6
+
7
+ ## When to use
8
+
9
+ - The data is spreadsheet-like and people scan many rows at once to compare values: inventories, ledgers, bills of materials, imports and field mappings.
10
+ - Rows fall into a few sections people read separately, such as Inputs, Wastes and emissions and Products.
11
+ - Every row has the same attributes and people need to find, compare and act on several rows.
12
+
13
+ ## When not to use
14
+
15
+ - As a density setting, a comfortable mode or a user preference. TwoUX has one density for the app; dense is only for this kind of data.
16
+ - For a list people triage one record at a time (an inbox, candidates). Use the List table at its default 36px rows.
17
+ - To type, paste from Excel, fill or undo cell edits. Use the Sheet grid with size="dense": a table's cells are not focusable.
18
+
19
+ ## Structure
20
+
21
+ 1. Table size="dense": 28px header and rows, 8px cell padding, 13px text
22
+ 2. Frozen first columns (data-pinned): selection, row number with ⋮ row menu, the identifying column
23
+ 3. Filter row: a search field under each filterable header
24
+ 4. Group rows: label, info tooltip, count, optionally collapsible
25
+ 5. Cells: Tag with a value, Switch with its word, two linked items with an arrow, n/a, in-cell button on hover and focus
26
+ 6. Footer: pagination; the Bulk action bar while rows are selected
27
+
28
+ ## Guidelines
29
+
30
+ - Use Table size="dense". Rows and the header are 28px, cells have 8px padding and text stays 13px, so dense never means smaller type.
31
+ - Freeze the selection column, the row number and the identifying column with data-pinned, and set each one's --pin offset. Wide tables scroll sideways inside their own box.
32
+ - Group rows split the table into sections. Put each group in its own TableBody with a TableGroupRow first: label, count, and an info tooltip saying what belongs there. Make it collapsible when the sections are long.
33
+ - The row number cell holds the ⋮ row menu, named “Actions for <row>”. Every row action lives there.
34
+ - An in-cell button (Open, Share) shows on row hover and keyboard focus, and always on touch screens. It is a shortcut: the same action is always in the row menu.
35
+ - A filter row puts one search field under each filterable header, named “Filter <column>”. Filtering goes back to page 1 and Esc clears a field.
36
+ - Keep at most one in-cell button per row, and no more than two quick actions plus the row menu, so rows stay scannable.
37
+
38
+ ## Content
39
+
40
+ - Tag with a value: the category as a Tag, then the value in tabular figures, such as Allocate 90.3 %. The Tag never carries the number.
41
+ - Switch with its word: the word says the current state (Published, Draft), never only the switch.
42
+ - Linked items: “Magnet preparation → NdFeB magnet powder”, read as “Magnet preparation to NdFeB magnet powder”.
43
+ - Use n/a (read as “not applicable”) when a value can't exist for the row, and a dash with hidden text when it exists but is empty.
44
+ - Right-align numbers in tabular figures with the same decimals; put units in their own column or the header, and region codes in monospace.
45
+ - Header labels in sentence case, short nouns: Amount, In reference unit.
46
+
47
+ ## Accessibility
48
+
49
+ - APG table pattern: a dense table is still a table, with an accessible name, header cells and scope="rowgroup" on group rows.
50
+ - The collapsible group label is a button with aria-expanded and aria-controls naming its TableBody.
51
+ - 2.5.8 Target size: row menu buttons and checkboxes keep a 24px hit area inside 28px rows.
52
+ - In-cell buttons stay in the tab order when hidden, appear on focus, and are named for their row (“Open Electricity, medium voltage”).
53
+ - Filter fields have their own labels, not only a placeholder.
54
+
55
+ ## Keyboard
56
+
57
+ | Keys | Action |
58
+ |---|---|
59
+ | Tab | Move through checkboxes, row menus, in-cell buttons and filter fields |
60
+ | Enter / Space | Open the row menu, press an in-cell button, collapse or expand a group |
61
+ | Esc (filter field) | Clear the filter |
62
+ | J / K, X, Enter | Row focus, selection and open, with useRowKeyboard as on the List table |
63
+
64
+ ## Examples
65
+
66
+ ### inventory
67
+
68
+ ```tsx
69
+ import {
70
+ DropdownMenuItem,
71
+ DropdownMenuSeparator,
72
+ Table,
73
+ TableBody,
74
+ TableCell,
75
+ TableCellAction,
76
+ TableGroupRow,
77
+ TableHead,
78
+ TableHeader,
79
+ TableLinkedItems,
80
+ TableNotApplicable,
81
+ TableRow,
82
+ TableRowNumber,
83
+ Tag,
84
+ toast,
85
+ } from "@twoux/ui";
86
+ import { Copy, PanelRight, Pencil, Trash2 } from "lucide-react";
87
+ import { type CSSProperties, useState } from "react";
88
+
89
+ export const meta = {
90
+ title: "Life-cycle inventory",
91
+ description: "size=\"dense\" for 28px rows. Group rows with an info tooltip and count collapse their section. Hover or tab into a row to reveal Open; the ⋮ menu on the row number has Open too. Scroll sideways: the row number and Flow stay in view.",
92
+ order: 1,
93
+ };
94
+
95
+ type Exchange = { id: string; flow: string; from?: [string, string]; kind: "Unit process" | "System process" | "Elementary flow"; allocation?: number; region: string; amount: number; unit: string; reference?: string; note?: string };
96
+ type Group = { id: string; label: string; info: string; rows: Exchange[] };
97
+
98
+ const GROUPS: Group[] = [
99
+ {
100
+ id: "intermediary",
101
+ label: "Intermediary inputs",
102
+ info: "Products made by other processes in this model and fed into this one.",
103
+ rows: [
104
+ { id: "e1", flow: "NdFeB magnet powder", from: ["Magnet preparation", "NdFeB magnet powder"], kind: "Unit process", allocation: 90.3, region: "CN", amount: 1.042, unit: "kg", reference: "1.042 kg" },
105
+ { id: "e2", flow: "Dysprosium oxide", from: ["Rare earth separation", "Dysprosium oxide"], kind: "System process", allocation: 6.1, region: "CN", amount: 0.031, unit: "kg", reference: "31 g" },
106
+ { id: "e3", flow: "Nickel plating", from: ["Surface treatment", "Plated part"], kind: "Unit process", region: "GB", amount: 0.18, unit: "m²", reference: "1,800 cm²" },
107
+ { id: "e4", flow: "Sintering, vacuum furnace", from: ["Heat treatment", "Sintered block"], kind: "Unit process", allocation: 100, region: "GB", amount: 1, unit: "item", reference: "1 item" },
108
+ ],
109
+ },
110
+ {
111
+ id: "inputs",
112
+ label: "Inputs",
113
+ info: "Energy, water and resources taken in directly, not made by another process in this model.",
114
+ rows: [
115
+ { id: "e5", flow: "Electricity, medium voltage", kind: "System process", region: "GB", amount: 14.6, unit: "kWh", reference: "52.56 MJ" },
116
+ { id: "e6", flow: "Natural gas, burned in furnace", kind: "System process", region: "RER", amount: 21.4, unit: "MJ", reference: "5.94 kWh" },
117
+ { id: "e7", flow: "Argon, liquid", kind: "System process", region: "RER", amount: 0.42, unit: "kg", reference: "420 g" },
118
+ { id: "e8", flow: "Water, deionised", kind: "Elementary flow", region: "GB", amount: 0.012, unit: "m³", reference: "12 l" },
119
+ { id: "e9", flow: "Transport, lorry 16 to 32 t", kind: "System process", region: "RER", amount: 312, unit: "t·km", reference: "312,000 kg·km" },
120
+ ],
121
+ },
122
+ {
123
+ id: "wastes",
124
+ label: "Wastes and emissions",
125
+ info: "What leaves the process without value: waste for treatment and emissions to air, water and soil.",
126
+ rows: [
127
+ { id: "e10", flow: "Grinding sludge", from: ["Hazardous waste treatment", "Treated sludge"], kind: "Unit process", region: "GB", amount: 0.064, unit: "kg", reference: "64 g" },
128
+ { id: "e11", flow: "Carbon dioxide, fossil", kind: "Elementary flow", region: "GLO", amount: 1.21, unit: "kg", note: "Air, urban" },
129
+ { id: "e12", flow: "Particulates, < 2.5 µm", kind: "Elementary flow", region: "GLO", amount: 0.0004, unit: "kg", reference: "0.4 g" },
130
+ ],
131
+ },
132
+ {
133
+ id: "products",
134
+ label: "Products",
135
+ info: "What the process makes. The reference product carries the result; co-products take a share by allocation.",
136
+ rows: [
137
+ { id: "e13", flow: "NdFeB magnet, sintered", kind: "Unit process", allocation: 94.5, region: "GB", amount: 1, unit: "kg", reference: "1 kg" },
138
+ { id: "e14", flow: "Magnet offcuts, for recycling", kind: "Unit process", allocation: 5.5, region: "GB", amount: 0.058, unit: "kg", reference: "58 g" },
139
+ ],
140
+ },
141
+ ];
142
+
143
+ const COLS = 9;
144
+ const fmt = (n: number) => n.toLocaleString("en-GB", { maximumFractionDigits: 4 });
145
+
146
+ export default function Example() {
147
+ const [collapsed, setCollapsed] = useState<string[]>([]);
148
+ const open = (e: Exchange) => toast(`Opened ${e.flow}`, { description: "In a product it opens in the side panel." });
149
+ let n = 0;
150
+ return (
151
+ <div className="w-full overflow-hidden rounded-sm border border-line bg-surface">
152
+ <div className="flex items-center gap-2 border-b border-line px-3 py-2">
153
+ <h3 className="text-md font-semibold text-ink">NdFeB magnet, per kg</h3>
154
+ <span className="text-sm text-ink-3">Corvane Magnetics · Sheffield plant · 2025</span>
155
+ </div>
156
+ <Table size="dense" aria-label="Inventory for NdFeB magnet, per kg" className="min-w-328 table-fixed" containerClassName="max-h-120">
157
+ <TableHeader>
158
+ <TableRow className="hover:bg-transparent">
159
+ <TableHead data-pinned="" className="w-16"><span aria-hidden="true">#</span><span className="sr-only">Row number</span></TableHead>
160
+ <TableHead data-pinned="last" style={{ "--pin": "64px" } as CSSProperties} className="w-64">Flow</TableHead>
161
+ <TableHead className="w-80">Provider</TableHead>
162
+ <TableHead className="w-44">Allocation</TableHead>
163
+ <TableHead className="w-20">Region</TableHead>
164
+ <TableHead className="w-24 text-right">Amount</TableHead>
165
+ <TableHead className="w-16">Unit</TableHead>
166
+ <TableHead className="w-36 text-right">In reference unit</TableHead>
167
+ <TableHead className="w-28">Compartment</TableHead>
168
+ </TableRow>
169
+ </TableHeader>
170
+ {GROUPS.map((g) => {
171
+ const isOpen = !collapsed.includes(g.id);
172
+ return (
173
+ <TableBody key={g.id} id={`inv-${g.id}`}>
174
+ <TableGroupRow
175
+ label={g.label}
176
+ info={g.info}
177
+ count={g.rows.length}
178
+ colSpan={COLS}
179
+ expanded={isOpen}
180
+ controls={`inv-${g.id}`}
181
+ onExpandedChange={(o) => setCollapsed((c) => (o ? c.filter((x) => x !== g.id) : [...c, g.id]))}
182
+ />
183
+ {isOpen &&
184
+ g.rows.map((e) => {
185
+ n += 1;
186
+ return (
187
+ <TableRow key={e.id}>
188
+ <TableRowNumber n={n} label={e.flow} data-pinned="">
189
+ <DropdownMenuItem onSelect={() => open(e)}><PanelRight />Open</DropdownMenuItem>
190
+ <DropdownMenuItem><Pencil />Edit amount</DropdownMenuItem>
191
+ <DropdownMenuItem><Copy />Duplicate</DropdownMenuItem>
192
+ <DropdownMenuSeparator />
193
+ <DropdownMenuItem variant="destructive"><Trash2 />Remove from inventory</DropdownMenuItem>
194
+ </TableRowNumber>
195
+ <TableCell data-pinned="last" style={{ "--pin": "64px" } as CSSProperties}>
196
+ <span className="flex min-w-0 items-center gap-2">
197
+ <span className="min-w-0 flex-1 truncate font-medium text-ink">{e.flow}</span>
198
+ <TableCellAction aria-label={`Open ${e.flow}`} onClick={() => open(e)}>Open</TableCellAction>
199
+ </span>
200
+ </TableCell>
201
+ <TableCell className="max-w-80">{e.from ? <TableLinkedItems from={e.from[0]} to={e.from[1]} /> : <TableNotApplicable />}</TableCell>
202
+ <TableCell>
203
+ <span className="flex items-center gap-2">
204
+ <Tag>{e.kind === "Elementary flow" ? "Elementary" : e.allocation !== undefined ? "Allocate" : e.kind}</Tag>
205
+ {e.allocation !== undefined ? <span className="tabular text-ink">{e.allocation.toLocaleString("en-GB", { minimumFractionDigits: 1 })} %</span> : null}
206
+ </span>
207
+ </TableCell>
208
+ <TableCell className="font-mono text-sm text-ink-2">{e.region}</TableCell>
209
+ <TableCell className="tabular text-right text-ink">{fmt(e.amount)}</TableCell>
210
+ <TableCell className="text-ink-2">{e.unit}</TableCell>
211
+ <TableCell className="tabular text-right text-ink-2">{e.reference ?? <TableNotApplicable />}</TableCell>
212
+ <TableCell className="text-ink-2">{e.note ?? <TableNotApplicable />}</TableCell>
213
+ </TableRow>
214
+ );
215
+ })}
216
+ </TableBody>
217
+ );
218
+ })}
219
+ </Table>
220
+ </div>
221
+ );
222
+ }
223
+ ```
224
+
225
+ ### product-library
226
+
227
+ ```tsx
228
+ import {
229
+ BulkActionBar,
230
+ Button,
231
+ Checkbox,
232
+ DropdownMenuItem,
233
+ DropdownMenuSeparator,
234
+ SplitButton,
235
+ Switch,
236
+ Table,
237
+ TableBody,
238
+ TableCell,
239
+ TableCellAction,
240
+ TableFilterRow,
241
+ TableHead,
242
+ TableHeader,
243
+ TablePagination,
244
+ TableRow,
245
+ TableRowNumber,
246
+ Tag,
247
+ toast,
248
+ } from "@twoux/ui";
249
+ import { Copy, Globe, Share2, Trash2, Upload } from "lucide-react";
250
+ import { type CSSProperties, useMemo, useState } from "react";
251
+
252
+ export const meta = {
253
+ title: "Product library",
254
+ description: "A dense table with a selection column, a search field under each header, Published switches with their word, a Share button revealed on hover or focus (also in the ⋮ menu), pagination and an Import data split button.",
255
+ order: 2,
256
+ };
257
+
258
+ type Product = { id: string; name: string; category: string; unit: string; gwp: number; version: string; published: boolean; updated: string };
259
+
260
+ const NAMES = [
261
+ ["NdFeB magnet, sintered", "Magnets", "1 kg"],
262
+ ["Ferrite magnet, ring", "Magnets", "1 kg"],
263
+ ["Copper winding wire", "Metals", "1 kg"],
264
+ ["Aluminium housing, die cast", "Metals", "1 item"],
265
+ ["Steel shaft, machined", "Metals", "1 item"],
266
+ ["Motor controller board", "Electronics", "1 item"],
267
+ ["Hall sensor", "Electronics", "1 item"],
268
+ ["Ball bearing, 6204", "Components", "1 item"],
269
+ ["Epoxy potting compound", "Chemicals", "1 kg"],
270
+ ["Varnish, insulating", "Chemicals", "1 l"],
271
+ ["Cardboard box, double wall", "Packaging", "1 item"],
272
+ ["Polyethylene foam insert", "Packaging", "1 item"],
273
+ ["Hub motor, 250 W", "Assemblies", "1 item"],
274
+ ["Stator assembly", "Assemblies", "1 item"],
275
+ ["Rotor assembly", "Assemblies", "1 item"],
276
+ ["Cable harness, 1.2 m", "Electronics", "1 item"],
277
+ ["Silicon steel lamination", "Metals", "1 kg"],
278
+ ["Nickel plating bath", "Chemicals", "1 l"],
279
+ ["Pallet, softwood", "Packaging", "1 item"],
280
+ ["Shrink wrap film", "Packaging", "1 kg"],
281
+ ["Thermal paste", "Chemicals", "1 kg"],
282
+ ["Encoder disc", "Components", "1 item"],
283
+ ["Brushless fan, 80 mm", "Assemblies", "1 item"],
284
+ ] as const;
285
+
286
+ const PRODUCTS: Product[] = NAMES.map(([name, category, unit], i) => ({
287
+ id: `p${i + 1}`,
288
+ name,
289
+ category,
290
+ unit,
291
+ gwp: Math.round(((i * 7.31) % 38) * 100 + 42) / 100,
292
+ version: `${1 + (i % 3)}.${i % 4}`,
293
+ published: i % 3 !== 1,
294
+ updated: new Date(Date.UTC(2025, 9, 28 - i)).toISOString().slice(0, 10),
295
+ }));
296
+
297
+ const PAGE = 8;
298
+ const date = (v: string) => new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "short" }).format(new Date(v));
299
+ const pin = (px: number) => ({ "--pin": `${px}px` }) as CSSProperties;
300
+
301
+ export default function Example() {
302
+ const [products, setProducts] = useState(PRODUCTS);
303
+ const [filters, setFilters] = useState<Record<string, string>>({});
304
+ const [selected, setSelected] = useState<string[]>([]);
305
+ const [page, setPage] = useState(0);
306
+
307
+ const shown = useMemo(
308
+ () =>
309
+ products.filter((p) =>
310
+ Object.entries(filters).every(([k, q]) => !q.trim() || String(k === "published" ? (p.published ? "Published" : "Draft") : p[k as keyof Product]).toLowerCase().includes(q.trim().toLowerCase())),
311
+ ),
312
+ [products, filters],
313
+ );
314
+ const pageRows = shown.slice(page * PAGE, (page + 1) * PAGE);
315
+ const allOnPage = pageRows.length > 0 && pageRows.every((p) => selected.includes(p.id));
316
+ const someOnPage = pageRows.some((p) => selected.includes(p.id));
317
+ const toggle = (id: string) => setSelected((s) => (s.includes(id) ? s.filter((x) => x !== id) : [...s, id]));
318
+ const share = (p: Product) => toast.success(`Link to ${p.name} copied`, { description: "Anyone in Corvane Magnetics can open it." });
319
+ const publish = (p: Product, on: boolean) => {
320
+ setProducts((ps) => ps.map((x) => (x.id === p.id ? { ...x, published: on } : x)));
321
+ toast(on ? `Published ${p.name}` : `${p.name} is a draft again`, { action: { label: "Undo", onClick: () => setProducts((ps) => ps.map((x) => (x.id === p.id ? { ...x, published: !on } : x))) } });
322
+ };
323
+
324
+ return (
325
+ <div className="w-full overflow-hidden rounded-sm border border-line bg-surface">
326
+ <div className="flex flex-wrap items-center gap-2 border-b border-line px-3 py-2">
327
+ <h3 className="text-md font-semibold text-ink">Product library</h3>
328
+ <span className="tabular text-sm text-ink-3">{products.length} products</span>
329
+ <span className="flex-1" />
330
+ <SplitButton
331
+ variant="outline"
332
+ onClick={() => toast("Choose a CSV or Excel file to import")}
333
+ menuLabel="More import options"
334
+ menu={
335
+ <>
336
+ <DropdownMenuItem onSelect={() => toast("Importing all products from the file")}>Import all</DropdownMenuItem>
337
+ <DropdownMenuItem disabled={!selected.length} onSelect={() => toast(`Updating ${selected.length} selected products from the file`)}>Import selection{selected.length ? ` (${selected.length})` : ""}</DropdownMenuItem>
338
+ </>
339
+ }
340
+ >
341
+ <Upload />Import data
342
+ </SplitButton>
343
+ </div>
344
+ <BulkActionBar size="dense" count={selected.length} noun={selected.length === 1 ? "product" : "products"} onClear={() => setSelected([])} actions={[
345
+ { label: "Publish", icon: <Globe />, onSelect: () => { setProducts((ps) => ps.map((p) => (selected.includes(p.id) ? { ...p, published: true } : p))); toast.success(`Published ${selected.length} products`); setSelected([]); } },
346
+ { label: "Share", icon: <Share2 />, onSelect: () => toast.success(`Links to ${selected.length} products copied`) },
347
+ ]} />
348
+ <Table size="dense" aria-label="Product library" className="min-w-260 table-fixed">
349
+ <TableHeader>
350
+ <TableRow className="hover:bg-transparent">
351
+ <TableHead data-pinned="" className="w-9">
352
+ <Checkbox aria-label="Select all on this page" checked={allOnPage ? true : someOnPage ? "indeterminate" : false} onCheckedChange={(v) => setSelected((s) => (v ? [...new Set([...s, ...pageRows.map((p) => p.id)])] : s.filter((id) => !pageRows.some((p) => p.id === id))))} />
353
+ </TableHead>
354
+ <TableHead data-pinned="" style={pin(36)} className="w-14"><span aria-hidden="true">#</span><span className="sr-only">Row number</span></TableHead>
355
+ <TableHead data-pinned="last" style={pin(92)} className="w-64">Product</TableHead>
356
+ <TableHead className="w-32">Category</TableHead>
357
+ <TableHead className="w-24">Declared unit</TableHead>
358
+ <TableHead className="w-32 text-right">GWP, kg CO₂e</TableHead>
359
+ <TableHead className="w-20">Version</TableHead>
360
+ <TableHead className="w-36">Status</TableHead>
361
+ <TableHead className="w-24">Updated</TableHead>
362
+ </TableRow>
363
+ <TableFilterRow
364
+ columns={[null, null, { id: "name", label: "Product" }, { id: "category", label: "Category" }, { id: "unit", label: "Declared unit" }, { id: "gwp", label: "GWP", align: "right" }, { id: "version", label: "Version" }, { id: "published", label: "Status" }, null]}
365
+ values={filters}
366
+ onChange={(id, v) => { setFilters((f) => ({ ...f, [id]: v })); setPage(0); }}
367
+ />
368
+ </TableHeader>
369
+ <TableBody>
370
+ {pageRows.map((p, i) => {
371
+ const on = selected.includes(p.id);
372
+ return (
373
+ <TableRow key={p.id} data-state={on ? "selected" : undefined}>
374
+ <TableCell data-pinned=""><Checkbox aria-label={`Select ${p.name}`} checked={on} onCheckedChange={() => toggle(p.id)} /></TableCell>
375
+ <TableRowNumber n={page * PAGE + i + 1} label={p.name} data-pinned="" style={pin(36)}>
376
+ <DropdownMenuItem onSelect={() => share(p)}><Share2 />Share</DropdownMenuItem>
377
+ <DropdownMenuItem><Copy />Duplicate</DropdownMenuItem>
378
+ <DropdownMenuSeparator />
379
+ <DropdownMenuItem variant="destructive"><Trash2 />Delete product</DropdownMenuItem>
380
+ </TableRowNumber>
381
+ <TableCell data-pinned="last" style={pin(92)}>
382
+ <span className="flex min-w-0 items-center gap-2">
383
+ <span className="min-w-0 flex-1 truncate font-medium text-ink">{p.name}</span>
384
+ <TableCellAction aria-label={`Share ${p.name}`} onClick={() => share(p)}>Share</TableCellAction>
385
+ </span>
386
+ </TableCell>
387
+ <TableCell><Tag>{p.category}</Tag></TableCell>
388
+ <TableCell className="text-ink-2">{p.unit}</TableCell>
389
+ <TableCell className="tabular text-right text-ink">{p.gwp.toLocaleString("en-GB", { minimumFractionDigits: 2 })}</TableCell>
390
+ <TableCell className="tabular text-ink-2">{p.version}</TableCell>
391
+ <TableCell>
392
+ <span className="flex items-center gap-2">
393
+ <Switch size="sm" checked={p.published} onCheckedChange={(v) => publish(p, v)} aria-label={`Publish ${p.name}`} />
394
+ <span className={p.published ? "text-ink" : "text-ink-3"}>{p.published ? "Published" : "Draft"}</span>
395
+ </span>
396
+ </TableCell>
397
+ <TableCell className="tabular text-ink-2">{date(p.updated)}</TableCell>
398
+ </TableRow>
399
+ );
400
+ })}
401
+ </TableBody>
402
+ </Table>
403
+ {shown.length === 0 && <p className="px-3 py-4 text-base text-ink-2">No products match these filters.</p>}
404
+ <TablePagination page={page} pageCount={Math.ceil(shown.length / PAGE)} pageSize={PAGE} total={shown.length} selected={selected.length} noun="products" onPageChange={setPage} />
405
+ </div>
406
+ );
407
+ }
408
+ ```
409
+
410
+ ### prospects
411
+
412
+ ```tsx
413
+ import { BulkActionBar, Button, DropdownMenuItem, type SheetCellChange, SheetGrid, type SheetRow, type SheetSort, ShortcutList, sheetGridShortcuts, toast } from "@twoux/ui";
414
+ import { Copy, Trash2 } from "lucide-react";
415
+ import { useState } from "react";
416
+ import { PROSPECT_COLUMNS, prospectRows } from "./prospects-data";
417
+
418
+ export const meta = {
419
+ title: "TwoUX prospects: every cell editable",
420
+ description:
421
+ "When every cell is editable, use Sheet grid with size=\"dense\". Stage and Seniority are selectors: click the active cell or its chevron, or press Enter. Hover shows the row, the cell under the pointer and the active cell in three tones. Drag a header edge to resize, double-click it to fit; pin or unpin from the column menu. Add row scrolls to the new row. Every key is listed under the grid.",
422
+ order: 0,
423
+ };
424
+
425
+ export default function Example() {
426
+ const [columns, setColumns] = useState(PROSPECT_COLUMNS);
427
+ const [rows, setRows] = useState<SheetRow[]>(() => prospectRows());
428
+ const [sort, setSort] = useState<SheetSort>(null);
429
+ const [selected, setSelected] = useState<string[]>([]);
430
+ // A paste or fill arrives as one batch, so one state update covers every cell.
431
+ const onCellsChange = (changes: SheetCellChange[]) =>
432
+ setRows((rs) => rs.map((r) => {
433
+ const mine = changes.filter((c) => c.rowId === r.id);
434
+ return mine.length ? { ...r, values: { ...r.values, ...Object.fromEntries(mine.map((c) => [c.columnId, c.value])) } } : r;
435
+ }));
436
+ const remove = (ids: string[]) => {
437
+ const before = rows;
438
+ setRows((rs) => rs.filter((r) => !ids.includes(r.id)));
439
+ setSelected((s) => s.filter((id) => !ids.includes(id)));
440
+ toast(`Deleted ${ids.length} ${ids.length === 1 ? "person" : "people"}`, { action: { label: "Undo", onClick: () => setRows(before) } });
441
+ };
442
+ const duplicate = (ids: string[]) =>
443
+ setRows((rs) => [...rs, ...rs.filter((r) => ids.includes(r.id)).map((r, i) => ({ id: `${r.id}-copy-${rs.length + i}`, values: { ...r.values, name: `${r.values.name} (copy)` } }))]);
444
+ const n = selected.length;
445
+ return (
446
+ <div className="grid w-full gap-6">
447
+ <BulkActionBar
448
+ size="dense"
449
+ count={n}
450
+ noun={n === 1 ? "person" : "people"}
451
+ onClear={() => setSelected([])}
452
+ className="-mb-6 rounded-sm border border-line"
453
+ actions={[
454
+ { label: "Duplicate", icon: <Copy />, onSelect: () => { duplicate(selected); toast.success(`Duplicated ${n} ${n === 1 ? "person" : "people"}`); setSelected([]); } },
455
+ { label: "Delete", icon: <Trash2 />, destructive: true, onSelect: () => remove(selected) },
456
+ ]}
457
+ />
458
+ <SheetGrid
459
+ aria-label="TwoUX prospects"
460
+ size="dense"
461
+ filterRow
462
+ className="h-112 w-full"
463
+ columns={columns}
464
+ onColumnsChange={setColumns}
465
+ rows={rows}
466
+ onCellChange={(id, col, v) => setRows((rs) => rs.map((r) => (r.id === id ? { ...r, values: { ...r.values, [col]: v } } : r)))}
467
+ onCellsChange={onCellsChange}
468
+ sort={sort}
469
+ onSortChange={setSort}
470
+ selectedRowIds={selected}
471
+ onSelectedRowIdsChange={setSelected}
472
+ rowActions={(row) => (
473
+ <>
474
+ <DropdownMenuItem onSelect={() => duplicate([row.id])}><Copy />Duplicate</DropdownMenuItem>
475
+ <DropdownMenuItem variant="destructive" onSelect={() => remove([row.id])}><Trash2 />Delete</DropdownMenuItem>
476
+ </>
477
+ )}
478
+ onAddRows={(n) => setRows((rs) => [...rs, ...Array.from({ length: n }, (_, i) => ({ id: `new-${rs.length + i}`, values: { subscribed: false } }))])}
479
+ />
480
+ {/* The keys work as in Google Sheets; list them under the grid so nobody has to guess. */}
481
+ <ShortcutList groups={sheetGridShortcuts()} />
482
+ </div>
483
+ );
484
+ }
485
+ ```
486
+
487
+ ## Related
488
+
489
+ - List table
490
+ - Sheet grid
491
+ - Bulk action bar
492
+ - Pagination and counts
493
+ - Split button
494
+ - Tag
495
+ - Switch
@@ -0,0 +1,53 @@
1
+ # Design tokens
2
+
3
+ Tokens are the only way a visual value enters TwoUX. They are named for their job, defined once in :root, and consumed by components. Raw hex, px and ms values in product code are bugs.
4
+
5
+ Kind: foundation · Group: Visual foundation
6
+
7
+ ## Principles
8
+
9
+ - A token is a decision, not a value: a name and value pair that represents a small, repeatable design decision.
10
+ - Choose by meaning. Pick --ink-2 because it is secondary text, not because it is the grey you wanted; never use a token just because the value matches.
11
+ - Components before tokens. Check for an existing component before building a custom one with tokens.
12
+ - Never raw values. Use tokens instead of hard-coded values.
13
+
14
+ ## Guidelines
15
+
16
+ - Naming: short role names with a family prefix. Colour roles are words (--bg, --surface, --ink, --accent, --ok); size families are prefixed (--t-* type, --r-* radius, --h-* height, --c-* categorical, --q-* ordinal). Numbers or size words step along a scale, and suffixes add a variant (-2, -soft, -bg, -line, -hover).
17
+ - Layers: role tokens in :root (the public API) are consumed by components; components may declare private local tokens such as --pk-x for the side panel's padding. Product code reads role tokens only through components.
18
+ - Today the role tokens hold raw values directly. A raw palette layer underneath is only needed when a second theme arrives; until then one layer keeps the system easy to read.
19
+ - Never change the value of an existing token to fix one screen. Add a role or fix the component.
20
+ - Enforce with lint: Stylelint for CSS and an ESLint rule for inline styles that reject hex, rgb, raw px font sizes and raw radii.
21
+
22
+ ## Accessibility
23
+
24
+ - Contrast is checked at the token pairing, so a correctly used token pair passes 1.4.3 and 1.4.11 without re-checking each screen.
25
+ - The focus ring is a token (--ring) so every focusable element gets the same visible indicator.
26
+
27
+ ## Do
28
+
29
+ - Write color: var(--ink-2), not color: #474756.
30
+ - Propose a new token in review when no existing role fits.
31
+
32
+ ## Don't
33
+
34
+ - Do not use a text token for a border because the colour is right.
35
+ - Do not override token values inside a component or page.
36
+ - Do not use --c* or --q* outside charts.
37
+
38
+ ## Tokens
39
+
40
+ | Token | Value | Use |
41
+ |---|---|---|
42
+ | `--ink-2` | `#474756` | Example colour role |
43
+ | `--t-base` | `13px` | Example type step |
44
+ | `--r-md` | `6px` | Example radius role |
45
+ | `--h-sm` | `28px` | Example control height |
46
+ | `--c1` | `#5753e0` | Example data token |
47
+
48
+ ## Related
49
+
50
+ - Consistency
51
+ - Colours
52
+ - Typography
53
+ - Spacing
@@ -0,0 +1,75 @@
1
+ # Disabled and read-only states
2
+
3
+ Show an action as unavailable only when the person can do something about it, say why in one sentence, and show values they can't change as read-only rather than greyed out.
4
+
5
+ Kind: pattern · Group: Patterns: general
6
+
7
+ **Problem:** A greyed-out button with no explanation leaves people guessing whether it is broken, waiting for something, or forbidden. Greyed-out fields are hard to read, and a disabled Save button hides which field is the problem.
8
+
9
+ **Solution:** Pick one of three treatments. Hide what this person can never use. Disable, with a one-sentence reason, what they could use once something changes. Make read-only what they need to see but not change. Submit buttons are never disabled.
10
+
11
+ **Built from:** Button, Dropdown menu, Tooltip, Bulk action bar, Text input, Radio, Banner (inline alert)
12
+
13
+ ## How it works
14
+
15
+ 1. Ask whether this person could ever use the control. If their role never can, such as a Viewer and Billing settings, hide it.
16
+ 2. If they could once something changes (select rows, publish the listing, ask the owner), show it as unavailable and give the reason.
17
+ 3. Put a short reason in a tooltip on the button, menu item, tab or option. Put a longer reason, or one with a next step, in text or a banner beside the control.
18
+ 4. For values that are shown but not editable, such as a listing's ID or a field locked by the job board feed, use the read-only state: normal text colour, no input border, still selectable and copyable.
19
+ 5. On a review step where nothing is editable, show plain text values instead of read-only inputs.
20
+
21
+ ## When to use
22
+
23
+ - Disabled: Close listing in the bulk bar while one of the selected listings is still a draft.
24
+ - Disabled: Publish on a listing whose owner is someone else and the person is an Editor without publish rights.
25
+ - Read-only: the salary band on a listing approved by finance, shown in the edit form so people know what will be published.
26
+
27
+ ## When not to use
28
+
29
+ - On a form's submit button. Keep it enabled and let pressing it show what is missing (see Inline editing and validation).
30
+ - For actions this person's role can never have. Hide them; a wall of disabled items is noise.
31
+ - To stop a reversible action that is merely risky. Let it run and offer Undo.
32
+
33
+ ## Guidelines
34
+
35
+ - Use aria-disabled, not the HTML disabled attribute, on any control that carries a reason, so it stays focusable and keyboard users can reach the reason.
36
+ - A disabled control keeps its place and size; it does not jump or vanish when the state changes.
37
+ - Reasons are plain text only. No links or buttons inside a tooltip reason; if the next step needs a link, use a banner or helper text.
38
+ - Only give a reason when the label and context don't already make it obvious. An unavailable Delete with nothing selected needs none.
39
+ - Read-only fields are still part of the form's data and its tab order, so screen reader users hear the value.
40
+ - Unavailable is never shown by colour alone: the control also loses its hover state and announces as unavailable.
41
+
42
+ ## Content
43
+
44
+ - Permission reason: "Only the listing owner can publish. Ask Priya Shah to publish it."
45
+ - Waiting on something: "Available when the listing is Live." or "Available when you select a listing."
46
+ - System limit: "Unavailable while the job board feed is syncing." Say what is happening, not "Error".
47
+ - One sentence for the reason, an optional second for the next step. No "please", no exclamation marks.
48
+
49
+ ## Accessibility
50
+
51
+ - Unavailable controls use aria-disabled="true", stay in the tab order, and ignore activation; the tooltip reason is linked with aria-describedby.
52
+ - Disabled text is exempt from contrast minimums, but TwoUX keeps it at 3:1 so it can still be read.
53
+ - Read-only inputs use the readonly attribute (or aria-readonly in a grid), never aria-disabled.
54
+ - Tooltip reasons appear on focus as well as hover and can be dismissed with Esc.
55
+
56
+ ## Do
57
+
58
+ - Do say what makes the action available.
59
+ - Do hide controls a role can never use.
60
+ - Do show locked values as readable text, not faded inputs.
61
+
62
+ ## Don't
63
+
64
+ - Don't disable a form's submit button to signal missing fields.
65
+ - Don't put links or buttons inside a disabled reason tooltip.
66
+ - Don't use the native disabled attribute on a control that has a reason; keyboard users can't reach the tooltip.
67
+
68
+ ## Related
69
+
70
+ - Inline editing and validation
71
+ - Page actions and row actions
72
+ - Selection and bulk actions
73
+ - Tooltip
74
+ - Button
75
+ - Form validation