@twoux/ui 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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/README.md +211 -2
  5. package/SHADCN-DIFF.md +228 -0
  6. package/agent/AGENTS.md +358 -0
  7. package/agent/docs-md/accessibility.md +49 -0
  8. package/agent/docs-md/action-catalogue.md +81 -0
  9. package/agent/docs-md/actions.md +74 -0
  10. package/agent/docs-md/agent-activity-log.md +70 -0
  11. package/agent/docs-md/agent-permissions.md +82 -0
  12. package/agent/docs-md/agent-run.md +90 -0
  13. package/agent/docs-md/agents-on-behalf.md +74 -0
  14. package/agent/docs-md/ai-column.md +85 -0
  15. package/agent/docs-md/ai-origin-label.md +70 -0
  16. package/agent/docs-md/announcing-features.md +73 -0
  17. package/agent/docs-md/answer-actions.md +81 -0
  18. package/agent/docs-md/app-shell.md +114 -0
  19. package/agent/docs-md/artifact-card.md +89 -0
  20. package/agent/docs-md/ask-on-screen.md +72 -0
  21. package/agent/docs-md/assistant-page.md +100 -0
  22. package/agent/docs-md/assistant-panel.md +142 -0
  23. package/agent/docs-md/avatar.md +82 -0
  24. package/agent/docs-md/banner.md +106 -0
  25. package/agent/docs-md/board.md +163 -0
  26. package/agent/docs-md/breadcrumb.md +89 -0
  27. package/agent/docs-md/breakdown-table.md +97 -0
  28. package/agent/docs-md/bulk-action-bar.md +112 -0
  29. package/agent/docs-md/button.md +293 -0
  30. package/agent/docs-md/card.md +115 -0
  31. package/agent/docs-md/change-list.md +99 -0
  32. package/agent/docs-md/change-request.md +166 -0
  33. package/agent/docs-md/chart-area.md +87 -0
  34. package/agent/docs-md/chart-bar.md +100 -0
  35. package/agent/docs-md/chart-box-plot.md +88 -0
  36. package/agent/docs-md/chart-bullet.md +89 -0
  37. package/agent/docs-md/chart-candlestick.md +112 -0
  38. package/agent/docs-md/chart-card-line.md +120 -0
  39. package/agent/docs-md/chart-donut.md +90 -0
  40. package/agent/docs-md/chart-drill-down.md +72 -0
  41. package/agent/docs-md/chart-funnel.md +71 -0
  42. package/agent/docs-md/chart-heatmap.md +76 -0
  43. package/agent/docs-md/chart-histogram.md +81 -0
  44. package/agent/docs-md/chart-map.md +165 -0
  45. package/agent/docs-md/chart-mixed.md +88 -0
  46. package/agent/docs-md/chart-price.md +92 -0
  47. package/agent/docs-md/chart-radial-bars.md +109 -0
  48. package/agent/docs-md/chart-radial-lines.md +111 -0
  49. package/agent/docs-md/chart-sankey.md +104 -0
  50. package/agent/docs-md/chart-scatter.md +87 -0
  51. package/agent/docs-md/chart-share-bar.md +89 -0
  52. package/agent/docs-md/chart-treemap.md +98 -0
  53. package/agent/docs-md/chart-waterfall.md +93 -0
  54. package/agent/docs-md/checkbox.md +131 -0
  55. package/agent/docs-md/choosing-selection-control.md +71 -0
  56. package/agent/docs-md/citations-sources.md +79 -0
  57. package/agent/docs-md/citing-sources.md +63 -0
  58. package/agent/docs-md/clarify-before-acting.md +69 -0
  59. package/agent/docs-md/clarifying-question.md +92 -0
  60. package/agent/docs-md/code-block.md +174 -0
  61. package/agent/docs-md/colours.md +87 -0
  62. package/agent/docs-md/columns-menu.md +140 -0
  63. package/agent/docs-md/combobox.md +161 -0
  64. package/agent/docs-md/command-palette.md +126 -0
  65. package/agent/docs-md/confidence-note.md +87 -0
  66. package/agent/docs-md/configurable-dashboard.md +76 -0
  67. package/agent/docs-md/confirmation-dialog.md +93 -0
  68. package/agent/docs-md/connected-tools.md +80 -0
  69. package/agent/docs-md/connector-picker.md +127 -0
  70. package/agent/docs-md/consistency.md +53 -0
  71. package/agent/docs-md/content-density.md +61 -0
  72. package/agent/docs-md/conversation-history.md +78 -0
  73. package/agent/docs-md/create-record.md +80 -0
  74. package/agent/docs-md/csv-import.md +88 -0
  75. package/agent/docs-md/dashboard-items.md +78 -0
  76. package/agent/docs-md/data-export.md +61 -0
  77. package/agent/docs-md/data-grid.md +137 -0
  78. package/agent/docs-md/data-vis-colours.md +76 -0
  79. package/agent/docs-md/data-visualisation.md +76 -0
  80. package/agent/docs-md/date-picker.md +178 -0
  81. package/agent/docs-md/delete-record.md +75 -0
  82. package/agent/docs-md/dense-table.md +495 -0
  83. package/agent/docs-md/design-tokens.md +53 -0
  84. package/agent/docs-md/disabled-read-only.md +75 -0
  85. package/agent/docs-md/drag-and-drop.md +74 -0
  86. package/agent/docs-md/dropdown-menu.md +137 -0
  87. package/agent/docs-md/edit-record.md +73 -0
  88. package/agent/docs-md/empty-loading-error.md +78 -0
  89. package/agent/docs-md/empty-state.md +128 -0
  90. package/agent/docs-md/error-messages.md +75 -0
  91. package/agent/docs-md/error-state.md +123 -0
  92. package/agent/docs-md/export-menu.md +86 -0
  93. package/agent/docs-md/feedback-prompt.md +88 -0
  94. package/agent/docs-md/field-mapping.md +78 -0
  95. package/agent/docs-md/file-drop.md +110 -0
  96. package/agent/docs-md/filter-bar.md +138 -0
  97. package/agent/docs-md/filter-panel.md +103 -0
  98. package/agent/docs-md/filtering-search.md +79 -0
  99. package/agent/docs-md/follow-ups-starters.md +79 -0
  100. package/agent/docs-md/form-validation.md +95 -0
  101. package/agent/docs-md/generated-columns.md +65 -0
  102. package/agent/docs-md/generated-drafts-files.md +71 -0
  103. package/agent/docs-md/grouped-list.md +89 -0
  104. package/agent/docs-md/guide-cue.md +170 -0
  105. package/agent/docs-md/help-panel.md +103 -0
  106. package/agent/docs-md/help-system.md +74 -0
  107. package/agent/docs-md/iconography.md +49 -0
  108. package/agent/docs-md/inbox.md +108 -0
  109. package/agent/docs-md/inline-editing-validation.md +80 -0
  110. package/agent/docs-md/inline-input-in-answer.md +70 -0
  111. package/agent/docs-md/items-palette.md +116 -0
  112. package/agent/docs-md/keyboard-model.md +47 -0
  113. package/agent/docs-md/keyboard-navigation.md +68 -0
  114. package/agent/docs-md/keyboard-shortcut-hint.md +87 -0
  115. package/agent/docs-md/keyboard-shortcut-sheet.md +90 -0
  116. package/agent/docs-md/labelling-ai-output.md +66 -0
  117. package/agent/docs-md/layout.md +47 -0
  118. package/agent/docs-md/list-table.md +254 -0
  119. package/agent/docs-md/loading-skeleton.md +91 -0
  120. package/agent/docs-md/managing-agents.md +71 -0
  121. package/agent/docs-md/message-composer.md +105 -0
  122. package/agent/docs-md/migrating.md +49 -0
  123. package/agent/docs-md/motion.md +93 -0
  124. package/agent/docs-md/navigation.md +76 -0
  125. package/agent/docs-md/notification-channels.md +65 -0
  126. package/agent/docs-md/notifications-decision.md +100 -0
  127. package/agent/docs-md/onboarding-step.md +114 -0
  128. package/agent/docs-md/order-book.md +108 -0
  129. package/agent/docs-md/page-header.md +96 -0
  130. package/agent/docs-md/pagination.md +88 -0
  131. package/agent/docs-md/pane-layout.md +105 -0
  132. package/agent/docs-md/period-comparison.md +66 -0
  133. package/agent/docs-md/plan-card.md +120 -0
  134. package/agent/docs-md/popover-rules.md +88 -0
  135. package/agent/docs-md/prompt-editor.md +90 -0
  136. package/agent/docs-md/propose-apply.md +65 -0
  137. package/agent/docs-md/provider-logo.md +97 -0
  138. package/agent/docs-md/radio.md +83 -0
  139. package/agent/docs-md/ranked-list.md +102 -0
  140. package/agent/docs-md/reasoning-progress.md +72 -0
  141. package/agent/docs-md/record-detail.md +71 -0
  142. package/agent/docs-md/regenerating-answers.md +70 -0
  143. package/agent/docs-md/run-status-bar.md +94 -0
  144. package/agent/docs-md/saved-tasks-schedules.md +63 -0
  145. package/agent/docs-md/saved-views.md +149 -0
  146. package/agent/docs-md/saving-views.md +68 -0
  147. package/agent/docs-md/scheduler.md +258 -0
  148. package/agent/docs-md/secondary-panels.md +72 -0
  149. package/agent/docs-md/segmented-control.md +88 -0
  150. package/agent/docs-md/selection-bulk-actions.md +70 -0
  151. package/agent/docs-md/separator.md +75 -0
  152. package/agent/docs-md/setup-checklist.md +92 -0
  153. package/agent/docs-md/shape-elevation.md +55 -0
  154. package/agent/docs-md/sheet-grid.md +211 -0
  155. package/agent/docs-md/side-panel.md +134 -0
  156. package/agent/docs-md/sign-in.md +114 -0
  157. package/agent/docs-md/spacing.md +55 -0
  158. package/agent/docs-md/split-button.md +100 -0
  159. package/agent/docs-md/stat-compare.md +82 -0
  160. package/agent/docs-md/static-dashboard.md +75 -0
  161. package/agent/docs-md/status-badge.md +140 -0
  162. package/agent/docs-md/step-card.md +75 -0
  163. package/agent/docs-md/stepper.md +92 -0
  164. package/agent/docs-md/suggested-changes.md +91 -0
  165. package/agent/docs-md/switch.md +78 -0
  166. package/agent/docs-md/table-settings.md +81 -0
  167. package/agent/docs-md/tabs.md +85 -0
  168. package/agent/docs-md/tag-input.md +108 -0
  169. package/agent/docs-md/tag.md +79 -0
  170. package/agent/docs-md/task-builder.md +87 -0
  171. package/agent/docs-md/team-instructions.md +72 -0
  172. package/agent/docs-md/templates-gallery.md +85 -0
  173. package/agent/docs-md/text-input.md +160 -0
  174. package/agent/docs-md/thinking-steps.md +89 -0
  175. package/agent/docs-md/timeline.md +189 -0
  176. package/agent/docs-md/timestamp.md +84 -0
  177. package/agent/docs-md/timestamps.md +70 -0
  178. package/agent/docs-md/toast.md +117 -0
  179. package/agent/docs-md/tooltip.md +90 -0
  180. package/agent/docs-md/typography.md +62 -0
  181. package/agent/docs-md/undo-destructive.md +66 -0
  182. package/agent/docs-md/unsaved-changes.md +71 -0
  183. package/agent/docs-md/usage-meter.md +80 -0
  184. package/agent/docs-md/user-feedback.md +71 -0
  185. package/agent/docs-md/voice-and-tone.md +60 -0
  186. package/agent/docs-md/why-choice.md +93 -0
  187. package/agent/docs-md/workspace-onboarding.md +74 -0
  188. package/agent/docs-md/writing.md +48 -0
  189. package/agent/llms-full.txt +18369 -0
  190. package/agent/llms.txt +198 -0
  191. package/agent/skills/twoux-audit/SKILL.md +49 -0
  192. package/agent/skills/twoux-build/SKILL.md +39 -0
  193. package/agent/skills/twoux-design/SKILL.md +54 -0
  194. package/agent/skills/twoux-write/SKILL.md +32 -0
  195. package/bin/check.mjs +123 -0
  196. package/bin/docs.mjs +24 -0
  197. package/bin/guard.mjs +27 -0
  198. package/bin/init.mjs +113 -0
  199. package/bin/knowledge.mjs +87 -0
  200. package/bin/mcp.mjs +116 -0
  201. package/bin/theme.mjs +168 -0
  202. package/bin/twoux.mjs +25 -0
  203. package/package.json +80 -3
  204. package/registry/jobs.json +1028 -0
  205. package/registry/palette.json +770 -0
  206. package/registry/rules.json +913 -0
  207. package/src/components/agent-review.tsx +364 -0
  208. package/src/components/ai-basics.tsx +212 -0
  209. package/src/components/ai-work.tsx +304 -0
  210. package/src/components/ai.tsx +227 -0
  211. package/src/components/alert-dialog.tsx +203 -0
  212. package/src/components/alert.tsx +86 -0
  213. package/src/components/avatar.tsx +114 -0
  214. package/src/components/badge.tsx +59 -0
  215. package/src/components/board.tsx +397 -0
  216. package/src/components/breadcrumb.tsx +114 -0
  217. package/src/components/button-group.tsx +90 -0
  218. package/src/components/button.tsx +109 -0
  219. package/src/components/calendar.tsx +247 -0
  220. package/src/components/card.tsx +102 -0
  221. package/src/components/chart-map.tsx +336 -0
  222. package/src/components/charts-more.tsx +1331 -0
  223. package/src/components/charts-radial.tsx +328 -0
  224. package/src/components/charts.tsx +390 -0
  225. package/src/components/checkbox.tsx +40 -0
  226. package/src/components/code.tsx +139 -0
  227. package/src/components/collapsible.tsx +37 -0
  228. package/src/components/combobox.tsx +339 -0
  229. package/src/components/command-extras.tsx +11 -0
  230. package/src/components/command.tsx +196 -0
  231. package/src/components/data-grid.tsx +213 -0
  232. package/src/components/data-table.tsx +334 -0
  233. package/src/components/data-views.tsx +740 -0
  234. package/src/components/date-picker.tsx +686 -0
  235. package/src/components/dense-table.tsx +207 -0
  236. package/src/components/dialog.tsx +162 -0
  237. package/src/components/dropdown-menu.tsx +264 -0
  238. package/src/components/empty.tsx +114 -0
  239. package/src/components/feedback-prompt.tsx +80 -0
  240. package/src/components/field.tsx +254 -0
  241. package/src/components/fields.tsx +112 -0
  242. package/src/components/flow.tsx +840 -0
  243. package/src/components/guide-cue.tsx +273 -0
  244. package/src/components/inbox.tsx +113 -0
  245. package/src/components/input-group.tsx +176 -0
  246. package/src/components/input.tsx +26 -0
  247. package/src/components/kbd.tsx +34 -0
  248. package/src/components/label.tsx +27 -0
  249. package/src/components/labels.tsx +176 -0
  250. package/src/components/motion.tsx +136 -0
  251. package/src/components/onboarding.tsx +454 -0
  252. package/src/components/order-book.tsx +185 -0
  253. package/src/components/pane-layout.tsx +417 -0
  254. package/src/components/popover.tsx +96 -0
  255. package/src/components/provider-logos.data.ts +49 -0
  256. package/src/components/radio-group.tsx +52 -0
  257. package/src/components/scheduler.tsx +997 -0
  258. package/src/components/segmented-control.tsx +28 -0
  259. package/src/components/select.tsx +195 -0
  260. package/src/components/separator.tsx +31 -0
  261. package/src/components/sheet.tsx +148 -0
  262. package/src/components/shell.tsx +426 -0
  263. package/src/components/shortcut-sheet.tsx +83 -0
  264. package/src/components/skeleton.tsx +18 -0
  265. package/src/components/sonner.tsx +107 -0
  266. package/src/components/spinner.tsx +13 -0
  267. package/src/components/split-button.tsx +58 -0
  268. package/src/components/stepper.tsx +78 -0
  269. package/src/components/switch.tsx +43 -0
  270. package/src/components/table.tsx +126 -0
  271. package/src/components/tabs.tsx +118 -0
  272. package/src/components/tag-input.tsx +253 -0
  273. package/src/components/textarea.tsx +21 -0
  274. package/src/components/theme.tsx +80 -0
  275. package/src/components/timeline.tsx +408 -0
  276. package/src/components/toggle-group.tsx +87 -0
  277. package/src/components/toggle.tsx +54 -0
  278. package/src/components/tooltip.tsx +65 -0
  279. package/src/components/workspace-actions.tsx +294 -0
  280. package/src/components/workspace-import.tsx +465 -0
  281. package/src/components/workspace.tsx +1884 -0
  282. package/src/index.ts +80 -0
  283. package/src/lib/active-indicator.tsx +53 -0
  284. package/src/lib/chart-kit.tsx +72 -0
  285. package/src/lib/cn.ts +6 -0
  286. package/src/lib/shortcuts.tsx +61 -0
  287. package/src/lib/workspace.tsx +10 -0
  288. package/src/shadcn/alert-dialog.tsx +196 -0
  289. package/src/shadcn/alert.tsx +65 -0
  290. package/src/shadcn/avatar.tsx +108 -0
  291. package/src/shadcn/badge.tsx +47 -0
  292. package/src/shadcn/breadcrumb.tsx +108 -0
  293. package/src/shadcn/button-group.tsx +83 -0
  294. package/src/shadcn/button.tsx +63 -0
  295. package/src/shadcn/calendar.tsx +220 -0
  296. package/src/shadcn/card.tsx +91 -0
  297. package/src/shadcn/checkbox.tsx +31 -0
  298. package/src/shadcn/collapsible.tsx +33 -0
  299. package/src/shadcn/command.tsx +184 -0
  300. package/src/shadcn/dialog.tsx +158 -0
  301. package/src/shadcn/dropdown-menu.tsx +256 -0
  302. package/src/shadcn/empty.tsx +103 -0
  303. package/src/shadcn/field.tsx +248 -0
  304. package/src/shadcn/input-group.tsx +170 -0
  305. package/src/shadcn/input.tsx +20 -0
  306. package/src/shadcn/kbd.tsx +28 -0
  307. package/src/shadcn/label.tsx +23 -0
  308. package/src/shadcn/popover.tsx +88 -0
  309. package/src/shadcn/radio-group.tsx +44 -0
  310. package/src/shadcn/select.tsx +189 -0
  311. package/src/shadcn/separator.tsx +27 -0
  312. package/src/shadcn/sheet.tsx +142 -0
  313. package/src/shadcn/skeleton.tsx +13 -0
  314. package/src/shadcn/sonner.tsx +40 -0
  315. package/src/shadcn/switch.tsx +34 -0
  316. package/src/shadcn/table.tsx +115 -0
  317. package/src/shadcn/tabs.tsx +90 -0
  318. package/src/shadcn/textarea.tsx +17 -0
  319. package/src/shadcn/toggle-group.tsx +83 -0
  320. package/src/shadcn/toggle.tsx +46 -0
  321. package/src/shadcn/tooltip.tsx +56 -0
  322. package/src/styles/palette.css +127 -0
  323. package/src/styles/theme.css +35 -0
  324. package/src/styles/twoux.css +236 -0
  325. package/theme.json +14 -0
