banhaten 0.3.0 → 0.5.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 (348) hide show
  1. package/CHANGELOG.md +139 -73
  2. package/CONTRIBUTING.md +46 -44
  3. package/README.md +192 -190
  4. package/SECURITY.md +15 -15
  5. package/banhaten.config.example.json +11 -11
  6. package/docs/design-spacing.md +13 -0
  7. package/docs/design-system/README.md +22 -20
  8. package/docs/design-system/adoption-quality-remediation.md +87 -87
  9. package/docs/design-system/agent-usage.md +13 -13
  10. package/docs/design-system/appearance-presets.md +189 -189
  11. package/docs/design-system/appearances/default.md +94 -94
  12. package/docs/design-system/appearances/rounded.md +95 -95
  13. package/docs/design-system/appearances/sharp.md +95 -95
  14. package/docs/design-system/component-docs-pattern.md +81 -81
  15. package/docs/design-system/component-implementation-ownership-audit.md +245 -244
  16. package/docs/design-system/component-quality-audit-2026-08-20.md +225 -223
  17. package/docs/design-system/component-showcase-consistency-report.md +219 -219
  18. package/docs/design-system/component-token-consistency-audit.md +173 -173
  19. package/docs/design-system/components/README.md +90 -76
  20. package/docs/design-system/components/accordion.md +53 -53
  21. package/docs/design-system/components/activity-feed.md +94 -94
  22. package/docs/design-system/components/activity-heatmap.md +57 -57
  23. package/docs/design-system/components/agent-input.md +85 -0
  24. package/docs/design-system/components/alert-dialog.md +72 -72
  25. package/docs/design-system/components/alert.md +98 -98
  26. package/docs/design-system/components/approval-card.md +134 -0
  27. package/docs/design-system/components/aspect-ratio.md +45 -45
  28. package/docs/design-system/components/attribute.md +89 -89
  29. package/docs/design-system/components/autocomplete.md +75 -75
  30. package/docs/design-system/components/avatar.md +54 -54
  31. package/docs/design-system/components/badge.md +54 -54
  32. package/docs/design-system/components/banner.md +87 -87
  33. package/docs/design-system/components/breadcrumbs.md +177 -176
  34. package/docs/design-system/components/button-group.md +98 -98
  35. package/docs/design-system/components/button.md +83 -80
  36. package/docs/design-system/components/card.md +88 -88
  37. package/docs/design-system/components/carousel.md +46 -46
  38. package/docs/design-system/components/catalog-components.md +47 -47
  39. package/docs/design-system/components/chart.md +95 -24
  40. package/docs/design-system/components/checkbox.md +54 -54
  41. package/docs/design-system/components/collapsible.md +67 -53
  42. package/docs/design-system/components/command.md +62 -62
  43. package/docs/design-system/components/context-menu.md +46 -46
  44. package/docs/design-system/components/date-picker.md +81 -79
  45. package/docs/design-system/components/divider.md +102 -102
  46. package/docs/design-system/components/empty-state.md +56 -56
  47. package/docs/design-system/components/field.md +70 -70
  48. package/docs/design-system/components/file-upload.md +187 -187
  49. package/docs/design-system/components/hover-card.md +48 -48
  50. package/docs/design-system/components/icons.md +48 -48
  51. package/docs/design-system/components/inline-citations.md +93 -0
  52. package/docs/design-system/components/input-group.md +58 -58
  53. package/docs/design-system/components/input-otp.md +56 -56
  54. package/docs/design-system/components/input.md +117 -61
  55. package/docs/design-system/components/item.md +59 -57
  56. package/docs/design-system/components/kbd.md +54 -49
  57. package/docs/design-system/components/label.md +49 -49
  58. package/docs/design-system/components/menu.md +71 -61
  59. package/docs/design-system/components/menubar.md +47 -47
  60. package/docs/design-system/components/modal.md +105 -105
  61. package/docs/design-system/components/native-select.md +54 -54
  62. package/docs/design-system/components/navigation-menu.md +50 -50
  63. package/docs/design-system/components/onboarding-step-list-item.md +82 -82
  64. package/docs/design-system/components/page-header.md +93 -86
  65. package/docs/design-system/components/pagination.md +57 -57
  66. package/docs/design-system/components/popover.md +60 -60
  67. package/docs/design-system/components/progress-slider.md +50 -50
  68. package/docs/design-system/components/progress.md +76 -76
  69. package/docs/design-system/components/radio-card.md +51 -51
  70. package/docs/design-system/components/radio-group.md +57 -57
  71. package/docs/design-system/components/resizable.md +44 -44
  72. package/docs/design-system/components/rolling-count.md +61 -0
  73. package/docs/design-system/components/scroll-area.md +51 -51
  74. package/docs/design-system/components/segmented-control.md +54 -54
  75. package/docs/design-system/components/select.md +68 -62
  76. package/docs/design-system/components/sheet.md +67 -67
  77. package/docs/design-system/components/shimmer-text.md +67 -0
  78. package/docs/design-system/components/sidebar.md +127 -76
  79. package/docs/design-system/components/skeleton.md +75 -75
  80. package/docs/design-system/components/slideout.md +65 -65
  81. package/docs/design-system/components/slider.md +63 -63
  82. package/docs/design-system/components/social-button.md +50 -50
  83. package/docs/design-system/components/spinner.md +62 -62
  84. package/docs/design-system/components/steps.md +65 -65
  85. package/docs/design-system/components/streaming-text.md +82 -0
  86. package/docs/design-system/components/table.md +419 -414
  87. package/docs/design-system/components/tabs.md +59 -55
  88. package/docs/design-system/components/tag.md +80 -80
  89. package/docs/design-system/components/task-list.md +95 -0
  90. package/docs/design-system/components/textarea.md +50 -50
  91. package/docs/design-system/components/thinking-reasoning.md +72 -0
  92. package/docs/design-system/components/thinking-state.md +67 -0
  93. package/docs/design-system/components/timeline.md +83 -83
  94. package/docs/design-system/components/toast.md +58 -58
  95. package/docs/design-system/components/toggle.md +81 -81
  96. package/docs/design-system/components/toolbar.md +92 -87
  97. package/docs/design-system/components/tooltip.md +94 -92
  98. package/docs/design-system/components/typography.md +18 -18
  99. package/docs/design-system/density-and-elevation.md +65 -61
  100. package/docs/design-system/design-system-test-missing-items.md +368 -368
  101. package/docs/design-system/direction-and-localization.md +23 -23
  102. package/docs/design-system/form-recipes.md +73 -73
  103. package/docs/design-system/gap-promotion-policy.md +21 -21
  104. package/docs/design-system/icons.md +74 -69
  105. package/docs/design-system/installation.md +70 -68
  106. package/docs/design-system/launch-comparison-shadcn.md +98 -98
  107. package/docs/design-system/registry-and-cli.md +64 -64
  108. package/docs/design-system/release-policy.md +30 -30
  109. package/docs/design-system/roadmap.md +14 -14
  110. package/docs/design-system/support-matrix.md +23 -23
  111. package/docs/design-system/tabs.md +54 -53
  112. package/docs/design-system/template-gap-audit.md +32 -0
  113. package/docs/design-system/templates.md +59 -0
  114. package/docs/design-system/token-governance.md +72 -60
  115. package/package.json +98 -98
  116. package/registry/component-contract.json +40 -40
  117. package/registry/components/accordion.tsx +155 -155
  118. package/registry/components/agent-input-parts.tsx +552 -0
  119. package/registry/components/agent-input-state.ts +156 -0
  120. package/registry/components/agent-input.css +307 -0
  121. package/registry/components/agent-input.tsx +382 -0
  122. package/registry/components/alert-dialog.tsx +297 -297
  123. package/registry/components/alert.tsx +293 -293
  124. package/registry/components/approval-card.css +409 -0
  125. package/registry/components/approval-card.tsx +401 -0
  126. package/registry/components/approval-frame.tsx +108 -0
  127. package/registry/components/approval-questions.tsx +559 -0
  128. package/registry/components/aspect-ratio.tsx +30 -30
  129. package/registry/components/attribute.tsx +454 -448
  130. package/registry/components/autocomplete.tsx +598 -651
  131. package/registry/components/avatar.tsx +385 -381
  132. package/registry/components/badge.tsx +629 -629
  133. package/registry/components/button-group.tsx +565 -565
  134. package/registry/components/button.tsx +127 -123
  135. package/registry/components/card.tsx +638 -638
  136. package/registry/components/carousel.tsx +296 -296
  137. package/registry/components/chart-block-data.ts +177 -0
  138. package/registry/components/chart-block.css +171 -0
  139. package/registry/components/chart-block.tsx +260 -0
  140. package/registry/components/chart-interactions.ts +123 -0
  141. package/registry/components/chart-options.cartesian.ts +162 -0
  142. package/registry/components/chart-options.paint.ts +99 -0
  143. package/registry/components/chart-options.parts.ts +72 -0
  144. package/registry/components/chart-options.radial.ts +109 -0
  145. package/registry/components/chart-options.ts +44 -0
  146. package/registry/components/chart-options.types.ts +67 -0
  147. package/registry/components/chart.tsx +203 -203
  148. package/registry/components/checkbox.tsx +247 -247
  149. package/registry/components/collapsible.tsx +109 -69
  150. package/registry/components/command.tsx +208 -208
  151. package/registry/components/context-menu.tsx +236 -236
  152. package/registry/components/data-chart.tsx +40 -0
  153. package/registry/components/date-picker.tsx +1289 -1277
  154. package/registry/components/echart-theme.ts +246 -0
  155. package/registry/components/echart.css +49 -0
  156. package/registry/components/echart.tsx +178 -0
  157. package/registry/components/expanded/ActivityFeed.tsx +227 -227
  158. package/registry/components/expanded/ActivityHeatmap.tsx +64 -64
  159. package/registry/components/expanded/Banner.tsx +179 -179
  160. package/registry/components/expanded/Breadcrumbs.tsx +131 -131
  161. package/registry/components/expanded/CatalogComponentsShowcase.tsx +205 -205
  162. package/registry/components/expanded/CatalogDivider.tsx +49 -49
  163. package/registry/components/expanded/EmptyState.tsx +155 -155
  164. package/registry/components/expanded/FileUpload.tsx +543 -543
  165. package/registry/components/expanded/OnboardingStepListItem.tsx +60 -59
  166. package/registry/components/expanded/PageHeader.tsx +179 -166
  167. package/registry/components/expanded/Slideout.tsx +108 -108
  168. package/registry/components/expanded/Steps.tsx +402 -402
  169. package/registry/components/expanded/Table.tsx +1092 -1071
  170. package/registry/components/expanded/Timeline.tsx +203 -203
  171. package/registry/components/expanded/activityFeed.css +248 -248
  172. package/registry/components/expanded/activityHeatmap.css +66 -66
  173. package/registry/components/expanded/banner.css +260 -255
  174. package/registry/components/expanded/breadcrumbs.css +140 -140
  175. package/registry/components/expanded/catalogComponentsShowcase.css +87 -87
  176. package/registry/components/expanded/divider.css +75 -75
  177. package/registry/components/expanded/emptyState.css +111 -111
  178. package/registry/components/expanded/fileUpload.css +457 -457
  179. package/registry/components/expanded/onboardingStepListItem.css +135 -126
  180. package/registry/components/expanded/pageHeader.css +248 -290
  181. package/registry/components/expanded/slideout.css +276 -276
  182. package/registry/components/expanded/steps.css +356 -356
  183. package/registry/components/expanded/table.css +606 -603
  184. package/registry/components/expanded/timeline.css +286 -286
  185. package/registry/components/field.tsx +283 -283
  186. package/registry/components/hover-card.tsx +48 -48
  187. package/registry/components/inline-citations.css +126 -0
  188. package/registry/components/inline-citations.tsx +292 -0
  189. package/registry/components/input-content.ts +107 -107
  190. package/registry/components/input-group.tsx +130 -130
  191. package/registry/components/input-otp.tsx +584 -584
  192. package/registry/components/input.tsx +990 -972
  193. package/registry/components/item.tsx +189 -189
  194. package/registry/components/kbd.tsx +246 -151
  195. package/registry/components/label.tsx +78 -78
  196. package/registry/components/menu.tsx +978 -986
  197. package/registry/components/menubar.tsx +226 -226
  198. package/registry/components/modal.tsx +872 -872
  199. package/registry/components/native-select.tsx +205 -205
  200. package/registry/components/navigation-menu.tsx +171 -171
  201. package/registry/components/pagination.css +36 -36
  202. package/registry/components/pagination.tsx +560 -555
  203. package/registry/components/popover.tsx +338 -338
  204. package/registry/components/progress-slider.tsx +216 -216
  205. package/registry/components/radio-card.tsx +676 -677
  206. package/registry/components/radio-group.tsx +570 -570
  207. package/registry/components/resizable.tsx +108 -108
  208. package/registry/components/rolling-count.css +37 -0
  209. package/registry/components/rolling-count.tsx +110 -0
  210. package/registry/components/scroll-area.tsx +91 -91
  211. package/registry/components/segmented-control.tsx +87 -87
  212. package/registry/components/select-content.tsx +218 -218
  213. package/registry/components/select.tsx +937 -932
  214. package/registry/components/sheet.tsx +305 -305
  215. package/registry/components/shimmer-text.css +69 -0
  216. package/registry/components/shimmer-text.tsx +43 -0
  217. package/registry/components/sidebar.tsx +1433 -358
  218. package/registry/components/skeleton.css +54 -54
  219. package/registry/components/skeleton.tsx +482 -482
  220. package/registry/components/slider.tsx +380 -380
  221. package/registry/components/social-button.tsx +387 -387
  222. package/registry/components/spinner.tsx +78 -78
  223. package/registry/components/streaming-text.css +45 -0
  224. package/registry/components/streaming-text.tsx +165 -0
  225. package/registry/components/table-elements.tsx +128 -128
  226. package/registry/components/tabs.css +222 -218
  227. package/registry/components/tabs.tsx +241 -241
  228. package/registry/components/tag.tsx +423 -423
  229. package/registry/components/task-list.css +328 -0
  230. package/registry/components/task-list.tsx +429 -0
  231. package/registry/components/textarea.tsx +610 -610
  232. package/registry/components/thinking-reasoning.css +178 -0
  233. package/registry/components/thinking-reasoning.tsx +253 -0
  234. package/registry/components/thinking-state.css +8 -0
  235. package/registry/components/thinking-state.tsx +46 -0
  236. package/registry/components/toast.tsx +136 -136
  237. package/registry/components/toggle.tsx +256 -256
  238. package/registry/components/toolbar.tsx +539 -539
  239. package/registry/components/tooltip.tsx +433 -431
  240. package/registry/examples/agent-input-demo.tsx +90 -0
  241. package/registry/examples/alert-demo.tsx +34 -34
  242. package/registry/examples/alert-dialog-demo.tsx +42 -42
  243. package/registry/examples/approval-card-demo.tsx +83 -0
  244. package/registry/examples/aspect-ratio-demo.tsx +11 -11
  245. package/registry/examples/attribute-demo.tsx +65 -65
  246. package/registry/examples/autocomplete-demo.tsx +109 -109
  247. package/registry/examples/avatar-demo.tsx +132 -132
  248. package/registry/examples/badge-demo.tsx +69 -69
  249. package/registry/examples/button-demo.tsx +86 -83
  250. package/registry/examples/button-group-demo.tsx +63 -63
  251. package/registry/examples/card-demo.tsx +86 -86
  252. package/registry/examples/carousel-demo.tsx +43 -43
  253. package/registry/examples/chart-block-demo.tsx +5 -0
  254. package/registry/examples/chart-demo.tsx +18 -33
  255. package/registry/examples/checkbox-demo.tsx +62 -62
  256. package/registry/examples/collapsible-demo.tsx +16 -16
  257. package/registry/examples/collection-filter-bar-demo.tsx +135 -135
  258. package/registry/examples/command-demo.tsx +42 -42
  259. package/registry/examples/command-palette-demo.tsx +235 -236
  260. package/registry/examples/context-menu-demo.tsx +29 -29
  261. package/registry/examples/control-density-contract-demo.tsx +65 -61
  262. package/registry/examples/date-picker-demo.tsx +127 -127
  263. package/registry/examples/divider-demo.tsx +17 -17
  264. package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -29
  265. package/registry/examples/expanded/banner-demo.tsx +48 -48
  266. package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -59
  267. package/registry/examples/expanded/empty-state-demo.tsx +39 -39
  268. package/registry/examples/expanded/file-upload-demo.tsx +60 -60
  269. package/registry/examples/expanded/onboarding-step-demo.tsx +11 -11
  270. package/registry/examples/expanded/slideout-demo.tsx +15 -15
  271. package/registry/examples/expanded/steps-demo.tsx +29 -29
  272. package/registry/examples/expanded/tabs-demo.tsx +13 -13
  273. package/registry/examples/expanded/timeline-demo.tsx +18 -18
  274. package/registry/examples/field-demo.tsx +51 -51
  275. package/registry/examples/hover-card-demo.tsx +23 -23
  276. package/registry/examples/inline-citations-demo.tsx +31 -0
  277. package/registry/examples/input-actions-demo.tsx +36 -0
  278. package/registry/examples/input-demo.tsx +87 -87
  279. package/registry/examples/input-group-demo.tsx +16 -16
  280. package/registry/examples/input-otp-demo.tsx +72 -72
  281. package/registry/examples/item-demo.tsx +46 -46
  282. package/registry/examples/kbd-demo.tsx +12 -11
  283. package/registry/examples/label-demo.tsx +20 -20
  284. package/registry/examples/menu-demo.tsx +122 -122
  285. package/registry/examples/menubar-demo.tsx +34 -34
  286. package/registry/examples/modal-demo.tsx +74 -74
  287. package/registry/examples/native-select-demo.tsx +16 -16
  288. package/registry/examples/navigation-menu-demo.tsx +29 -29
  289. package/registry/examples/pagination-demo.tsx +20 -20
  290. package/registry/examples/popover-demo.tsx +547 -546
  291. package/registry/examples/progress-demo.tsx +37 -37
  292. package/registry/examples/progress-slider-demo.tsx +29 -29
  293. package/registry/examples/radio-card-demo.tsx +51 -51
  294. package/registry/examples/radio-group-demo.tsx +62 -62
  295. package/registry/examples/resizable-demo.tsx +22 -22
  296. package/registry/examples/results-toolbar-demo.tsx +44 -44
  297. package/registry/examples/rolling-count-demo.tsx +39 -0
  298. package/registry/examples/scroll-area-demo.tsx +15 -15
  299. package/registry/examples/segmented-control-demo.tsx +39 -39
  300. package/registry/examples/select-demo.tsx +87 -87
  301. package/registry/examples/sheet-demo.tsx +47 -47
  302. package/registry/examples/shimmer-text-demo.tsx +20 -0
  303. package/registry/examples/sidebar-demo.tsx +101 -57
  304. package/registry/examples/slider-demo.tsx +31 -31
  305. package/registry/examples/social-button-demo.tsx +61 -61
  306. package/registry/examples/spinner-demo.tsx +59 -59
  307. package/registry/examples/streaming-text-demo.tsx +28 -0
  308. package/registry/examples/tabs-demo.tsx +13 -13
  309. package/registry/examples/tag-demo.tsx +29 -29
  310. package/registry/examples/task-list-demo.tsx +74 -0
  311. package/registry/examples/textarea-demo.tsx +79 -79
  312. package/registry/examples/thinking-reasoning-demo.tsx +74 -0
  313. package/registry/examples/thinking-state-demo.tsx +13 -0
  314. package/registry/examples/toast-demo.tsx +35 -35
  315. package/registry/examples/toggle-demo.tsx +59 -59
  316. package/registry/examples/tooltip-demo.tsx +115 -115
  317. package/registry/guidance/USAGE.md +27 -27
  318. package/registry/guidance/charts.LICENSE.txt +24 -0
  319. package/registry/hooks/use-controllable-state.ts +33 -0
  320. package/registry/hooks/use-direction.ts +207 -207
  321. package/registry/hooks/use-listbox-navigation.ts +73 -0
  322. package/registry/hooks/use-motion-end.ts +55 -0
  323. package/registry/hooks/use-reduced-motion.ts +28 -0
  324. package/registry/index.json +1634 -184
  325. package/registry/styles/globals.css +1462 -1570
  326. package/registry.json +364 -10
  327. package/schema/config.schema.json +51 -51
  328. package/schema/registry.schema.json +194 -190
  329. package/schema/tokens.schema.json +167 -163
  330. package/src/cli/command-line.js +194 -194
  331. package/src/cli/config-editors.js +9 -9
  332. package/src/cli/doctor.js +399 -399
  333. package/src/cli/file-transaction.js +302 -295
  334. package/src/cli/global-css.js +153 -153
  335. package/src/cli/index.js +1002 -1009
  336. package/src/cli/install-plan.js +592 -592
  337. package/src/cli/install-receipt.js +171 -171
  338. package/src/cli/jsonc.js +203 -203
  339. package/src/cli/package-install-state.js +254 -254
  340. package/src/cli/package-manager.js +238 -238
  341. package/src/cli/package-plan.js +185 -185
  342. package/src/cli/package-version.js +97 -97
  343. package/src/cli/registry-query.js +207 -207
  344. package/src/cli/reporting.js +55 -55
  345. package/src/cli/schema-validation.js +19 -19
  346. package/src/cli/source-scanner.js +686 -686
  347. package/src/cli/vite-config.js +478 -478
  348. package/tokens/banhaten.tokens.json +458 -224
