@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,114 @@
1
+ /**
2
+ * Generated from shadcn/ui new-york-v4 "empty" by scripts/tailor-shadcn.mjs. Do not edit by hand.
3
+ * TwoUX changes:
4
+ * - Dashed container sized to the region it replaces, compact spacing
5
+ * - 32px neutral icon tile, no illustration; adds the error tile for the error state
6
+ * - The title is a real heading for screen readers (level 2 by default, set level to fit the page)
7
+ * - Description at most 36 characters wide, readable grey
8
+ */
9
+ import { cva, type VariantProps } from "class-variance-authority"
10
+ import { cn } from "../lib/cn"
11
+
12
+ function Empty({ className, ...props }: React.ComponentProps<"div">) {
13
+ return (
14
+ <div
15
+ data-slot="empty"
16
+ className={cn(
17
+ "flex min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-sm border border-dashed border-line-strong p-6 text-center text-balance md:p-10",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ )
23
+ }
24
+
25
+ function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
26
+ return (
27
+ <div
28
+ data-slot="empty-header"
29
+ className={cn(
30
+ "flex max-w-sm flex-col items-center gap-1 text-center",
31
+ className
32
+ )}
33
+ {...props}
34
+ />
35
+ )
36
+ }
37
+
38
+ const emptyMediaVariants = cva(
39
+ "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
40
+ {
41
+ variants: {
42
+ variant: {
43
+ default: "bg-transparent",
44
+ icon: "flex size-8 shrink-0 items-center justify-center rounded-sm bg-muted text-ink-2 [&_svg:not([class*='size-'])]:size-4",
45
+ error: "flex size-8 shrink-0 items-center justify-center rounded-sm bg-bad-bg text-bad [&_svg:not([class*='size-'])]:size-4",
46
+ },
47
+ },
48
+ defaultVariants: {
49
+ variant: "default",
50
+ },
51
+ }
52
+ )
53
+
54
+ function EmptyMedia({
55
+ className,
56
+ variant = "default",
57
+ ...props
58
+ }: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
59
+ return (
60
+ <div
61
+ data-slot="empty-icon"
62
+ data-variant={variant}
63
+ className={cn(emptyMediaVariants({ variant, className }))}
64
+ {...props}
65
+ />
66
+ )
67
+ }
68
+
69
+ function EmptyTitle({ className, level = 2, ...props }: React.ComponentProps<"div"> & { level?: 1 | 2 | 3 | 4 | 5 | 6 }) {
70
+ return (
71
+ <div
72
+ role="heading"
73
+ aria-level={level}
74
+ data-slot="empty-title"
75
+ className={cn("text-md font-semibold", className)}
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
82
+ return (
83
+ <div
84
+ data-slot="empty-description"
85
+ className={cn(
86
+ "max-w-[36ch] text-base text-ink-2 [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
87
+ className
88
+ )}
89
+ {...props}
90
+ />
91
+ )
92
+ }
93
+
94
+ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
95
+ return (
96
+ <div
97
+ data-slot="empty-content"
98
+ className={cn(
99
+ "flex w-full max-w-sm min-w-0 flex-col items-center gap-3 text-base text-balance",
100
+ className
101
+ )}
102
+ {...props}
103
+ />
104
+ )
105
+ }
106
+
107
+ export {
108
+ Empty,
109
+ EmptyHeader,
110
+ EmptyTitle,
111
+ EmptyDescription,
112
+ EmptyContent,
113
+ EmptyMedia,
114
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * TwoUX composition: “Was this helpful?” with Yes / No, one vote, an optional follow-up, and every state
3
+ * (sending, sent, failed with Try again). One per page or panel.
4
+ */
5
+ import { Check, ThumbsDown, ThumbsUp } from "lucide-react"
6
+ import * as React from "react"
7
+ import { Button } from "./button"
8
+ import { Textarea } from "./textarea"
9
+
10
+ type Vote = "yes" | "no"
11
+
12
+ export function FeedbackPrompt({
13
+ question = "Was this helpful?",
14
+ description,
15
+ onVote,
16
+ onFollowUp,
17
+ privacy = "Don't include personal information about candidates.",
18
+ }: {
19
+ question?: string
20
+ description?: string
21
+ /** Sends the vote. Reject to show the failed state. */
22
+ onVote: (vote: Vote) => Promise<void>
23
+ /** Optional: sends what they typed after voting. */
24
+ onFollowUp?: (text: string, vote: Vote) => Promise<void>
25
+ privacy?: string
26
+ }) {
27
+ const [vote, setVote] = React.useState<Vote | null>(null)
28
+ const [state, setState] = React.useState<"idle" | "sending" | "sent" | "failed">("idle")
29
+ const [text, setText] = React.useState("")
30
+ const [followState, setFollowState] = React.useState<"idle" | "sending" | "sent" | "failed">("idle")
31
+ const id = React.useId()
32
+
33
+ const send = async (v: Vote) => {
34
+ setVote(v)
35
+ setState("sending")
36
+ try { await onVote(v); setState("sent") } catch { setState("failed") }
37
+ }
38
+ const sendFollow = async () => {
39
+ if (!vote || !onFollowUp) return
40
+ setFollowState("sending")
41
+ try { await onFollowUp(text, vote); setFollowState("sent") } catch { setFollowState("failed") }
42
+ }
43
+
44
+ return (
45
+ <section aria-labelledby={`${id}-q`} data-slot="feedback-prompt" className="grid gap-2">
46
+ <h3 id={`${id}-q`} className="text-base font-medium text-ink">{question}</h3>
47
+ {description && <p className="text-sm text-ink-2">{description}</p>}
48
+ {state === "sent" && vote ? (
49
+ <p role="status" className="flex items-center gap-control text-base text-ink-2">
50
+ <Check className="size-3.5 text-ok" aria-hidden="true" />
51
+ You answered {vote === "yes" ? "Yes" : "No"}. Thanks for telling us.
52
+ </p>
53
+ ) : (
54
+ <div className="flex items-center gap-2">
55
+ <Button variant="outline" size="sm" loading={state === "sending" && vote === "yes"} disabled={state === "sending"} onClick={() => send("yes")}><ThumbsUp />Yes</Button>
56
+ <Button variant="outline" size="sm" loading={state === "sending" && vote === "no"} disabled={state === "sending"} onClick={() => send("no")}><ThumbsDown />No</Button>
57
+ </div>
58
+ )}
59
+ {state === "failed" && (
60
+ <p role="alert" className="flex items-center gap-2 text-sm text-bad">
61
+ Your answer wasn't sent.
62
+ <Button variant="ghost" size="xs" onClick={() => vote && send(vote)}>Try again</Button>
63
+ </p>
64
+ )}
65
+ {state === "sent" && onFollowUp && followState !== "sent" && (
66
+ <div className="grid gap-2">
67
+ <label htmlFor={`${id}-f`} className="text-sm font-medium text-ink">{vote === "no" ? "What was missing or wrong? (optional)" : "Anything else? (optional)"}</label>
68
+ <Textarea id={`${id}-f`} value={text} maxLength={500} onChange={(e) => setText(e.target.value)} aria-describedby={`${id}-p ${id}-c`} />
69
+ <div className="flex items-center justify-between gap-2 text-sm text-ink-3">
70
+ <span id={`${id}-p`}>{privacy}</span>
71
+ <span id={`${id}-c`} className="tabular">{text.length} / 500</span>
72
+ </div>
73
+ {followState === "failed" && <p role="alert" className="text-sm text-bad">Your comment wasn't sent. It's still here; try again.</p>}
74
+ <Button size="sm" variant="outline" className="justify-self-start" disabled={!text.trim()} loading={followState === "sending"} onClick={sendFollow}>Send</Button>
75
+ </div>
76
+ )}
77
+ {followState === "sent" && <p role="status" className="text-sm text-ink-2">Comment sent.</p>}
78
+ </section>
79
+ )
80
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Generated from shadcn/ui new-york-v4 "field" by scripts/tailor-shadcn.mjs. Do not edit by hand.
3
+ * TwoUX changes:
4
+ * - TwoUX spacing between fields is tighter (compact density)
5
+ * - An invalid field keeps its label and value in normal text; only the edge and the message turn red (less alarming, still clear)
6
+ */
7
+ "use client"
8
+
9
+ import { useMemo } from "react"
10
+ import { cva, type VariantProps } from "class-variance-authority"
11
+ import { cn } from "../lib/cn"
12
+
13
+ import { Label } from "./label"
14
+ import { Separator } from "./separator"
15
+
16
+ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
17
+ return (
18
+ <fieldset
19
+ data-slot="field-set"
20
+ className={cn(
21
+ "flex flex-col gap-6",
22
+ "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ )
28
+ }
29
+
30
+ function FieldLegend({
31
+ className,
32
+ variant = "legend",
33
+ ...props
34
+ }: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
35
+ return (
36
+ <legend
37
+ data-slot="field-legend"
38
+ data-variant={variant}
39
+ className={cn(
40
+ "mb-3 font-medium",
41
+ "data-[variant=legend]:text-md",
42
+ "data-[variant=label]:text-base",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
51
+ return (
52
+ <div
53
+ data-slot="field-group"
54
+ className={cn(
55
+ "group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
56
+ className
57
+ )}
58
+ {...props}
59
+ />
60
+ )
61
+ }
62
+
63
+ const fieldVariants = cva(
64
+ "group/field flex w-full gap-3",
65
+ {
66
+ variants: {
67
+ orientation: {
68
+ vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
69
+ horizontal: [
70
+ "flex-row items-center",
71
+ "[&>[data-slot=field-label]]:flex-auto",
72
+ "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
73
+ ],
74
+ responsive: [
75
+ "flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto",
76
+ "@md/field-group:[&>[data-slot=field-label]]:flex-auto",
77
+ "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
78
+ ],
79
+ },
80
+ },
81
+ defaultVariants: {
82
+ orientation: "vertical",
83
+ },
84
+ }
85
+ )
86
+
87
+ function Field({
88
+ className,
89
+ orientation = "vertical",
90
+ ...props
91
+ }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
92
+ return (
93
+ <div
94
+ role="group"
95
+ data-slot="field"
96
+ data-orientation={orientation}
97
+ className={cn(fieldVariants({ orientation }), className)}
98
+ {...props}
99
+ />
100
+ )
101
+ }
102
+
103
+ function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
104
+ return (
105
+ <div
106
+ data-slot="field-content"
107
+ className={cn(
108
+ "group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
109
+ className
110
+ )}
111
+ {...props}
112
+ />
113
+ )
114
+ }
115
+
116
+ function FieldLabel({
117
+ className,
118
+ ...props
119
+ }: React.ComponentProps<typeof Label>) {
120
+ return (
121
+ <Label
122
+ data-slot="field-label"
123
+ className={cn(
124
+ "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
125
+ "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-sm has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
126
+ "has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5",
127
+ className
128
+ )}
129
+ {...props}
130
+ />
131
+ )
132
+ }
133
+
134
+ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
135
+ return (
136
+ <div
137
+ data-slot="field-label"
138
+ className={cn(
139
+ "flex w-fit items-center gap-2 text-base leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
140
+ className
141
+ )}
142
+ {...props}
143
+ />
144
+ )
145
+ }
146
+
147
+ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
148
+ return (
149
+ <p
150
+ data-slot="field-description"
151
+ className={cn(
152
+ "text-base leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
153
+ "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
154
+ "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
155
+ className
156
+ )}
157
+ {...props}
158
+ />
159
+ )
160
+ }
161
+
162
+ function FieldSeparator({
163
+ children,
164
+ className,
165
+ ...props
166
+ }: React.ComponentProps<"div"> & {
167
+ children?: React.ReactNode
168
+ }) {
169
+ return (
170
+ <div
171
+ data-slot="field-separator"
172
+ data-content={!!children}
173
+ className={cn(
174
+ "relative -my-2 h-5 text-base group-data-[variant=outline]/field-group:-mb-2",
175
+ className
176
+ )}
177
+ {...props}
178
+ >
179
+ <Separator className="absolute inset-0 top-1/2" />
180
+ {children && (
181
+ <span
182
+ className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
183
+ data-slot="field-separator-content"
184
+ >
185
+ {children}
186
+ </span>
187
+ )}
188
+ </div>
189
+ )
190
+ }
191
+
192
+ function FieldError({
193
+ className,
194
+ children,
195
+ errors,
196
+ ...props
197
+ }: React.ComponentProps<"div"> & {
198
+ errors?: Array<{ message?: string } | undefined>
199
+ }) {
200
+ const content = useMemo(() => {
201
+ if (children) {
202
+ return children
203
+ }
204
+
205
+ if (!errors?.length) {
206
+ return null
207
+ }
208
+
209
+ const uniqueErrors = [
210
+ ...new Map(errors.map((error) => [error?.message, error])).values(),
211
+ ]
212
+
213
+ if (uniqueErrors?.length == 1) {
214
+ return uniqueErrors[0]?.message
215
+ }
216
+
217
+ return (
218
+ <ul className="ml-4 flex list-disc flex-col gap-1">
219
+ {uniqueErrors.map(
220
+ (error, index) =>
221
+ error?.message && <li key={index}>{error.message}</li>
222
+ )}
223
+ </ul>
224
+ )
225
+ }, [children, errors])
226
+
227
+ if (!content) {
228
+ return null
229
+ }
230
+
231
+ return (
232
+ <div
233
+ role="alert"
234
+ data-slot="field-error"
235
+ className={cn("text-base font-normal text-destructive", className)}
236
+ {...props}
237
+ >
238
+ {content}
239
+ </div>
240
+ )
241
+ }
242
+
243
+ export {
244
+ Field,
245
+ FieldLabel,
246
+ FieldDescription,
247
+ FieldError,
248
+ FieldGroup,
249
+ FieldLegend,
250
+ FieldSeparator,
251
+ FieldSet,
252
+ FieldContent,
253
+ FieldTitle,
254
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * TwoUX compositions on shadcn Field, Label, Input, Checkbox, RadioGroup and Switch.
3
+ * They wire label, hint and error to the control (htmlFor, aria-describedby, aria-invalid) so every field is accessible by default.
4
+ */
5
+ import { AlertCircle, CheckCircle2 } from "lucide-react"
6
+ import * as React from "react"
7
+ import { cn } from "../lib/cn"
8
+ import { Checkbox } from "./checkbox"
9
+ import { Field, FieldDescription, FieldLabel } from "./field"
10
+ import { Input } from "./input"
11
+ import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "./input-group"
12
+ import { RadioGroupItem } from "./radio-group"
13
+ import { Switch } from "./switch"
14
+
15
+ type TextFieldProps = Omit<React.ComponentProps<"input">, "prefix"> & {
16
+ label: string
17
+ hint?: string
18
+ error?: string
19
+ /** Confirms the value meets the rules, e.g. a password. Shown in place of the hint. */
20
+ success?: string
21
+ optional?: boolean
22
+ /** Inside the field, before the value: £, a search icon. */
23
+ prefix?: React.ReactNode
24
+ /** Inside the field, after the value: a unit or a shortcut. */
25
+ suffix?: React.ReactNode
26
+ }
27
+
28
+ export function TextField({ label, hint, error, success, optional, required, prefix, suffix, id, className, ...props }: TextFieldProps) {
29
+ const auto = React.useId()
30
+ const fid = id ?? auto
31
+ const describedBy = error ? `${fid}-error` : success ? `${fid}-ok` : hint ? `${fid}-hint` : undefined
32
+ const control = { id: fid, required, "aria-invalid": error ? true : undefined, "aria-describedby": describedBy, ...props }
33
+ return (
34
+ <Field data-invalid={!!error || undefined} className={cn("gap-2", className)}>
35
+ <FieldLabel htmlFor={fid}>
36
+ {label}
37
+ {required && <span aria-hidden="true" className="text-bad">*</span>}
38
+ {optional && <span className="font-normal text-ink-3">(optional)</span>}
39
+ </FieldLabel>
40
+ {prefix || suffix ? (
41
+ <InputGroup>
42
+ {prefix && <InputGroupAddon><InputGroupText>{prefix}</InputGroupText></InputGroupAddon>}
43
+ <InputGroupInput {...control} />
44
+ {suffix && <InputGroupAddon align="inline-end"><InputGroupText>{suffix}</InputGroupText></InputGroupAddon>}
45
+ </InputGroup>
46
+ ) : (
47
+ <Input {...control} />
48
+ )}
49
+ {error ? (
50
+ <p id={`${fid}-error`} className="flex items-start gap-control text-sm text-bad">
51
+ <AlertCircle className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
52
+ {error}
53
+ </p>
54
+ ) : success ? (
55
+ <p id={`${fid}-ok`} className="flex items-start gap-control text-sm text-ok">
56
+ <CheckCircle2 className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
57
+ {success}
58
+ </p>
59
+ ) : hint ? (
60
+ <FieldDescription id={`${fid}-hint`}>{hint}</FieldDescription>
61
+ ) : null}
62
+ </Field>
63
+ )
64
+ }
65
+
66
+ function Choice({ id, label, hint, control }: { id: string; label: string; hint?: string; control: React.ReactNode }) {
67
+ return (
68
+ <div className="grid grid-cols-[16px_1fr] gap-x-2">
69
+ <span className="flex h-6 items-center">{control}</span>
70
+ <label htmlFor={id} className="flex min-h-6 cursor-pointer items-center text-base text-ink">{label}</label>
71
+ {hint && <span id={`${id}-hint`} className="col-start-2 text-sm text-ink-3">{hint}</span>}
72
+ </div>
73
+ )
74
+ }
75
+
76
+ export function CheckboxField({ id, label, hint, ...props }: React.ComponentProps<typeof Checkbox> & { id: string; label: string; hint?: string }) {
77
+ return <Choice id={id} label={label} hint={hint} control={<Checkbox id={id} aria-describedby={hint ? `${id}-hint` : undefined} {...props} />} />
78
+ }
79
+
80
+ export function RadioField({ id, value, label, hint, disabled }: { id: string; value: string; label: string; hint?: string; disabled?: boolean }) {
81
+ return <Choice id={id} label={label} hint={hint} control={<RadioGroupItem id={id} value={value} disabled={disabled} aria-describedby={hint ? `${id}-hint` : undefined} />} />
82
+ }
83
+
84
+ export function SwitchField({ id, label, hint, ...props }: React.ComponentProps<typeof Switch> & { id: string; label: string; hint?: string }) {
85
+ return (
86
+ <div className="flex items-start justify-between gap-6">
87
+ <div className="grid gap-0.5">
88
+ <label htmlFor={id} className="cursor-pointer text-base text-ink">{label}</label>
89
+ {hint && <span id={`${id}-hint`} className="text-sm text-ink-3">{hint}</span>}
90
+ </div>
91
+ <span className="flex h-5 items-center"><Switch id={id} aria-describedby={hint ? `${id}-hint` : undefined} {...props} /></span>
92
+ </div>
93
+ )
94
+ }
95
+
96
+ /** Shown above a form after a failed submit. Each link moves focus to its field. */
97
+ export function ErrorSummary({ errors, className }: { errors: { field: string; message: string; href: string }[]; className?: string }) {
98
+ if (!errors.length) return null
99
+ return (
100
+ <div role="alert" tabIndex={-1} className={cn("grid gap-2 rounded-md border border-bad-line bg-bad-bg/60 px-3 py-2", className)}>
101
+ <p className="flex items-center gap-control text-base font-medium text-bad">
102
+ <AlertCircle className="size-3.5" aria-hidden="true" />
103
+ {errors.length === 1 ? "1 field needs fixing" : `${errors.length} fields need fixing`}
104
+ </p>
105
+ <ul className="grid gap-0.5 pl-5.5 text-sm">
106
+ {errors.map((e) => (
107
+ <li key={e.href}><a href={e.href} className="text-ink underline decoration-bad-line underline-offset-3 transition-colors duration-(--duration-fast) hover:decoration-bad"><span className="font-medium">{e.field}</span>: {e.message}</a></li>
108
+ ))}
109
+ </ul>
110
+ </div>
111
+ )
112
+ }