@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,339 @@
1
+ /**
2
+ * Combobox: pick one or several values from a long list by typing, and add a missing one without leaving the form.
3
+ * TwoUX composition on the generated shadcn Popover and Command (cmdk), following shadcn's combobox pattern.
4
+ * Proposal: proposals/combobox.md.
5
+ */
6
+ "use client"
7
+
8
+ import { AlertCircle, Check, ChevronDown, Plus, X } from "lucide-react"
9
+ import * as React from "react"
10
+ import { cn } from "../lib/cn"
11
+ import { Button } from "./button"
12
+ import { Command, CommandGroup, CommandInput, CommandItem, CommandList } from "./command"
13
+ import { Field, FieldDescription, FieldLabel } from "./field"
14
+ import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "./popover"
15
+
16
+ export type ComboboxOption = {
17
+ value: string
18
+ label: string
19
+ /** Secondary text, matched as well as the label: a region, a team, a code. */
20
+ description?: string
21
+ /** Options with the same group are listed under that heading, in the order they first appear. */
22
+ group?: string
23
+ disabled?: boolean
24
+ }
25
+
26
+ type Common = {
27
+ label: string
28
+ options: ComboboxOption[]
29
+ /** The thing listed, used in messages: { one: "location", many: "locations" }. Defaults to the label in lower case. */
30
+ noun?: { one: string; many: string }
31
+ placeholder?: string
32
+ hint?: string
33
+ error?: string
34
+ required?: boolean
35
+ optional?: boolean
36
+ disabled?: boolean
37
+ /** Shows the value as text, without the list. */
38
+ readOnly?: boolean
39
+ id?: string
40
+ /** Submitted with the form: one value, or one value per chosen option. */
41
+ name?: string
42
+ className?: string
43
+ /** Called as people type, after 250ms without typing. Set filter to null when the app filters (server search). */
44
+ onSearch?: (text: string) => void
45
+ /** null: the app has already filtered options for the text. A function: custom matching. Default: label and description, ignoring case and accents. */
46
+ filter?: null | ((option: ComboboxOption, text: string) => boolean)
47
+ /** The app is fetching options. "Searching…" appears only if this lasts more than a second. */
48
+ loading?: boolean
49
+ /** Options failed to load: what failed and what to check, shown in the list with Try again. */
50
+ loadError?: string
51
+ onRetry?: () => void
52
+ /**
53
+ * Offers Create "<text>" when nothing matches exactly. Return the new option to select it at once (name-only
54
+ * records), or open an overlay side panel and select the record when it is saved.
55
+ */
56
+ onCreate?: (text: string) => ComboboxOption | void | Promise<ComboboxOption | void>
57
+ }
58
+ type Single = Common & { multiple?: false; value: string | null; onValueChange: (value: string | null) => void }
59
+ type Multiple = Common & {
60
+ multiple: true
61
+ value: string[]
62
+ onValueChange: (value: string[]) => void
63
+ /** Most options people can choose. The footer says so. */
64
+ max?: number
65
+ }
66
+ export type ComboboxProps = Single | Multiple
67
+
68
+ const collator = new Intl.Collator("en-GB", { sensitivity: "base", usage: "search" })
69
+ const fold = (s: string) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase()
70
+ /** Case- and accent-insensitive "contains", on the label and the description. */
71
+ export const matchOption = (o: ComboboxOption, text: string) => {
72
+ const t = fold(text.trim())
73
+ return !t || fold(o.label).includes(t) || (!!o.description && fold(o.description).includes(t))
74
+ }
75
+ const MORE = 3
76
+
77
+ function useDelayed(on: boolean | undefined, ms: number) {
78
+ const [late, setLate] = React.useState(false)
79
+ React.useEffect(() => {
80
+ if (!on) return setLate(false)
81
+ const t = setTimeout(() => setLate(true), ms)
82
+ return () => clearTimeout(t)
83
+ }, [on, ms])
84
+ return late
85
+ }
86
+
87
+ /** Searchable select for long lists: one value, or several with multiple. Values always come from the list or from Create. */
88
+ export function Combobox(props: ComboboxProps) {
89
+ const { label, options, noun, placeholder, hint, error, required, optional, disabled, readOnly, id, name, className, onSearch, filter, loading, loadError, onRetry, onCreate } = props
90
+ const auto = React.useId()
91
+ const fid = id ?? auto
92
+ const many = noun?.many ?? label.toLowerCase()
93
+ const multiple = props.multiple === true
94
+ const chosen: string[] = multiple ? props.value : props.value ? [props.value] : []
95
+ const max = multiple ? props.max : undefined
96
+ const known = React.useRef(new Map<string, ComboboxOption>())
97
+ for (const o of options) known.current.set(o.value, o)
98
+ const labelOf = (v: string) => known.current.get(v)?.label ?? v
99
+
100
+ const [open, setOpen] = React.useState(false)
101
+ const [text, setText] = React.useState("")
102
+ const [order, setOrder] = React.useState<string[]>([])
103
+ const [creating, setCreating] = React.useState(false)
104
+ const triggerRef = React.useRef<HTMLButtonElement>(null)
105
+ const tagRefs = React.useRef<(HTMLButtonElement | null)[]>([])
106
+ const searching = useDelayed(loading, 1000)
107
+
108
+ React.useEffect(() => {
109
+ if (!onSearch || !open) return
110
+ const t = setTimeout(() => onSearch(text), 250)
111
+ return () => clearTimeout(t)
112
+ }, [text, open, onSearch])
113
+
114
+ const setValue = (next: string[]) => (multiple ? (props as Multiple).onValueChange(next) : (props as Single).onValueChange(next[0] ?? null))
115
+ const openWith = (initial: string) => {
116
+ // Chosen options are listed first, in the order they had when the list opened, so rows don't jump while ticking.
117
+ setOrder(multiple ? [...chosen] : [])
118
+ setText(initial)
119
+ setOpen(true)
120
+ }
121
+ const pick = (v: string) => {
122
+ if (!multiple) {
123
+ setValue([v])
124
+ setOpen(false)
125
+ return
126
+ }
127
+ if (chosen.includes(v)) setValue(chosen.filter((x) => x !== v))
128
+ else if (!max || chosen.length < max) setValue([...chosen, v])
129
+ }
130
+ const create = async () => {
131
+ if (!onCreate) return
132
+ setCreating(true)
133
+ try {
134
+ const made = await onCreate(text.trim())
135
+ if (made) {
136
+ known.current.set(made.value, made)
137
+ pick(made.value)
138
+ }
139
+ setText("")
140
+ } finally {
141
+ setCreating(false)
142
+ }
143
+ }
144
+ const remove = (i: number) => {
145
+ setValue(chosen.filter((_, j) => j !== i))
146
+ requestAnimationFrame(() => (tagRefs.current[i - 1] ?? triggerRef.current)?.focus())
147
+ }
148
+
149
+ const visible = filter === null ? options : options.filter((o) => (filter ?? matchOption)(o, text))
150
+ const sorted = order.length ? [...visible.filter((o) => order.includes(o.value)), ...visible.filter((o) => !order.includes(o.value))] : visible
151
+ const groups: [string, ComboboxOption[]][] = []
152
+ for (const o of sorted) {
153
+ const g = o.group ?? ""
154
+ const at = groups.find(([k]) => k === g)
155
+ if (at) at[1].push(o)
156
+ else groups.push([g, [o]])
157
+ }
158
+ const exact = options.some((o) => collator.compare(o.label, text.trim()) === 0)
159
+ const canCreate = !!onCreate && !!text.trim() && !exact && !loading
160
+ const full = !!max && chosen.length >= max
161
+ const shown = chosen.slice(0, multiple ? MORE : 1)
162
+ const rest = chosen.length - shown.length
163
+ const msgId = `${fid}-msg`
164
+ const labelId = `${fid}-label`
165
+ const valueId = `${fid}-value`
166
+ const status = searching ? `Searching ${many}…` : loadError ? "" : `${visible.length} ${visible.length === 1 ? "result" : "results"}`
167
+ const summary = multiple ? (chosen.length ? `${chosen.length} selected` : "None selected") : chosen[0] ? labelOf(chosen[0]) : "None selected"
168
+
169
+ const message = error ? (
170
+ <p id={msgId} className="flex items-start gap-control text-sm text-bad">
171
+ <AlertCircle className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
172
+ {error}
173
+ </p>
174
+ ) : hint ? (
175
+ <FieldDescription id={msgId}>{hint}</FieldDescription>
176
+ ) : null
177
+
178
+ const fieldLabel = (
179
+ <FieldLabel id={labelId} htmlFor={fid}>
180
+ {label}
181
+ {required && <span aria-hidden="true" className="text-bad">*</span>}
182
+ {optional && <span className="font-normal text-ink-3">(optional)</span>}
183
+ </FieldLabel>
184
+ )
185
+
186
+ if (readOnly)
187
+ return (
188
+ <Field className={cn("gap-2", className)}>
189
+ {fieldLabel}
190
+ <p id={fid} className="flex min-h-8 flex-wrap items-center gap-1 text-base text-ink">
191
+ {chosen.length ? chosen.map((v) => (multiple ? <span key={v} className="inline-flex h-5.5 items-center rounded-xs border border-line-strong bg-surface px-control text-sm">{labelOf(v)}</span> : <span key={v}>{labelOf(v)}</span>)) : <span className="text-ink-3">None</span>}
192
+ </p>
193
+ </Field>
194
+ )
195
+
196
+ return (
197
+ <Field data-invalid={!!error || undefined} className={cn("gap-2", className)}>
198
+ {fieldLabel}
199
+ <Popover
200
+ open={open}
201
+ onOpenChange={(o) => {
202
+ if (o) openWith("")
203
+ else setOpen(false)
204
+ }}
205
+ >
206
+ <PopoverAnchor asChild>
207
+ <div
208
+ data-slot="combobox"
209
+ className={cn(
210
+ "flex min-h-8 w-full min-w-0 flex-wrap items-center gap-1 rounded-sm border border-input bg-background shadow-xs transition-[color,background-color,border-color,box-shadow] duration-(--duration-fast) ease-(--ease-out) hover:border-input-hover",
211
+ "has-[[data-slot=combobox-trigger]:focus-visible]:border-ring has-[[data-slot=combobox-trigger]:focus-visible]:ring-3 has-[[data-slot=combobox-trigger]:focus-visible]:ring-ring/50",
212
+ error && "border-destructive-edge",
213
+ multiple && chosen.length > 0 && "py-1 pl-1",
214
+ disabled && "pointer-events-none opacity-50",
215
+ )}
216
+ >
217
+ {multiple &&
218
+ shown.map((v, i) => (
219
+ <span key={v} className="inline-flex h-5.5 max-w-48 items-center gap-control rounded-xs border border-line-strong bg-surface px-control text-sm text-ink">
220
+ <span className="truncate">{labelOf(v)}</span>
221
+ <button
222
+ type="button"
223
+ ref={(el) => {
224
+ tagRefs.current[i] = el
225
+ }}
226
+ aria-label={`Remove ${labelOf(v)}`}
227
+ disabled={disabled}
228
+ onClick={() => remove(i)}
229
+ onKeyDown={(e) => {
230
+ if (e.key === "Backspace" || e.key === "Delete") {
231
+ e.preventDefault()
232
+ remove(i)
233
+ }
234
+ if (e.key === "ArrowLeft") tagRefs.current[i - 1]?.focus()
235
+ if (e.key === "ArrowRight") (tagRefs.current[i + 1] ?? triggerRef.current)?.focus()
236
+ }}
237
+ className="relative -mr-0.5 grid size-4 shrink-0 place-items-center rounded-xs text-ink-3 after:absolute after:-inset-1 hover:bg-hover hover:text-ink focus-visible:outline-2 focus-visible:outline-ring"
238
+ >
239
+ <X className="size-3" aria-hidden="true" />
240
+ </button>
241
+ </span>
242
+ ))}
243
+ {multiple && rest > 0 && <span className="tabular px-0.5 text-sm text-ink-3">+{rest} more</span>}
244
+ <PopoverTrigger asChild>
245
+ <button
246
+ ref={triggerRef}
247
+ id={fid}
248
+ type="button"
249
+ data-slot="combobox-trigger"
250
+ role="combobox"
251
+ aria-expanded={open}
252
+ aria-haspopup="listbox"
253
+ aria-labelledby={`${labelId} ${valueId}`}
254
+ aria-describedby={message ? msgId : undefined}
255
+ aria-invalid={error ? true : undefined}
256
+ aria-required={required || undefined}
257
+ disabled={disabled}
258
+ onKeyDown={(e) => {
259
+ if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey && e.key !== " ") {
260
+ e.preventDefault()
261
+ openWith(e.key)
262
+ }
263
+ if (e.key === "ArrowDown") {
264
+ e.preventDefault()
265
+ openWith("")
266
+ }
267
+ if (e.key === "Backspace" && multiple && shown.length) {
268
+ e.preventDefault()
269
+ tagRefs.current[shown.length - 1]?.focus()
270
+ }
271
+ }}
272
+ // With tags chosen the button only holds the chevron, so it needs just enough room to stay on the tags' line.
273
+ className={cn("flex h-7.5 flex-1 items-center justify-between gap-2 rounded-sm px-2 text-left text-base outline-none", multiple && chosen.length ? "min-w-7" : "min-w-16")}
274
+ >
275
+ <span id={valueId} className={cn("truncate", (multiple || !chosen[0]) && "text-muted-foreground")}>
276
+ {multiple ? (chosen.length ? <span className="sr-only">{summary}</span> : (placeholder ?? `Choose ${many}`)) : chosen[0] ? <span className="text-ink">{labelOf(chosen[0])}</span> : (placeholder ?? `Choose ${noun?.one ?? many}`)}
277
+ </span>
278
+ <ChevronDown className="size-3.5 shrink-0 text-ink-3" aria-hidden="true" />
279
+ </button>
280
+ </PopoverTrigger>
281
+ </div>
282
+ </PopoverAnchor>
283
+ <PopoverContent align="start" className="w-(--radix-popover-trigger-width) min-w-64 p-0" onOpenAutoFocus={(e) => e.preventDefault()}>
284
+ <Command shouldFilter={false} loop={false} label={label}>
285
+ <CommandInput autoFocus value={text} onValueChange={setText} placeholder={`Search ${many}`} aria-label={`Search ${many}`} />
286
+ <CommandList aria-multiselectable={multiple || undefined} aria-label={label}>
287
+ {loadError ? (
288
+ <div role="alert" className="grid justify-items-start gap-2 px-3 py-3 text-base text-ink-2">
289
+ <span className="flex items-start gap-control"><AlertCircle className="mt-0.5 size-3.5 shrink-0 text-bad" aria-hidden="true" />{loadError}</span>
290
+ {onRetry && <Button variant="outline" size="xs" onClick={onRetry}>Try again</Button>}
291
+ </div>
292
+ ) : searching && !visible.length ? (
293
+ <p className="px-3 py-3 text-base text-ink-2">Searching {many}…</p>
294
+ ) : !visible.length && !canCreate ? (
295
+ <p className="px-3 py-3 text-base text-ink-2">{text.trim() ? `No ${many} match "${text.trim()}".` : `No ${many} yet.`}</p>
296
+ ) : null}
297
+ {!loadError &&
298
+ groups.map(([g, items]) => (
299
+ <CommandGroup key={g || "_"} heading={g || undefined}>
300
+ {items.map((o) => {
301
+ const on = chosen.includes(o.value)
302
+ return (
303
+ <CommandItem key={o.value} value={o.value} disabled={o.disabled || (full && !on)} data-checked={on || undefined} onSelect={() => pick(o.value)} className="pr-8">
304
+ <span className="min-w-0 flex-1 truncate">{o.label}</span>
305
+ {o.description && <span className="shrink-0 text-sm text-ink-3">{o.description}</span>}
306
+ <Check className={cn("absolute right-2 size-3.5 text-ink-2", !on && "invisible")} aria-hidden="true" />
307
+ <span className="sr-only">{on ? ", selected" : ""}</span>
308
+ </CommandItem>
309
+ )
310
+ })}
311
+ </CommandGroup>
312
+ ))}
313
+ {canCreate && !loadError && (
314
+ <CommandGroup>
315
+ <CommandItem value={`__create__${text}`} disabled={creating} onSelect={create}>
316
+ <Plus aria-hidden="true" />
317
+ <span className="truncate">Create "{text.trim()}"</span>
318
+ </CommandItem>
319
+ </CommandGroup>
320
+ )}
321
+ </CommandList>
322
+ </Command>
323
+ {multiple && (
324
+ <div className="flex items-center gap-2 border-t border-line px-3 py-2 text-sm text-ink-2">
325
+ <span className="tabular flex-1">
326
+ {chosen.length} selected
327
+ {max ? ` · up to ${max} ${many}` : ""}
328
+ </span>
329
+ {chosen.length > 0 && <Button variant="ghost" size="xs" onClick={() => setValue([])}>Clear</Button>}
330
+ </div>
331
+ )}
332
+ <span className="sr-only" role="status" aria-live="polite">{open ? status : ""}</span>
333
+ </PopoverContent>
334
+ </Popover>
335
+ {name && chosen.map((v) => <input key={v} type="hidden" name={name} value={v} />)}
336
+ {message}
337
+ </Field>
338
+ )
339
+ }
@@ -0,0 +1,11 @@
1
+ /** TwoUX additions to shadcn Command: one-line secondary text and a key-hint footer. */
2
+ import type * as React from "react"
3
+ import { cn } from "../lib/cn"
4
+
5
+ export function CommandItemMeta({ className, ...props }: React.ComponentProps<"span">) {
6
+ return <span className={cn("min-w-0 truncate text-ink-3", className)} {...props} />
7
+ }
8
+
9
+ export function CommandFooter({ className, ...props }: React.ComponentProps<"div">) {
10
+ return <div className={cn("flex h-9 items-center gap-4 border-t border-line px-3 text-sm text-ink-3", className)} {...props} />
11
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Generated from shadcn/ui new-york-v4 "command" by scripts/tailor-shadcn.mjs. Do not edit by hand.
3
+ * TwoUX changes:
4
+ * - Icons stay smaller than the label and muted
5
+ * - Group labels in sentence case, quiet
6
+ * - Dialog keeps compact rows and 14px icons; shadcn enlarges them to 20px with 12px padding
7
+ * - Shortcut slot holds words as well as keys: no letter-spacing, icons align
8
+ * - Adds commandProps (shouldFilter, loop…) passed to the inner Command
9
+ * - Opens near the top like a search, with the floating shadow; Esc closes it so no close button
10
+ */
11
+ "use client"
12
+
13
+ import * as React from "react"
14
+ import { Command as CommandPrimitive } from "cmdk"
15
+ import { cn } from "../lib/cn"
16
+ import { SearchIcon } from "lucide-react"
17
+
18
+ import {
19
+ Dialog,
20
+ DialogContent,
21
+ DialogDescription,
22
+ DialogHeader,
23
+ DialogTitle,
24
+ } from "./dialog"
25
+
26
+ function Command({
27
+ className,
28
+ ...props
29
+ }: React.ComponentProps<typeof CommandPrimitive>) {
30
+ return (
31
+ <CommandPrimitive
32
+ data-slot="command"
33
+ className={cn(
34
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
35
+ className
36
+ )}
37
+ {...props}
38
+ />
39
+ )
40
+ }
41
+
42
+ function CommandDialog({
43
+ title = "Command Palette",
44
+ description = "Search for a command to run...",
45
+ children,
46
+ className,
47
+ showCloseButton = false,
48
+ commandProps,
49
+ ...props
50
+ }: React.ComponentProps<typeof Dialog> & {
51
+ commandProps?: React.ComponentProps<typeof CommandPrimitive>
52
+ title?: string
53
+ description?: string
54
+ className?: string
55
+ showCloseButton?: boolean
56
+ }) {
57
+ return (
58
+ <Dialog {...props}>
59
+ <DialogHeader className="sr-only">
60
+ <DialogTitle>{title}</DialogTitle>
61
+ <DialogDescription>{description}</DialogDescription>
62
+ </DialogHeader>
63
+ <DialogContent
64
+ className={cn("top-24 translate-y-0 overflow-hidden border-line p-0 shadow-pop sm:max-w-xl", className)}
65
+ showCloseButton={showCloseButton}
66
+ >
67
+ <Command {...commandProps} className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group]]:px-1.5 [&_[cmdk-input]]:h-12 [&_[cmdk-input]]:text-md">
68
+ {children}
69
+ </Command>
70
+ </DialogContent>
71
+ </Dialog>
72
+ )
73
+ }
74
+
75
+ function CommandInput({
76
+ className,
77
+ ...props
78
+ }: React.ComponentProps<typeof CommandPrimitive.Input>) {
79
+ return (
80
+ <div
81
+ data-slot="command-input-wrapper"
82
+ className="flex h-9 items-center gap-2 border-b px-3"
83
+ >
84
+ <SearchIcon className="size-3.5 shrink-0 text-muted-foreground" />
85
+ <CommandPrimitive.Input
86
+ data-slot="command-input"
87
+ className={cn(
88
+ "flex h-10 w-full rounded-sm bg-transparent py-3 text-base outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
89
+ className
90
+ )}
91
+ {...props}
92
+ />
93
+ </div>
94
+ )
95
+ }
96
+
97
+ function CommandList({
98
+ className,
99
+ ...props
100
+ }: React.ComponentProps<typeof CommandPrimitive.List>) {
101
+ return (
102
+ <CommandPrimitive.List
103
+ data-slot="command-list"
104
+ className={cn(
105
+ "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
106
+ className
107
+ )}
108
+ {...props}
109
+ />
110
+ )
111
+ }
112
+
113
+ function CommandEmpty({
114
+ ...props
115
+ }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
116
+ return (
117
+ <CommandPrimitive.Empty
118
+ data-slot="command-empty"
119
+ className="py-6 text-center text-base"
120
+ {...props}
121
+ />
122
+ )
123
+ }
124
+
125
+ function CommandGroup({
126
+ className,
127
+ ...props
128
+ }: React.ComponentProps<typeof CommandPrimitive.Group>) {
129
+ return (
130
+ <CommandPrimitive.Group
131
+ data-slot="command-group"
132
+ className={cn(
133
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-sm [&_[cmdk-group-heading]]:text-ink-3 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
134
+ className
135
+ )}
136
+ {...props}
137
+ />
138
+ )
139
+ }
140
+
141
+ function CommandSeparator({
142
+ className,
143
+ ...props
144
+ }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
145
+ return (
146
+ <CommandPrimitive.Separator
147
+ data-slot="command-separator"
148
+ className={cn("-mx-1 h-px bg-border", className)}
149
+ {...props}
150
+ />
151
+ )
152
+ }
153
+
154
+ function CommandItem({
155
+ className,
156
+ ...props
157
+ }: React.ComponentProps<typeof CommandPrimitive.Item>) {
158
+ return (
159
+ <CommandPrimitive.Item
160
+ data-slot="command-item"
161
+ className={cn(
162
+ "relative flex cursor-default items-center gap-2 rounded-xs px-2 py-1.5 text-base outline-hidden select-none transition-colors duration-(--duration-fast) ease-(--ease-out) data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 [&_svg:not([class*='text-'])]:text-muted-foreground",
163
+ className
164
+ )}
165
+ {...props}
166
+ />
167
+ )
168
+ }
169
+
170
+ function CommandShortcut({
171
+ className,
172
+ ...props
173
+ }: React.ComponentProps<"span">) {
174
+ return (
175
+ <span
176
+ data-slot="command-shortcut"
177
+ className={cn(
178
+ "ml-auto flex items-center gap-1 text-sm text-muted-foreground",
179
+ className
180
+ )}
181
+ {...props}
182
+ />
183
+ )
184
+ }
185
+
186
+ export {
187
+ Command,
188
+ CommandDialog,
189
+ CommandInput,
190
+ CommandList,
191
+ CommandEmpty,
192
+ CommandGroup,
193
+ CommandItem,
194
+ CommandShortcut,
195
+ CommandSeparator,
196
+ }