@@ -0,0 +1,82 @@
1
+ # Avatar and avatar stack
2
+
3
+ Shows who owns or is linked to a record, as a photo or initials, and several people as an overlapping stack with +N.
4
+
5
+ Kind: component · Group: Actions and inputs · Status: built, import from @twoux/ui
6
+
7
+ ## When to use
8
+
9
+ - To show the owner of a listing or the recruiter on a candidate, next to their name or in a table cell.
10
+ - A stack when several people relate to one record and space is short.
11
+
12
+ ## When not to use
13
+
14
+ - When each person must be clearly identified. List names in full.
15
+ - For a single person in a stack. Use one avatar.
16
+ - For teams or categories. Use Tag.
17
+
18
+ ## Structure
19
+
20
+ 1. Circle (22px)
21
+ 2. Photo or initials
22
+ 3. Stack: overlapping avatars with 2px white ring
23
+ 4. +N overflow count
24
+
25
+ ## Guidelines
26
+
27
+ - 22px circle, 10px semibold white initials on a colour assigned per person; photo when one exists.
28
+ - Stack shows up to three, overlapping by 6px with a 2px white ring, then +N in small grey text.
29
+ - +N reveals the hidden names on hover or focus.
30
+ - Each person keeps the same colour everywhere.
31
+
32
+ ## Content
33
+
34
+ - Initials stand in when there is no photo, and are hidden from screen readers when the name is shown.
35
+ - The +N count says how many people are hidden, not the total.
36
+ - Tooltips on avatars show the full name, matching the alt text.
37
+
38
+ ## Accessibility
39
+
40
+ - Standalone avatars have alt text with the person's name; next to a visible name they are decorative (empty alt).
41
+ - A stack has a label (for example Candidates) and screen reader text for +N: 3 more candidates.
42
+ - White initials need 4.5:1 on the assigned colour; pick colours that pass.
43
+ - If the avatar opens a tooltip or popover it must be focusable.
44
+ - Interactive avatars are 22px, so keep 24px spacing from other targets.
45
+
46
+ ## Keyboard
47
+
48
+ | Keys | Action |
49
+ |---|---|
50
+ | Tab to +N | Focus the overflow count and show hidden names |
51
+
52
+ ## Examples
53
+
54
+ ### people
55
+
56
+ ```tsx
57
+ import { AvatarStack, PersonAvatar } from "@twoux/ui";
58
+
59
+ export const meta = { title: "Owners and interviewers", description: "Initials on a colour fixed per person (white text at 5.8:1 or more). A stack shows three, then +N, which names the rest on hover or focus.", order: 1 };
60
+
61
+ export default function Example() {
62
+ return (
63
+ <div className="grid gap-4">
64
+ <div className="flex items-center gap-3">
65
+ <PersonAvatar name="Priya Shah" size="lg" /><PersonAvatar name="Tom Becker" /><PersonAvatar name="Amara Okafor" size="sm" />
66
+ <span className="text-base text-ink-2">Priya Shah owns this listing</span>
67
+ </div>
68
+ <div className="flex items-center gap-3">
69
+ <AvatarStack label="Interviewers" people={[{ name: "Priya Shah" }, { name: "Tom Becker" }, { name: "Amara Okafor" }, { name: "Liam Walsh" }, { name: "Sofia Romano" }]} />
70
+ <span className="text-base text-ink-2">5 interviewers</span>
71
+ </div>
72
+ </div>
73
+ );
74
+ }
75
+ ```
76
+
77
+ ## Related
78
+
79
+ - Tag
80
+ - Tooltip
81
+ - Status badge
82
+ - Provider logo
@@ -0,0 +1,106 @@
1
+ # Banner (inline alert)
2
+
3
+ A persistent message inside the page about a condition that affects what the person is looking at, with at most one action.
4
+
5
+ Kind: component · Group: Feedback and overlays · Status: built, import from @twoux/ui
6
+
7
+ ## When to use
8
+
9
+ - A condition on the current page that stays true until it is resolved, such as "Two listings expire this week."
10
+ - Something blocks a task until fixed, such as form errors before Publish.
11
+ - Data-loss or connection problems that affect the whole page. These may sit above the page header.
12
+
13
+ ## When not to use
14
+
15
+ - Confirming an action the person just took. Use a Toast.
16
+ - Background events that need someone to act later. Use the Inbox.
17
+ - A whole region failed to load. Use the Error state in place of that region.
18
+ - Feature announcements or tips. Leave them out, or put them in the setup checklist in the sidebar.
19
+
20
+ ## Structure
21
+
22
+ 1. Icon (info, warning or error)
23
+ 2. Message: one sentence
24
+ 3. Dismiss (ghost), only when nothing is blocked
25
+ 4. One action (secondary button), such as Review
26
+ 5. Form variant: error strip with count and "Go to first" link
27
+
28
+ ## Guidelines
29
+
30
+ - Place the banner in context: above the table it refers to, or above the submit button for form problems.
31
+ - Show one banner per region. If several apply, order them from most to least severe.
32
+ - Offer Dismiss only when nothing is blocked and no action is required. Condition banners clear themselves when the condition ends.
33
+ - At most one action, as a secondary button on the right. Dismiss sits before it as a ghost button.
34
+ - Use neutral styling for information and keep red for errors that stop the task.
35
+ - The form error strip counts the problems and links to the first one: "2 fields need fixing before you can publish. Go to first".
36
+
37
+ ## Content
38
+
39
+ - Say what is true and what to do: the condition, then a verb on the button ("Review").
40
+ - One or two short sentences, sentence case, no title when one sentence is enough.
41
+ - For errors, say what went wrong and how to recover, without blaming the person.
42
+ - No apologies, no "please", no exclamation marks.
43
+
44
+ ## Accessibility
45
+
46
+ - Information banners that appear after load sit in a polite live region; the form error strip uses role="alert" because it reports a blocking problem (4.1.3).
47
+ - The icon is never the only cue: the sentence states severity in words, and the icon has an accessible label (1.4.1).
48
+ - Errors name the field and describe the problem in text, and "Go to first" moves focus to it (3.3.1).
49
+ - A banner must not be sticky over content in a way that hides the focused element (2.4.11).
50
+
51
+ ## Keyboard
52
+
53
+ | Keys | Action |
54
+ |---|---|
55
+ | Tab | Reaches Dismiss, then the action |
56
+ | Enter | Activates the focused button or the "Go to first" link |
57
+
58
+ ## Examples
59
+
60
+ ### states
61
+
62
+ ```tsx
63
+ import { Alert, AlertAction, AlertDescription, AlertTitle, Button } from "@twoux/ui";
64
+ import { CircleAlert, Info, TriangleAlert } from "lucide-react";
65
+ import { useState } from "react";
66
+
67
+ export const meta = { title: "Information, warning and error", description: "Information is neutral and can be dismissed. A warning names one next step. An error that blocks publishing counts the problems and links to the first.", order: 1 };
68
+
69
+ export default function Example() {
70
+ const [showInfo, setShowInfo] = useState(true);
71
+ return (
72
+ <div className="grid w-full max-w-xl gap-3">
73
+ {showInfo && (
74
+ <Alert>
75
+ <Info />
76
+ <AlertTitle>Salary bands were updated on 3 November.</AlertTitle>
77
+ <AlertAction>
78
+ <Button variant="ghost" size="sm" onClick={() => setShowInfo(false)}>Dismiss</Button>
79
+ </AlertAction>
80
+ </Alert>
81
+ )}
82
+ <Alert variant="warning">
83
+ <TriangleAlert />
84
+ <AlertTitle>4 listings close this week with no applicants.</AlertTitle>
85
+ <AlertAction>
86
+ <Button variant="outline" size="sm">Review</Button>
87
+ </AlertAction>
88
+ </Alert>
89
+ <Alert variant="destructive">
90
+ <CircleAlert />
91
+ <AlertTitle>2 fields need fixing before you can publish.</AlertTitle>
92
+ <AlertDescription>
93
+ <a href="#salary" className="font-medium text-ink underline decoration-bad-line underline-offset-2 hover:decoration-bad">Go to first</a>
94
+ </AlertDescription>
95
+ </Alert>
96
+ </div>
97
+ );
98
+ }
99
+ ```
100
+
101
+ ## Related
102
+
103
+ - Toast
104
+ - Error state (failed to load)
105
+ - Notifications vs inbox vs toast
106
+ - Page header
@@ -0,0 +1,163 @@
1
+ # Board and board item
2
+
3
+ A dashboard grid of self-contained items, such as charts, stats and lists, that people can reorder, resize and remove to build the overview they need.
4
+
5
+ Kind: component · Group: Data visualisation · Status: built, import from @twoux/ui
6
+
7
+ ## When to use
8
+
9
+ - For an overview page people tailor to their own job, such as a hiring lead choosing which pipeline charts to watch.
10
+ - When each item stands alone and can be read without the others.
11
+
12
+ ## When not to use
13
+
14
+ - For a fixed report everyone reads the same way. Lay it out with a plain grid; rearranging adds work without benefit.
15
+ - For a list of records. Use the List table.
16
+
17
+ ## Structure
18
+
19
+ 1. Board: a grid of up to 4 columns that stacks on narrow screens
20
+ 2. Board item: drag handle, title and optional description, settings menu
21
+ 3. Item content (padded by default)
22
+ 4. Item footer (optional), such as a link to the full report
23
+ 5. Empty slot: what shows when no items are on the board
24
+
25
+ ## Guidelines
26
+
27
+ - Each item does one job: one chart, one stat, one short list. Its title names what it shows.
28
+ - Put the most important item first: the first row is what people see before they scroll.
29
+ - Every drag has two alternatives: the keyboard (pick up with Enter or Space, move with arrows, drop with Enter, Esc to cancel) and the item menu (Move earlier, Move later, Wider, Narrower).
30
+ - Announce every pick-up, move, resize, drop and removal to screen readers.
31
+ - Show an empty state with a way to add the first item when the board has none.
32
+ - Save the layout per person, so a change on one screen doesn't move someone else's dashboard.
33
+ - Add items opens the items palette in the side panel. Removed items return to the palette, so Remove and Add undo each other.
34
+ - Offer Reset to default layout in the page's more menu, applied at once with Undo in a toast.
35
+ - Save the layout on the server as each change happens, per person and per column count (4, 2 and 1), and restore it on reload and on other devices.
36
+ - The board has 4 columns on wide screens, 2 on tablet widths and 1 on phones; items span 1 to 4 columns and keep their order when stacking.
37
+ - When the board has no items, its empty state offers Add items and Reset to default layout.
38
+ - While dragging, show a drop preview of the item's size. Items swap where there is room and push others along where there is not; dropping outside the board returns the item to where it was.
39
+ - Moving an item keeps its size and its own settings. Heights snap to fixed row steps; the default height fits the default content.
40
+ - Show every stage of a drag. Ready: the grip darkens on hover and focus, with a grab cursor and a tooltip saying how to move it. Dragging: a lifted copy follows the pointer, the board tints to show where drops are allowed, and a dashed slot shows exactly where the item will land.
41
+ - Other items glide to make room (180ms, off with reduced motion) instead of jumping. On drop, a short highlight settles on the moved item and the new position is announced. Esc cancels and puts it back.
42
+ - Work out the landing slot against where items were when the drag started, so the slot doesn't jump as items move aside.
43
+
44
+ ## Content
45
+
46
+ - Item titles are nouns for what is shown: “Applications this month”, not “Chart 1”.
47
+ - The menu says what will happen to the item: “Move earlier”, “Wider”, “Remove from dashboard”.
48
+
49
+ ## Accessibility
50
+
51
+ - The drag handle is a button named “Move <title>”, with a description of the keys.
52
+ - Dragging is never the only way to move or resize (2.5.7 Dragging Movements).
53
+ - Moves and resizes are announced in a live region (4.1.3 Status Messages).
54
+ - Each item is an article labelled by its title, so screen reader users can jump between items.
55
+ - Every board item region has an accessible name, and the board offers a keyboard way to jump between items.
56
+ - Use a grab cursor on handles and a grabbing cursor while dragging, in addition to the handle icon.
57
+
58
+ ## Keyboard
59
+
60
+ | Keys | Action |
61
+ |---|---|
62
+ | Enter / Space (handle) | Pick up the item, or drop it |
63
+ | Arrow keys | Move the picked-up item one place |
64
+ | Esc | Cancel and put the item back |
65
+ | Tab | Reach the handle, the item's controls and its settings menu |
66
+
67
+ ## Examples
68
+
69
+ ### hiring-overview
70
+
71
+ ```tsx
72
+ import { Board, BoardItem, type BoardItemData, Button, DropdownMenuItem, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, toast } from "@twoux/ui";
73
+ import { LayoutDashboard } from "lucide-react";
74
+ import type React from "react";
75
+ import { useState } from "react";
76
+
77
+ export const meta = { title: "Hiring overview", description: "Drag an item by its handle, or focus the handle and press Enter, then the arrow keys. The ⋯ menu moves and resizes without dragging. Every change is announced.", order: 1 };
78
+
79
+ const DEFAULT: BoardItemData[] = [
80
+ { id: "apps", title: "Applications this month", columnSpan: 1 },
81
+ { id: "open", title: "Open listings", columnSpan: 1 },
82
+ { id: "tth", title: "Median time to hire", columnSpan: 2 },
83
+ { id: "stages", title: "Pipeline by stage", columnSpan: 2 },
84
+ { id: "closing", title: "Closing this week", columnSpan: 2 },
85
+ ];
86
+
87
+ const STAGES = [["Applied", 412], ["Screen", 126], ["Interview", 48], ["Offer", 9]] as const;
88
+
89
+ // Colour says whether a change is good, not which way it went.
90
+ function Stat({ value, change, note, tone = "neutral" }: { value: string; change: string; note: string; tone?: "good" | "neutral" }) {
91
+ return (
92
+ <div className="grid gap-0.5">
93
+ <span className="tabular text-2xl font-semibold text-ink">{value}</span>
94
+ <span className="text-sm text-ink-2"><span className={tone === "good" ? "font-medium text-ok" : "font-medium text-ink"}>{change}</span> {note}</span>
95
+ </div>
96
+ );
97
+ }
98
+
99
+ function content(id: string) {
100
+ if (id === "apps") return <Stat value="595" change="↑ 12%" note="vs October" tone="good" />;
101
+ if (id === "open") return <Stat value="14" change="↓ 2" note="vs last week" />;
102
+ if (id === "tth") return <Stat value="23 days" change="↓ 4 days" note="vs last quarter" tone="good" />;
103
+ if (id === "stages")
104
+ return (
105
+ <ul className="grid gap-1.5">
106
+ {STAGES.map(([s, n]) => (
107
+ <li key={s} className="grid grid-cols-[72px_1fr_40px] items-center gap-2 text-sm">
108
+ <span className="text-ink-2">{s}</span>
109
+ <span className="h-2 overflow-hidden rounded-xs bg-hover" aria-hidden="true">
110
+ <span className="block h-full w-(--w) rounded-xs bg-primary" style={{ "--w": `${(n / 412) * 100}%` } as React.CSSProperties} />
111
+ </span>
112
+ <span className="tabular text-right text-ink">{n}</span>
113
+ </li>
114
+ ))}
115
+ </ul>
116
+ );
117
+ return (
118
+ <ul className="grid gap-1 text-sm">
119
+ {[["Frontend Engineer", "Fri 21 Nov"], ["Customer Success Lead", "Sun 23 Nov"]].map(([r, d]) => (
120
+ <li key={r} className="flex justify-between gap-2"><span className="truncate text-ink">{r}</span><span className="tabular shrink-0 text-ink-3">{d}</span></li>
121
+ ))}
122
+ </ul>
123
+ );
124
+ }
125
+
126
+ export default function Example() {
127
+ const [items, setItems] = useState(DEFAULT);
128
+ return (
129
+ <Board
130
+ className="w-full"
131
+ aria-label="Hiring overview"
132
+ items={items}
133
+ onItemsChange={setItems}
134
+ empty={
135
+ <Empty className="min-h-48">
136
+ <EmptyHeader>
137
+ <EmptyMedia variant="icon"><LayoutDashboard /></EmptyMedia>
138
+ <EmptyTitle>No items on this dashboard</EmptyTitle>
139
+ <EmptyDescription>Add the charts and lists you want to watch.</EmptyDescription>
140
+ </EmptyHeader>
141
+ <EmptyContent><Button variant="outline" size="sm" onClick={() => setItems(DEFAULT)}>Restore default items</Button></EmptyContent>
142
+ </Empty>
143
+ }
144
+ renderItem={(it) => (
145
+ <BoardItem
146
+ id={it.id}
147
+ header={it.title}
148
+ footer={it.id === "stages" ? <a href="#pipeline" className="font-medium text-primary-ink hover:underline">Open pipeline report</a> : undefined}
149
+ settings={<DropdownMenuItem onSelect={() => toast(`Opened ${it.title}`)}>View details</DropdownMenuItem>}
150
+ >
151
+ {content(it.id)}
152
+ </BoardItem>
153
+ )}
154
+ />
155
+ );
156
+ }
157
+ ```
158
+
159
+ ## Related
160
+
161
+ - Line chart card
162
+ - Stat card with comparison
163
+ - Empty state
@@ -0,0 +1,89 @@
1
+ # Breadcrumb
2
+
3
+ A short trail in the top bar showing where the page sits in the hierarchy, ending with the current page.
4
+
5
+ Kind: component · Group: Navigation · Status: built, import from @twoux/ui
6
+
7
+ ## When to use
8
+
9
+ - On pages below the top level, to show the section they belong to ("Hiring > Listings").
10
+ - On a record's full page, so the person can go back up to the list ("Hiring > Listings > Junior Product Designer").
11
+
12
+ ## When not to use
13
+
14
+ - As main navigation. The sidebar is the main navigation; the breadcrumb supplements it.
15
+ - Linking to sibling or related pages. Use tabs or the sidebar.
16
+ - Showing the path the person took. Breadcrumbs show hierarchy, not history; use the browser's Back.
17
+
18
+ ## Structure
19
+
20
+ 1. Section name (link when it has its own page)
21
+ 2. Chevron separator (decorative)
22
+ 3. Current page (not a link, darker text)
23
+
24
+ ## Guidelines
25
+
26
+ - Sits at the left of the top bar on every page; top-level pages (Inbox, Assistant) show their name only.
27
+ - The last item is the current page, styled darker and not a link.
28
+ - Names match page titles exactly.
29
+ - When space runs out, collapse middle items behind an ellipsis button; the current page always stays visible.
30
+ - Keep it on one line; never wrap.
31
+
32
+ ## Content
33
+
34
+ - Use the page and section names exactly as shown in the sidebar.
35
+ - For a record, end with its title; add the ID only when titles can repeat.
36
+
37
+ ## Accessibility
38
+
39
+ - Wrap it in a nav landmark labelled "Breadcrumb" containing an ordered list (1.3.1).
40
+ - The current item has aria-current="page"; when it is not a link this is optional but still set for clarity.
41
+ - Separators are decorative and hidden from assistive technology.
42
+ - Breadcrumbs help meet 2.4.8 Location by showing where the page sits.
43
+
44
+ ## Keyboard
45
+
46
+ | Keys | Action |
47
+ |---|---|
48
+ | Tab | Move through the links in the trail |
49
+ | Enter | Follow the focused link |
50
+
51
+ ## Examples
52
+
53
+ ### trail
54
+
55
+ ```tsx
56
+ import { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@twoux/ui";
57
+
58
+ export const meta = { title: "Where the page sits", description: "Names match page titles. The current page is the last item, darker and not a link. On narrow screens middle items collapse behind the ellipsis; the trail never wraps.", order: 1 };
59
+
60
+ export default function Example() {
61
+ return (
62
+ <div className="grid w-full max-w-xl gap-4">
63
+ <Breadcrumb>
64
+ <BreadcrumbList>
65
+ <BreadcrumbItem><BreadcrumbLink href="#listings">Listings</BreadcrumbLink></BreadcrumbItem>
66
+ <BreadcrumbSeparator />
67
+ <BreadcrumbItem><BreadcrumbLink href="#l1040">Junior Product Designer</BreadcrumbLink></BreadcrumbItem>
68
+ <BreadcrumbSeparator />
69
+ <BreadcrumbItem><BreadcrumbPage>Screening questions</BreadcrumbPage></BreadcrumbItem>
70
+ </BreadcrumbList>
71
+ </Breadcrumb>
72
+ <Breadcrumb>
73
+ <BreadcrumbList>
74
+ <BreadcrumbItem><BreadcrumbLink href="#settings">Settings</BreadcrumbLink></BreadcrumbItem>
75
+ <BreadcrumbSeparator />
76
+ <BreadcrumbItem><BreadcrumbEllipsis /></BreadcrumbItem>
77
+ <BreadcrumbSeparator />
78
+ <BreadcrumbItem><BreadcrumbPage>Email templates</BreadcrumbPage></BreadcrumbItem>
79
+ </BreadcrumbList>
80
+ </Breadcrumb>
81
+ </div>
82
+ );
83
+ }
84
+ ```
85
+
86
+ ## Related
87
+
88
+ - App shell and sidebar navigation
89
+ - Page header
@@ -0,0 +1,97 @@
1
+ # Breakdown table
2
+
3
+ A table under a chart that splits its figure by team, location or owner, with current and previous periods side by side and change in both units.
4
+
5
+ Kind: component · Group: Data visualisation · Status: built, import from @twoux/ui
6
+
7
+ ## When to use
8
+
9
+ - A chart needs its exact values and a split by dimension; the table doubles as the chart's long description.
10
+ - People compare values across rows.
11
+
12
+ ## When not to use
13
+
14
+ - For many columns people edit. Use the TwoUX Data grid.
15
+ - For record lists. Use the TwoUX List table.
16
+
17
+ ## Structure
18
+
19
+ 1. Breakdown switch (By team, By location, By owner)
20
+ 2. Export CSV
21
+ 3. Columns: dimension, This period, Previous period, Change, Change %
22
+
23
+ ## Guidelines
24
+
25
+ - Sit directly under the chart it explains.
26
+ - Show change with sign and arrow, not colour alone.
27
+ - Show a dash when there is nothing to compare, with hidden text for screen readers.
28
+ - Sort by the current period by default and allow sorting by any column.
29
+ - When a chart offers drill-down, the breakdown table is where the same split lives in full; a popover breakdown never holds data that is not also here.
30
+ - Each row's count can link to the filtered List table behind it; link the count or the name, not both.
31
+ - Rows follow the chart's filters and series toggles, so the table and chart never disagree.
32
+
33
+ ## Content
34
+
35
+ - Right-align numbers with consistent decimals.
36
+ - Column headers state the period, "This period", "Previous period".
37
+ - Write change with its sign, "+12" and "−8%", so direction reads without colour.
38
+
39
+ ## Accessibility
40
+
41
+ - 1.3.1 Info and Relationships: real table markup with scoped headers.
42
+ - APG sortable table: aria-sort on the sorted header only.
43
+ - The By team, By location, By owner switch exposes the pressed state.
44
+ - When the chart filters change, announce the new row count politely without moving focus.
45
+
46
+ ## Keyboard
47
+
48
+ | Keys | Action |
49
+ |---|---|
50
+ | Tab | Move to the switch, Export and sortable headers |
51
+ | Enter / Space | Change breakdown or sort |
52
+
53
+ ## Examples
54
+
55
+ ### by-team
56
+
57
+ ```tsx
58
+ import type React from "react";
59
+ import { CartesianChart, ChartFrame, ChartLegend, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, seriesColor } from "@twoux/ui";
60
+ import { APPLICATIONS, WEEKS } from "../chart-card-line/data";
61
+
62
+ export const meta = { title: "Chart with its breakdown", description: "The table under the chart splits the same figure by team, with current, previous and change. The colour key in each row ties it to the chart.", order: 1 };
63
+
64
+ export default function Example() {
65
+ return (
66
+ <ChartFrame className="w-full" title="Applications per week" description="Last 8 weeks" legend={<ChartLegend items={APPLICATIONS.map((s, i) => ({ id: s.id, label: s.label, color: seriesColor(i), shape: "line" }))} />}>
67
+ <CartesianChart kind="line" x={WEEKS} series={APPLICATIONS} height={180} aria-label="Applications per week by team" />
68
+ <div className="overflow-hidden rounded-xs border border-line">
69
+ <Table aria-label="Applications by team">
70
+ <TableHeader><TableRow><TableHead>Team</TableHead><TableHead className="text-right">This week</TableHead><TableHead className="text-right">Last week</TableHead><TableHead className="text-right">Change</TableHead></TableRow></TableHeader>
71
+ <TableBody>
72
+ {APPLICATIONS.map((s, i) => {
73
+ const [now, prev] = [s.data[s.data.length - 1] ?? 0, s.data[s.data.length - 2] ?? 0];
74
+ const pct = Math.round(((now - prev) / (prev || 1)) * 100);
75
+ return (
76
+ <TableRow key={s.id}>
77
+ <TableCell><span className="inline-flex items-center gap-2"><span aria-hidden="true" className="h-0.5 w-3 rounded-full bg-(--c)" style={{ "--c": seriesColor(i) } as React.CSSProperties} />{s.label}</span></TableCell>
78
+ <TableCell className="tabular text-right font-medium">{now}</TableCell>
79
+ <TableCell className="tabular text-right text-ink-2">{prev}</TableCell>
80
+ <TableCell className="tabular text-right">{pct >= 0 ? "↑" : "↓"} {Math.abs(pct)}%</TableCell>
81
+ </TableRow>
82
+ );
83
+ })}
84
+ </TableBody>
85
+ </Table>
86
+ </div>
87
+ </ChartFrame>
88
+ );
89
+ }
90
+ ```
91
+
92
+ ## Related
93
+
94
+ - Line chart card
95
+ - Bar chart
96
+ - Stat card with comparison
97
+ - Export menu
@@ -0,0 +1,112 @@
1
+ # Bulk action bar
2
+
3
+ A grey band directly above the table while rows are selected: the actions that apply to every selected row, the count and Clear selection.
4
+
5
+ Kind: component · Group: Data · Status: built, import from @twoux/ui
6
+
7
+ ## When to use
8
+
9
+ - People act on several records at once; checkboxes plus batch actions is the standard pattern.
10
+ - An action applies to all selected items, such as extending or closing listings.
11
+ - People select rows from the keyboard with X or Shift with J and K, then act without reaching for the mouse.
12
+
13
+ ## When not to use
14
+
15
+ - For one or two actions on a single row. Use inline row actions or the side panel.
16
+ - For actions on the whole table regardless of selection, such as Export. Put them in the table toolbar.
17
+
18
+ ## Structure
19
+
20
+ 1. Band directly above the table, below any filters; the header row stays visible
21
+ 2. Actions with icons and shortcuts, up to four (Ask, Extend 30 days, Duplicate); the rest in More
22
+ 3. Destructive action (Close listings)
23
+ 4. Count ("2 listings selected") and Clear selection (Esc)
24
+
25
+ ## Guidelines
26
+
27
+ - Show the bar only while rows are selected, and always show how many.
28
+ - Place the bar directly above the table, below the filters, so the filters and the column headers stay in view.
29
+ - Show up to four actions as buttons. The rest, and any that don't fit the width, move into a More menu that keeps their shortcuts; never shrink actions to icons only.
30
+ - Exit by clearing the selection or pressing Esc.
31
+ - Disable row-level actions while the bulk bar is showing so the two do not compete.
32
+ - Confirm destructive bulk actions in a dialog that states the count and consequence, and offer Undo when the action can be reversed.
33
+ - Selection on the header checkbox covers the visible page only, and actions affect only selected rows.
34
+
35
+ ## Content
36
+
37
+ - Label actions verb plus noun, "Close listings", and put the count in confirmations, "Close 2 listings?".
38
+ - Show each action's shortcut on the button.
39
+ - Put destructive actions last.
40
+
41
+ ## Accessibility
42
+
43
+ - APG toolbar pattern: role="toolbar" with aria-label "Bulk actions", one tab stop, arrow keys between buttons.
44
+ - 4.1.3 Status Messages: announce "2 listings selected" when the count changes.
45
+ - 2.4.11 Focus Not Obscured: the bar sits in the page flow above the table, so it never covers a focused row.
46
+ - 2.5.8 Target Size: every button is at least 24 by 24 CSS pixels.
47
+
48
+ ## Keyboard
49
+
50
+ | Keys | Action |
51
+ |---|---|
52
+ | ← → | Move between actions |
53
+ | T | Extend 30 days |
54
+ | D | Duplicate |
55
+ | ⇧C | Close listings (asks to confirm) |
56
+ | ⌘J | Ask the assistant about the selection |
57
+ | Esc | Clear the selection |
58
+
59
+ ## Examples
60
+
61
+ ### selection
62
+
63
+ ```tsx
64
+ import { BulkActionBar, Button, Checkbox, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, toast } from "@twoux/ui";
65
+ import { CalendarPlus, Copy } from "lucide-react";
66
+ import { useState } from "react";
67
+ import { LISTINGS } from "../list-table/data";
68
+
69
+ export const meta = { title: "Act on selected listings", description: "While rows are selected, a band appears directly above the table with the actions, then the count and Clear selection. The header row and any filters stay in view. Arrow keys move between its buttons; Esc clears.", order: 1 };
70
+
71
+ const ROWS = LISTINGS.slice(0, 4);
72
+
73
+ export default function Example() {
74
+ const [sel, setSel] = useState<string[]>([ROWS[0].id, ROWS[2].id]);
75
+ const toggle = (id: string) => setSel((s) => (s.includes(id) ? s.filter((x) => x !== id) : [...s, id]));
76
+ const all = sel.length === ROWS.length ? true : sel.length ? "indeterminate" : false;
77
+ const setAll = (v: boolean) => setSel(v ? ROWS.map((r) => r.id) : []);
78
+ const n = sel.length;
79
+ return (
80
+ <div className="w-full max-w-xl overflow-hidden rounded-sm border border-line bg-surface">
81
+ <BulkActionBar count={n} noun={n === 1 ? "listing" : "listings"} onClear={() => setSel([])} actions={[
82
+ { label: "Extend 30 days", icon: <CalendarPlus />, shortcut: "T", onSelect: () => toast.success(`Extended ${n} listing${n === 1 ? "" : "s"} by 30 days`, { action: { label: "Undo", onClick: () => {} } }) },
83
+ { label: "Duplicate", icon: <Copy />, shortcut: "D", onSelect: () => toast.success(`Duplicated ${n} listing${n === 1 ? "" : "s"}`) },
84
+ ]} />
85
+ <Table aria-label="Listings">
86
+ <TableHeader>
87
+ <TableRow className="hover:bg-transparent">
88
+ <TableHead className="w-9"><Checkbox aria-label="Select all on this page" checked={all} onCheckedChange={(v) => setAll(!!v)} /></TableHead>
89
+ <TableHead>Role</TableHead>
90
+ <TableHead>Location</TableHead>
91
+ </TableRow>
92
+ </TableHeader>
93
+ <TableBody>
94
+ {ROWS.map((r) => (
95
+ <TableRow key={r.id} data-state={sel.includes(r.id) ? "selected" : undefined}>
96
+ <TableCell><Checkbox checked={sel.includes(r.id)} onCheckedChange={() => toggle(r.id)} aria-label={`Select ${r.role}`} /></TableCell>
97
+ <TableCell className="font-medium text-ink">{r.role}</TableCell>
98
+ <TableCell className="text-ink-2">{r.location}</TableCell>
99
+ </TableRow>
100
+ ))}
101
+ </TableBody>
102
+ </Table>
103
+ </div>
104
+ );
105
+ }
106
+ ```
107
+
108
+ ## Related
109
+
110
+ - List table
111
+ - Data grid
112
+ - Export menu