@@ -0,0 +1,552 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ BookOpenIcon,
6
+ CheckIcon,
7
+ ImageIcon,
8
+ PaperclipIcon,
9
+ PlusIcon,
10
+ SlashIcon,
11
+ } from "lucide-react"
12
+
13
+ import { Button } from "@/components/ui/button"
14
+ import {
15
+ Menu,
16
+ MenuContent,
17
+ MenuItem,
18
+ MenuItemDescription,
19
+ MenuItemIcon,
20
+ MenuItemText,
21
+ MenuItemTitle,
22
+ MenuLabel,
23
+ MenuSeparator,
24
+ MenuSub,
25
+ MenuSubContent,
26
+ MenuSubTrigger,
27
+ MenuTrigger,
28
+ } from "@/components/ui/menu"
29
+ import { Tag } from "@/components/ui/tag"
30
+ import { useListboxNavigation } from "@/hooks/use-listbox-navigation"
31
+ import { useMotionEnd, waitForMotion } from "@/hooks/use-motion-end"
32
+ import { useReducedMotion } from "@/hooks/use-reduced-motion"
33
+
34
+ import {
35
+ findSlashQuery,
36
+ matchSkills,
37
+ settleLeavingChip,
38
+ withLeavingChips,
39
+ type AgentInputLabels,
40
+ type AgentInputModel,
41
+ type AgentInputPhase,
42
+ type AgentInputSkill,
43
+ type AttachmentKind,
44
+ type Chip,
45
+ type ChipEntry,
46
+ type LeavingChip,
47
+ type SlashToken,
48
+ } from "./agent-input-state"
49
+
50
+ /* ------------------------------------------------------------------ Chips */
51
+
52
+ /**
53
+ * Wraps one chip. A removed chip keeps its slot until its exit animation
54
+ * ends, then reports `onExited`; without a running animation it reports at
55
+ * once, so a removed chip never lingers.
56
+ */
57
+ function ChipSlot({
58
+ children,
59
+ exiting,
60
+ onExited,
61
+ }: {
62
+ children: React.ReactNode
63
+ exiting: boolean
64
+ onExited: () => void
65
+ }) {
66
+ const ref = React.useRef<HTMLSpanElement>(null)
67
+ useMotionEnd(ref, exiting, onExited)
68
+
69
+ return (
70
+ <span data-exit={exiting ? "true" : "false"} data-slot="agent-input-chip" ref={ref}>
71
+ {children}
72
+ </span>
73
+ )
74
+ }
75
+
76
+ /**
77
+ * The chips of the current value, plus copies of removed ones kept where they
78
+ * were until their exit animation ends. The value drops a chip at once and
79
+ * only its exit lingers, so quick removals and host updates in the meantime
80
+ * are never undone.
81
+ */
82
+ function useChipPresence(live: readonly Chip[]) {
83
+ const [leaving, setLeaving] = React.useState<readonly LeavingChip[]>([])
84
+ const liveKeys = new Set(live.map((chip) => chip.key))
85
+
86
+ // A chip added back while its old copy was still leaving replaces it.
87
+ if (leaving.some(({ chip }) => liveKeys.has(chip.key))) {
88
+ setLeaving(leaving.filter(({ chip }) => !liveKeys.has(chip.key)))
89
+ }
90
+
91
+ const chips = withLeavingChips(live, leaving)
92
+
93
+ /** Keeps a live chip on screen while it leaves; returns it, or null. */
94
+ const leave = (key: string) => {
95
+ const index = chips.findIndex(({ chip }) => chip.key === key)
96
+ const entry = chips[index]
97
+ if (!entry || entry.exiting) return null
98
+ setLeaving((current) => [...current, { chip: entry.chip, index }])
99
+ return entry.chip
100
+ }
101
+
102
+ const settle = (key: string) => setLeaving((current) => settleLeavingChip(current, key))
103
+
104
+ return { chips, leave, settle }
105
+ }
106
+
107
+ type ChipRowProps = {
108
+ chips: readonly ChipEntry[]
109
+ disabled: boolean
110
+ labels: AgentInputLabels
111
+ onExited: (key: string) => void
112
+ onRemove: (key: string) => void
113
+ skillName: (id: string) => string
114
+ }
115
+
116
+ function ChipRow({ chips, disabled, labels, onExited, onRemove, skillName }: ChipRowProps) {
117
+ if (chips.length === 0) return null
118
+
119
+ return (
120
+ <div data-slot="agent-input-chips">
121
+ {chips.map(({ chip, exiting }) => (
122
+ <ChipSlot exiting={exiting} key={chip.key} onExited={() => onExited(chip.key)}>
123
+ {chip.kind === "skill" ? (
124
+ <Tag
125
+ closeLabel={labels.removeSkill(skillName(chip.skillId))}
126
+ disabled={disabled}
127
+ icon={<SlashIcon />}
128
+ onClose={() => onRemove(chip.key)}
129
+ showCloseButton
130
+ size="xs"
131
+ state="active"
132
+ type="icon"
133
+ >
134
+ {skillName(chip.skillId)}
135
+ </Tag>
136
+ ) : (
137
+ <Tag
138
+ closeLabel={labels.removeAttachment(chip.attachment.name)}
139
+ disabled={disabled}
140
+ icon={chip.attachment.kind === "image" ? <ImageIcon /> : <PaperclipIcon />}
141
+ onClose={() => onRemove(chip.key)}
142
+ showCloseButton
143
+ size="xs"
144
+ type="icon"
145
+ >
146
+ {chip.attachment.name}
147
+ </Tag>
148
+ )}
149
+ </ChipSlot>
150
+ ))}
151
+ </div>
152
+ )
153
+ }
154
+
155
+ /* ---------------------------------------------------------- Slash palette */
156
+
157
+ /**
158
+ * Opens when a "/" sits before the caret, filters skills by what follows it,
159
+ * and remembers that token so picking a skill can take it out of the text.
160
+ */
161
+ function useSlashPalette(skills: readonly AgentInputSkill[]) {
162
+ const [open, setOpen] = React.useState(false)
163
+ const [query, setQuery] = React.useState("")
164
+ // True while the arrows drive the list, so a resting pointer does not steal
165
+ // the active option.
166
+ const [keyboard, setKeyboard] = React.useState(false)
167
+ const token = React.useRef<SlashToken | null>(null)
168
+ const id = React.useId()
169
+ const results = React.useMemo(() => matchSkills(skills, query), [skills, query])
170
+ const nav = useListboxNavigation({ idPrefix: id, open, options: results })
171
+
172
+ const close = () => {
173
+ setOpen(false)
174
+ setQuery("")
175
+ setKeyboard(false)
176
+ token.current = null
177
+ }
178
+
179
+ const detect = (text: string, caret: number | null) => {
180
+ if (caret === null || skills.length === 0) return close()
181
+ const found = findSlashQuery(text, caret)
182
+ if (!found) return close()
183
+ token.current = { end: found.end, start: found.start }
184
+ // A new query starts again from its first match.
185
+ if (found.query !== query) nav.setActiveIndex(0)
186
+ setQuery(found.query)
187
+ setOpen(true)
188
+ }
189
+
190
+ /** Moves, picks, or closes from the keyboard; returns whether it used the key. */
191
+ const handleKey = (
192
+ event: React.KeyboardEvent,
193
+ pick: (skill: AgentInputSkill) => void
194
+ ) => {
195
+ if (!open) return false
196
+ if (results.length > 0 && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
197
+ event.preventDefault()
198
+ setKeyboard(true)
199
+ nav.move(event.key === "ArrowDown" ? 1 : -1)
200
+ return true
201
+ }
202
+ if (results.length > 0 && (event.key === "Enter" || event.key === "Tab")) {
203
+ event.preventDefault()
204
+ pick(results[Math.max(nav.activeIndex, 0)])
205
+ return true
206
+ }
207
+ if (event.key === "Escape") {
208
+ event.preventDefault()
209
+ close()
210
+ return true
211
+ }
212
+ return false
213
+ }
214
+
215
+ return { close, detect, handleKey, id, keyboard, nav, open, results, setKeyboard, token }
216
+ }
217
+
218
+ type SlashPaletteState = ReturnType<typeof useSlashPalette>
219
+
220
+ function SlashPalette({
221
+ labels,
222
+ onPick,
223
+ slash,
224
+ }: {
225
+ labels: AgentInputLabels
226
+ onPick: (skill: AgentInputSkill) => void
227
+ slash: SlashPaletteState
228
+ }) {
229
+ return (
230
+ <div
231
+ aria-label={labels.skills}
232
+ data-keyboard={slash.keyboard ? "true" : "false"}
233
+ data-slot="agent-input-palette"
234
+ id={slash.id}
235
+ onMouseMove={() => {
236
+ if (slash.keyboard) slash.setKeyboard(false)
237
+ }}
238
+ role="listbox"
239
+ >
240
+ <div aria-hidden="true" data-slot="agent-input-palette-label">
241
+ {labels.skills}
242
+ </div>
243
+ {slash.results.length > 0 ? (
244
+ slash.results.map((skill, index) => (
245
+ <button
246
+ aria-selected={index === slash.nav.activeIndex}
247
+ data-active={index === slash.nav.activeIndex ? "true" : "false"}
248
+ data-slot="agent-input-palette-item"
249
+ id={slash.nav.optionId(index)}
250
+ key={skill.id}
251
+ onClick={() => onPick(skill)}
252
+ onMouseDown={(event) => event.preventDefault()}
253
+ onMouseEnter={() => {
254
+ if (!slash.keyboard) slash.nav.setActiveIndex(index)
255
+ }}
256
+ role="option"
257
+ tabIndex={-1}
258
+ type="button"
259
+ >
260
+ {skill.icon ? <span data-slot="agent-input-palette-icon">{skill.icon}</span> : null}
261
+ <span data-slot="agent-input-palette-name" dir="auto">
262
+ {skill.name}
263
+ </span>
264
+ </button>
265
+ ))
266
+ ) : (
267
+ <div data-slot="agent-input-palette-empty">{labels.noSkills}</div>
268
+ )}
269
+ </div>
270
+ )
271
+ }
272
+
273
+ /* ------------------------------------------------------------ Enhancement */
274
+
275
+ /**
276
+ * Rewrites the prompt through `onEnhance`: a failure restores the original,
277
+ * revert brings it back, and unmounting aborts the request. The frame eases
278
+ * between its old and new height when the text changes size.
279
+ */
280
+ function usePromptEnhancement({
281
+ frameRef,
282
+ onEnhance,
283
+ onFinish,
284
+ onStart,
285
+ setValue,
286
+ value,
287
+ }: {
288
+ frameRef: React.RefObject<HTMLDivElement | null>
289
+ onEnhance?: (prompt: string, signal: AbortSignal) => Promise<string>
290
+ /** Runs once a request settles, unless the field unmounted meanwhile. */
291
+ onFinish: () => void
292
+ onStart: () => void
293
+ setValue: (value: string) => void
294
+ value: string
295
+ }) {
296
+ const [phase, setPhase] = React.useState<AgentInputPhase>("idle")
297
+ const original = React.useRef("")
298
+ const request = React.useRef<AbortController | null>(null)
299
+ const flipFrom = React.useRef<number | null>(null)
300
+ const reducedMotion = useReducedMotion()
301
+ const enhancing = phase === "enhancing"
302
+
303
+ React.useLayoutEffect(() => {
304
+ const frame = frameRef.current
305
+ const from = flipFrom.current
306
+ flipFrom.current = null
307
+ if (!frame || from === null || enhancing || reducedMotion) return
308
+ const to = frame.offsetHeight
309
+ if (from === to) return
310
+ frame.style.height = `${from}px`
311
+ frame.style.overflow = "hidden"
312
+ void frame.offsetHeight
313
+ frame.style.transition = "height var(--bh-duration-default) var(--bh-easing-emphasized)"
314
+ frame.style.height = `${to}px`
315
+ // Never cancelled: however the next render goes, the frame must drop
316
+ // its fixed height once the transition is over.
317
+ waitForMotion(frame, () => {
318
+ frame.style.transition = ""
319
+ frame.style.height = ""
320
+ frame.style.overflow = ""
321
+ })
322
+ }, [enhancing, frameRef, phase, reducedMotion])
323
+
324
+ React.useEffect(() => () => request.current?.abort(), [])
325
+
326
+ const run = async () => {
327
+ if (!onEnhance || !value.trim() || enhancing) return
328
+ original.current = value
329
+ setPhase("enhancing")
330
+ onStart()
331
+ const controller = new AbortController()
332
+ request.current = controller
333
+ try {
334
+ const result = await onEnhance(value, controller.signal)
335
+ if (controller.signal.aborted) return
336
+ flipFrom.current = frameRef.current?.offsetHeight ?? null
337
+ setValue(result)
338
+ setPhase("enhanced")
339
+ } catch {
340
+ if (controller.signal.aborted) return
341
+ setValue(original.current)
342
+ setPhase("idle")
343
+ }
344
+ onFinish()
345
+ }
346
+
347
+ const revert = () => {
348
+ request.current?.abort()
349
+ flipFrom.current = frameRef.current?.offsetHeight ?? null
350
+ setValue(original.current)
351
+ setPhase("idle")
352
+ }
353
+
354
+ /** Editing or sending leaves the enhanced prompt behind. */
355
+ const reset = () => setPhase("idle")
356
+
357
+ return { enhancing, phase, reset, revert, run }
358
+ }
359
+
360
+ /** Keeps the enhance pill mounted through its exit animation. */
361
+ function useEnhancePill(visible: boolean, enhancing: boolean) {
362
+ const ref = React.useRef<HTMLButtonElement>(null)
363
+ const [mounted, setMounted] = React.useState(false)
364
+ const [exiting, setExiting] = React.useState(false)
365
+
366
+ React.useEffect(() => {
367
+ if (visible) {
368
+ setMounted(true)
369
+ setExiting(false)
370
+ return
371
+ }
372
+ if (!mounted) return
373
+ // The spinner takes its place at once while a request runs.
374
+ if (enhancing) {
375
+ setMounted(false)
376
+ setExiting(false)
377
+ return
378
+ }
379
+ setExiting(true)
380
+ }, [enhancing, mounted, visible])
381
+
382
+ useMotionEnd(ref, exiting, () => {
383
+ setMounted(false)
384
+ setExiting(false)
385
+ })
386
+
387
+ return { exiting, mounted, ref }
388
+ }
389
+
390
+ /* --------------------------------------------------------------- Add menu */
391
+
392
+ /** A hidden multi-file input that the add menu opens as a photo or file picker. */
393
+ function useFilePicker(onPick: (files: File[], picker: AttachmentKind) => void) {
394
+ const ref = React.useRef<HTMLInputElement>(null)
395
+
396
+ const open = (kind: AttachmentKind) => {
397
+ const input = ref.current
398
+ if (!input) return
399
+ input.accept = kind === "image" ? "image/*" : ""
400
+ input.dataset.kind = kind
401
+ input.value = ""
402
+ input.click()
403
+ }
404
+
405
+ const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
406
+ const files = Array.from(event.target.files ?? [])
407
+ if (files.length === 0) return
408
+ onPick(files, (event.target.dataset.kind as AttachmentKind | undefined) ?? "file")
409
+ event.target.value = ""
410
+ }
411
+
412
+ return {
413
+ inputProps: { hidden: true, multiple: true, onChange, ref, tabIndex: -1, type: "file" },
414
+ open,
415
+ }
416
+ }
417
+
418
+ type AddMenuProps = {
419
+ disabled: boolean
420
+ labels: AgentInputLabels
421
+ model?: string
422
+ models: readonly AgentInputModel[]
423
+ onModelChange: (model: string) => void
424
+ onPickFiles: (kind: AttachmentKind) => void
425
+ onToggleSkill: (skillId: string) => void
426
+ selectedSkills: readonly string[]
427
+ skills: readonly AgentInputSkill[]
428
+ }
429
+
430
+ /** Photos, files, skills, and the model, behind the plus button. */
431
+ function AddMenu({
432
+ disabled,
433
+ labels,
434
+ model,
435
+ models,
436
+ onModelChange,
437
+ onPickFiles,
438
+ onToggleSkill,
439
+ selectedSkills,
440
+ skills,
441
+ }: AddMenuProps) {
442
+ const [open, setOpen] = React.useState(false)
443
+
444
+ return (
445
+ <Menu onOpenChange={setOpen} open={open}>
446
+ <MenuTrigger asChild>
447
+ <Button
448
+ aria-label={labels.addMenu}
449
+ data-open={open ? "true" : "false"}
450
+ data-slot="agent-input-add"
451
+ disabled={disabled}
452
+ size="icon-xs"
453
+ variant="soft"
454
+ >
455
+ <PlusIcon data-slot="agent-input-add-icon" />
456
+ </Button>
457
+ </MenuTrigger>
458
+ <MenuContent align="start" side="top">
459
+ <MenuItem onSelect={() => onPickFiles("image")}>
460
+ <MenuItemIcon>
461
+ <ImageIcon />
462
+ </MenuItemIcon>
463
+ {labels.addPhotos}
464
+ </MenuItem>
465
+ <MenuItem onSelect={() => onPickFiles("file")}>
466
+ <MenuItemIcon>
467
+ <PaperclipIcon />
468
+ </MenuItemIcon>
469
+ {labels.addFiles}
470
+ </MenuItem>
471
+ {skills.length > 0 ? (
472
+ <>
473
+ <MenuSeparator />
474
+ <MenuSub>
475
+ <MenuSubTrigger>
476
+ <MenuItemIcon>
477
+ <BookOpenIcon />
478
+ </MenuItemIcon>
479
+ {labels.skills}
480
+ </MenuSubTrigger>
481
+ <MenuSubContent>
482
+ {skills.map((skill) => {
483
+ const selected = selectedSkills.includes(skill.id)
484
+ return (
485
+ <MenuItem
486
+ aria-checked={selected}
487
+ key={skill.id}
488
+ onSelect={() => onToggleSkill(skill.id)}
489
+ role="menuitemcheckbox"
490
+ >
491
+ {skill.icon ? <MenuItemIcon>{skill.icon}</MenuItemIcon> : null}
492
+ {skill.name}
493
+ {selected ? (
494
+ <MenuItemIcon position="trailing">
495
+ <CheckIcon />
496
+ </MenuItemIcon>
497
+ ) : null}
498
+ </MenuItem>
499
+ )
500
+ })}
501
+ </MenuSubContent>
502
+ </MenuSub>
503
+ </>
504
+ ) : null}
505
+ {models.length > 0 ? (
506
+ <>
507
+ <MenuSeparator />
508
+ <MenuLabel label={labels.model} />
509
+ {models.map((item) => {
510
+ const selected = item.id === model
511
+ return (
512
+ <MenuItem
513
+ aria-checked={selected}
514
+ key={item.id}
515
+ kind={item.description ? "multiline" : "default"}
516
+ onSelect={() => onModelChange(item.id)}
517
+ role="menuitemcheckbox"
518
+ >
519
+ {item.icon ? <MenuItemIcon>{item.icon}</MenuItemIcon> : null}
520
+ <MenuItemText>
521
+ <MenuItemTitle>{item.name}</MenuItemTitle>
522
+ {item.description ? (
523
+ <MenuItemDescription>
524
+ {item.meta ? `${item.description} · ${item.meta}` : item.description}
525
+ </MenuItemDescription>
526
+ ) : null}
527
+ </MenuItemText>
528
+ {selected ? (
529
+ <MenuItemIcon position="trailing">
530
+ <CheckIcon />
531
+ </MenuItemIcon>
532
+ ) : null}
533
+ </MenuItem>
534
+ )
535
+ })}
536
+ </>
537
+ ) : null}
538
+ </MenuContent>
539
+ </Menu>
540
+ )
541
+ }
542
+
543
+ export {
544
+ AddMenu,
545
+ ChipRow,
546
+ SlashPalette,
547
+ useChipPresence,
548
+ useEnhancePill,
549
+ useFilePicker,
550
+ usePromptEnhancement,
551
+ useSlashPalette,
552
+ }
@@ -0,0 +1,156 @@
1
+ import type * as React from "react"
2
+
3
+ type AgentInputPhase = "idle" | "enhancing" | "enhanced"
4
+
5
+ type AgentInputModel = {
6
+ description?: string
7
+ icon?: React.ReactNode
8
+ id: string
9
+ /** Secondary detail such as the context window. */
10
+ meta?: string
11
+ name: string
12
+ }
13
+
14
+ type AgentInputSkill = {
15
+ icon?: React.ReactNode
16
+ id: string
17
+ name: string
18
+ }
19
+
20
+ type AgentInputAttachment = {
21
+ /** The picked file to upload. Attachments you pass in yourself may omit it. */
22
+ file?: File
23
+ id: string
24
+ kind: "image" | "file"
25
+ name: string
26
+ }
27
+
28
+ type AgentInputPayload = {
29
+ attachments: readonly AgentInputAttachment[]
30
+ model?: string
31
+ skills: readonly string[]
32
+ text: string
33
+ }
34
+
35
+ type AgentInputLabels = {
36
+ addFiles: string
37
+ addMenu: string
38
+ addPhotos: string
39
+ enhance: string
40
+ enhancing: string
41
+ model: string
42
+ noSkills: string
43
+ placeholder: string
44
+ removeAttachment: (name: string) => string
45
+ removeSkill: (name: string) => string
46
+ revert: string
47
+ send: string
48
+ skills: string
49
+ }
50
+
51
+ type AttachmentKind = AgentInputAttachment["kind"]
52
+
53
+ /**
54
+ * A file's own type decides its kind; the picker it came from only decides
55
+ * for files the browser could not type.
56
+ */
57
+ function attachmentKind(file: File, picker: AttachmentKind): AttachmentKind {
58
+ if (file.type.startsWith("image/")) return "image"
59
+ return file.type ? "file" : picker
60
+ }
61
+
62
+ /* ---------------------------------------------------------- Slash tokens */
63
+
64
+ type SlashToken = { end: number; start: number }
65
+
66
+ const slashPattern = /(?:^|\s)\/([^\s/]*)$/
67
+
68
+ /** The "/query" typed right before the caret, if there is one. */
69
+ function findSlashQuery(text: string, caret: number) {
70
+ const before = text.slice(0, caret)
71
+ const match = before.match(slashPattern)
72
+ if (!match) return null
73
+ const query = match[1]
74
+ return { end: caret, query, start: before.length - query.length - 1 }
75
+ }
76
+
77
+ /** Skills whose name contains the query, ignoring case. */
78
+ function matchSkills(skills: readonly AgentInputSkill[], query: string) {
79
+ const needle = query.toLowerCase()
80
+ return skills.filter((skill) => skill.name.toLowerCase().includes(needle))
81
+ }
82
+
83
+ /**
84
+ * The text without a slash token, and without the spaces it leaves at the end,
85
+ * including the no-break space some keyboards type.
86
+ */
87
+ function withoutSlashToken(text: string, token: SlashToken) {
88
+ return (text.slice(0, token.start) + text.slice(token.end)).replace(/[  ]+$/, "")
89
+ }
90
+
91
+ /* ------------------------------------------------------------------ Chips */
92
+
93
+ type Chip =
94
+ | { key: string; kind: "skill"; skillId: string }
95
+ | { attachment: AgentInputAttachment; key: string; kind: "attachment" }
96
+
97
+ type ChipEntry = { chip: Chip; exiting: boolean }
98
+
99
+ /** A removed chip's copy and the index it had when it left. */
100
+ type LeavingChip = { chip: Chip; index: number }
101
+
102
+ /** Skill chips first, then attachments, keyed so each keeps its identity. */
103
+ function chipsOf(
104
+ skillIds: readonly string[],
105
+ attachments: readonly AgentInputAttachment[]
106
+ ): Chip[] {
107
+ return [
108
+ ...skillIds.map((skillId): Chip => ({ key: `skill-${skillId}`, kind: "skill", skillId })),
109
+ ...attachments.map(
110
+ (attachment): Chip => ({ attachment, key: `attachment-${attachment.id}`, kind: "attachment" })
111
+ ),
112
+ ]
113
+ }
114
+
115
+ /** The live chips with leaving copies spliced back in where they were. */
116
+ function withLeavingChips(live: readonly Chip[], leaving: readonly LeavingChip[]) {
117
+ const liveKeys = new Set(live.map((chip) => chip.key))
118
+ const chips: ChipEntry[] = live.map((chip) => ({ chip, exiting: false }))
119
+ for (const { chip, index } of [...leaving].sort((a, b) => a.index - b.index)) {
120
+ if (liveKeys.has(chip.key)) continue
121
+ chips.splice(Math.min(index, chips.length), 0, { chip, exiting: true })
122
+ }
123
+ return chips
124
+ }
125
+
126
+ /** Drops a copy whose exit ended; later copies close the gap it leaves. */
127
+ function settleLeavingChip(leaving: readonly LeavingChip[], key: string) {
128
+ const settled = leaving.find(({ chip }) => chip.key === key)
129
+ if (!settled) return leaving
130
+ return leaving
131
+ .filter((entry) => entry !== settled)
132
+ .map((entry) => (entry.index > settled.index ? { ...entry, index: entry.index - 1 } : entry))
133
+ }
134
+
135
+ export {
136
+ attachmentKind,
137
+ chipsOf,
138
+ findSlashQuery,
139
+ matchSkills,
140
+ settleLeavingChip,
141
+ withLeavingChips,
142
+ withoutSlashToken,
143
+ }
144
+ export type {
145
+ AgentInputAttachment,
146
+ AgentInputLabels,
147
+ AgentInputModel,
148
+ AgentInputPayload,
149
+ AgentInputPhase,
150
+ AgentInputSkill,
151
+ AttachmentKind,
152
+ Chip,
153
+ ChipEntry,
154
+ LeavingChip,
155
+ SlashToken,
156
+ }