@justanarthur/modo-ui 0.5.0 → 0.6.0-canary.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 (461) hide show
  1. package/DESIGN.md +608 -0
  2. package/README.md +61 -39
  3. package/blocks/sidebar-app/examples.mdx +33 -0
  4. package/blocks/sidebar-app/index.tsx +218 -0
  5. package/blocks/sidebar-app/inset-topbar.tsx +25 -0
  6. package/blocks/sidebar-app/menu-grid.ts +26 -0
  7. package/blocks/sidebar-app/nav-data.ts +36 -0
  8. package/blocks/sidebar-app/search-field.tsx +48 -0
  9. package/blocks/sidebar-app/user-footer.tsx +67 -0
  10. package/blocks/sidebar-app/workspace-header.tsx +129 -0
  11. package/components/AI/input-message/examples.mdx +124 -0
  12. package/components/AI/input-message/file-thumbnail.tsx +173 -0
  13. package/components/AI/input-message/index.tsx +1383 -0
  14. package/components/AI/thinking-indicator/examples.mdx +27 -0
  15. package/components/AI/thinking-indicator/index.tsx +210 -0
  16. package/components/accordion/examples.mdx +148 -0
  17. package/components/accordion/index.tsx +1034 -0
  18. package/components/avatar/examples.mdx +95 -0
  19. package/components/avatar/index.tsx +129 -0
  20. package/components/badge/change-demo.tsx +29 -0
  21. package/components/badge/examples.mdx +120 -0
  22. package/components/badge/index.tsx +225 -0
  23. package/components/badge/variants.ts +73 -0
  24. package/components/button/examples.mdx +164 -0
  25. package/components/button/index.tsx +416 -0
  26. package/components/button/loading-demo.tsx +25 -0
  27. package/components/button/variants.ts +166 -0
  28. package/components/card/examples.mdx +212 -0
  29. package/components/card/index.tsx +1037 -0
  30. package/components/copy/examples.mdx +87 -0
  31. package/components/copy/index.tsx +482 -0
  32. package/components/inputs/checkbox/examples.mdx +83 -0
  33. package/components/inputs/checkbox/index.tsx +611 -0
  34. package/components/inputs/checkbox/select-all-demo.tsx +48 -0
  35. package/components/inputs/color-picker/examples.mdx +72 -0
  36. package/components/inputs/color-picker/index.tsx +1955 -0
  37. package/components/inputs/color-picker/variants.ts +401 -0
  38. package/components/inputs/combobox/examples.mdx +273 -0
  39. package/components/inputs/combobox/index.tsx +1436 -0
  40. package/components/inputs/combobox/remote-demo.tsx +101 -0
  41. package/components/inputs/date-time/calendar/examples.mdx +34 -0
  42. package/components/inputs/date-time/calendar/index.tsx +351 -0
  43. package/components/inputs/date-time/calendar/use-month-window.ts +163 -0
  44. package/components/inputs/date-time/date-picker/examples.mdx +166 -0
  45. package/components/inputs/date-time/date-picker/index.tsx +385 -0
  46. package/components/inputs/date-time/date-picker/masked-date-input.tsx +336 -0
  47. package/components/inputs/date-time/date-range-picker/examples.mdx +29 -0
  48. package/components/inputs/date-time/date-range-picker/index.tsx +217 -0
  49. package/components/inputs/date-time/duration-picker/examples.mdx +16 -0
  50. package/components/inputs/date-time/duration-picker/index.tsx +189 -0
  51. package/components/inputs/date-time/month-picker/examples.mdx +16 -0
  52. package/components/inputs/date-time/month-picker/index.tsx +190 -0
  53. package/components/inputs/date-time/picker-shell.tsx +395 -0
  54. package/components/inputs/date-time/time-columns.tsx +91 -0
  55. package/components/inputs/date-time/time-picker/examples.mdx +26 -0
  56. package/components/inputs/date-time/time-picker/index.tsx +166 -0
  57. package/components/inputs/field/examples.mdx +113 -0
  58. package/components/inputs/field/index.tsx +267 -0
  59. package/components/inputs/file-dropzone/examples.mdx +73 -0
  60. package/components/inputs/file-dropzone/file-row.tsx +201 -0
  61. package/components/inputs/file-dropzone/index.tsx +296 -0
  62. package/components/inputs/file-dropzone/upload-demo.tsx +67 -0
  63. package/components/inputs/file-dropzone/variants.ts +94 -0
  64. package/components/inputs/input/examples.mdx +135 -0
  65. package/components/inputs/input/index.tsx +494 -0
  66. package/components/inputs/input/password-demo.tsx +30 -0
  67. package/components/inputs/label/examples.mdx +67 -0
  68. package/components/inputs/label/index.tsx +85 -0
  69. package/components/inputs/number-input/examples.mdx +56 -0
  70. package/components/inputs/number-input/index.tsx +214 -0
  71. package/components/inputs/radio/examples.mdx +77 -0
  72. package/components/inputs/radio/index.tsx +636 -0
  73. package/components/inputs/search/examples.mdx +74 -0
  74. package/components/inputs/search/index.tsx +333 -0
  75. package/components/inputs/select/examples.mdx +218 -0
  76. package/components/inputs/select/index.tsx +862 -0
  77. package/components/inputs/select/variants.ts +32 -0
  78. package/components/inputs/slider/examples.mdx +127 -0
  79. package/components/inputs/slider/index.tsx +1725 -0
  80. package/components/inputs/switch/examples.mdx +94 -0
  81. package/components/inputs/switch/index.tsx +504 -0
  82. package/components/inputs/textarea/examples.mdx +88 -0
  83. package/components/inputs/textarea/index.tsx +161 -0
  84. package/components/navigation/navigation-menu/examples.mdx +109 -0
  85. package/components/navigation/navigation-menu/index.tsx +613 -0
  86. package/components/navigation/pagination/examples.mdx +64 -0
  87. package/components/navigation/pagination/index.tsx +395 -0
  88. package/components/navigation/sidebar/examples.mdx +319 -0
  89. package/components/navigation/sidebar/index.tsx +453 -0
  90. package/components/navigation/sidebar/sidebar-core.tsx +1408 -0
  91. package/components/navigation/sidebar/sidebar-menu.tsx +1495 -0
  92. package/components/navigation/sidebar/sidebar.css +74 -0
  93. package/components/navigation/sidebar/variants.ts +55 -0
  94. package/components/navigation/tabs/examples.mdx +89 -0
  95. package/components/navigation/tabs/index.tsx +494 -0
  96. package/components/navigation/tabs-subtle/examples.mdx +49 -0
  97. package/components/navigation/tabs-subtle/index.tsx +512 -0
  98. package/components/overlays/alert-dialog/examples.mdx +123 -0
  99. package/components/overlays/alert-dialog/index.tsx +258 -0
  100. package/components/overlays/alert-dialog/pending-demo.tsx +36 -0
  101. package/components/overlays/command/examples.mdx +113 -0
  102. package/components/overlays/command/index.tsx +416 -0
  103. package/components/overlays/context-menu/examples.mdx +56 -0
  104. package/components/overlays/context-menu/index.tsx +279 -0
  105. package/components/overlays/dialog/examples.mdx +144 -0
  106. package/components/overlays/dialog/index.tsx +366 -0
  107. package/components/overlays/dropdown/dropdown-search.tsx +395 -0
  108. package/components/overlays/dropdown/examples.mdx +294 -0
  109. package/components/overlays/dropdown/index.tsx +1168 -0
  110. package/components/overlays/dropdown/menu-item.tsx +464 -0
  111. package/components/overlays/dropdown/menu-parts.tsx +660 -0
  112. package/components/overlays/popover/anchored-demo.tsx +59 -0
  113. package/components/overlays/popover/examples.mdx +180 -0
  114. package/components/overlays/popover/index.tsx +315 -0
  115. package/components/overlays/preview-card/examples.mdx +61 -0
  116. package/components/overlays/preview-card/index.tsx +204 -0
  117. package/components/overlays/sheet/examples.mdx +89 -0
  118. package/components/overlays/sheet/index.tsx +247 -0
  119. package/components/overlays/toast/examples.mdx +68 -0
  120. package/components/overlays/toast/global-demo.tsx +72 -0
  121. package/components/overlays/toast/index.tsx +474 -0
  122. package/components/overlays/toast/manager.ts +193 -0
  123. package/components/overlays/tooltip/examples.mdx +106 -0
  124. package/components/overlays/tooltip/index.tsx +519 -0
  125. package/components/progress/examples.mdx +59 -0
  126. package/components/progress/index.tsx +174 -0
  127. package/components/progress/progress.css +49 -0
  128. package/components/progress/step-demo.tsx +27 -0
  129. package/components/table/examples.mdx +184 -0
  130. package/components/table/index.tsx +510 -0
  131. package/components/table/sort-demo.tsx +61 -0
  132. package/dist/base.css +100 -0
  133. package/dist/blocks/sidebar-app/index.js +39 -26
  134. package/dist/components/AI/input-message/index.js +22 -16
  135. package/dist/components/AI/thinking-indicator/index.js +9 -6
  136. package/dist/components/accordion/index.js +16 -13
  137. package/dist/components/avatar/index.d.ts +46 -0
  138. package/dist/components/avatar/index.js +18 -0
  139. package/dist/components/badge/index.d.ts +18 -11
  140. package/dist/components/badge/index.js +11 -7
  141. package/dist/components/badge/variants.d.ts +33 -0
  142. package/dist/components/badge/variants.js +10 -0
  143. package/dist/components/button/index.d.ts +53 -17
  144. package/dist/components/button/index.js +10 -6
  145. package/dist/components/button/variants.d.ts +39 -0
  146. package/dist/components/button/variants.js +8 -0
  147. package/dist/components/card/index.d.ts +16 -3
  148. package/dist/components/card/index.js +16 -11
  149. package/dist/components/copy/index.js +13 -9
  150. package/dist/components/inputs/checkbox/index.d.ts +28 -9
  151. package/dist/components/inputs/checkbox/index.js +17 -13
  152. package/dist/components/inputs/color-picker/index.d.ts +13 -13
  153. package/dist/components/inputs/color-picker/index.js +25 -18
  154. package/dist/components/inputs/color-picker/variants.d.ts +61 -0
  155. package/dist/components/inputs/color-picker/variants.js +28 -0
  156. package/dist/components/inputs/combobox/index.d.ts +60 -17
  157. package/dist/components/inputs/combobox/index.js +26 -18
  158. package/dist/components/inputs/date-time/calendar/index.js +14 -11
  159. package/dist/components/inputs/date-time/date-picker/index.d.ts +53 -11
  160. package/dist/components/inputs/date-time/date-picker/index.js +33 -25
  161. package/dist/components/inputs/date-time/date-range-picker/index.d.ts +3 -3
  162. package/dist/components/inputs/date-time/date-range-picker/index.js +32 -24
  163. package/dist/components/inputs/date-time/duration-picker/index.d.ts +3 -3
  164. package/dist/components/inputs/date-time/duration-picker/index.js +32 -24
  165. package/dist/components/inputs/date-time/month-picker/index.d.ts +3 -3
  166. package/dist/components/inputs/date-time/month-picker/index.js +31 -23
  167. package/dist/components/inputs/date-time/time-picker/index.d.ts +3 -3
  168. package/dist/components/inputs/date-time/time-picker/index.js +32 -24
  169. package/dist/components/inputs/field/index.d.ts +116 -0
  170. package/dist/components/inputs/field/index.js +28 -0
  171. package/dist/components/inputs/file-dropzone/index.d.ts +165 -0
  172. package/dist/components/inputs/file-dropzone/index.js +32 -0
  173. package/dist/components/inputs/file-dropzone/variants.d.ts +35 -0
  174. package/dist/components/inputs/file-dropzone/variants.js +12 -0
  175. package/dist/components/inputs/input/index.d.ts +95 -13
  176. package/dist/components/inputs/input/index.js +17 -9
  177. package/dist/components/inputs/label/index.d.ts +32 -0
  178. package/dist/components/inputs/label/index.js +11 -0
  179. package/dist/components/inputs/number-input/index.d.ts +84 -0
  180. package/dist/components/inputs/number-input/index.js +20 -0
  181. package/dist/components/inputs/radio/index.d.ts +20 -5
  182. package/dist/components/inputs/radio/index.js +15 -12
  183. package/dist/components/inputs/search/index.js +25 -18
  184. package/dist/components/inputs/select/index.d.ts +41 -11
  185. package/dist/components/inputs/select/index.js +27 -14
  186. package/dist/components/inputs/select/variants.d.ts +5 -0
  187. package/dist/components/inputs/select/variants.js +6 -0
  188. package/dist/components/inputs/slider/index.js +7 -5
  189. package/dist/components/inputs/switch/index.d.ts +8 -2
  190. package/dist/components/inputs/switch/index.js +14 -12
  191. package/dist/components/inputs/textarea/index.d.ts +4 -0
  192. package/dist/components/inputs/textarea/index.js +8 -6
  193. package/dist/components/navigation/navigation-menu/index.d.ts +114 -0
  194. package/dist/components/navigation/navigation-menu/index.js +40 -0
  195. package/dist/components/navigation/pagination/index.d.ts +73 -0
  196. package/dist/components/navigation/pagination/index.js +39 -0
  197. package/dist/components/navigation/sidebar/index.d.ts +44 -21
  198. package/dist/components/navigation/sidebar/index.js +41 -31
  199. package/dist/components/navigation/sidebar/variants.d.ts +25 -0
  200. package/dist/components/navigation/sidebar/variants.js +28 -0
  201. package/dist/components/navigation/tabs/index.d.ts +8 -2
  202. package/dist/components/navigation/tabs/index.js +14 -10
  203. package/dist/components/navigation/tabs-subtle/index.js +12 -10
  204. package/dist/components/overlays/alert-dialog/index.d.ts +121 -15
  205. package/dist/components/overlays/alert-dialog/index.js +22 -13
  206. package/dist/components/overlays/command/index.js +24 -17
  207. package/dist/components/overlays/context-menu/index.d.ts +33 -17
  208. package/dist/components/overlays/context-menu/index.js +27 -18
  209. package/dist/components/overlays/dialog/index.js +17 -12
  210. package/dist/components/overlays/dropdown/index.d.ts +257 -31
  211. package/dist/components/overlays/dropdown/index.js +42 -17
  212. package/dist/components/overlays/popover/index.d.ts +31 -9
  213. package/dist/components/overlays/popover/index.js +12 -9
  214. package/dist/components/overlays/preview-card/index.js +11 -8
  215. package/dist/components/overlays/sheet/index.js +18 -13
  216. package/dist/components/overlays/toast/index.d.ts +123 -47
  217. package/dist/components/overlays/toast/index.js +18 -10
  218. package/dist/components/overlays/tooltip/index.d.ts +113 -21
  219. package/dist/components/overlays/tooltip/index.js +15 -6
  220. package/dist/components/progress/index.d.ts +56 -0
  221. package/dist/components/progress/index.js +15 -0
  222. package/dist/components/table/index.d.ts +63 -8
  223. package/dist/components/table/index.js +19 -9
  224. package/dist/index.d.ts +2201 -736
  225. package/dist/index.js +223 -82
  226. package/dist/lib/date-locale.d.ts +14 -0
  227. package/dist/lib/date-locale.js +16 -0
  228. package/dist/lib/field-classes.d.ts +144 -0
  229. package/dist/lib/field-classes.js +40 -0
  230. package/dist/lib/field-variants.d.ts +47 -0
  231. package/dist/lib/field-variants.js +26 -0
  232. package/dist/lib/fluid-hover-highlight.d.ts +112 -0
  233. package/dist/lib/fluid-hover-highlight.js +17 -0
  234. package/dist/lib/focus-ring.d.ts +18 -0
  235. package/dist/lib/focus-ring.js +10 -0
  236. package/dist/lib/goo-indicator.d.ts +46 -0
  237. package/dist/lib/goo-indicator.js +14 -0
  238. package/dist/lib/icon-context.d.ts +30 -0
  239. package/dist/lib/icon-context.js +12 -0
  240. package/dist/lib/inert.d.ts +9 -0
  241. package/dist/lib/inert.js +8 -0
  242. package/dist/lib/morph-layers.d.ts +93 -0
  243. package/dist/lib/morph-layers.js +13 -0
  244. package/dist/lib/morph-part.d.ts +21 -0
  245. package/dist/lib/morph-part.js +12 -0
  246. package/dist/lib/popup.d.ts +20 -0
  247. package/dist/lib/popup.js +12 -0
  248. package/dist/lib/reduced-motion.d.ts +9 -0
  249. package/dist/lib/reduced-motion.js +6 -0
  250. package/dist/lib/row-index.d.ts +10 -0
  251. package/dist/lib/row-index.js +8 -0
  252. package/dist/lib/row-order.d.ts +28 -0
  253. package/dist/lib/row-order.js +10 -0
  254. package/dist/lib/scroll-column.d.ts +27 -0
  255. package/dist/lib/scroll-column.js +18 -0
  256. package/dist/lib/shape-context.d.ts +30 -0
  257. package/dist/lib/shape-context.js +12 -0
  258. package/dist/lib/size-context.d.ts +94 -0
  259. package/dist/lib/size-context.js +20 -0
  260. package/dist/lib/size-scale.d.ts +79 -0
  261. package/dist/lib/size-scale.js +8 -0
  262. package/dist/lib/slot.d.ts +12 -0
  263. package/dist/lib/slot.js +6 -0
  264. package/dist/lib/springs.d.ts +44 -0
  265. package/dist/lib/springs.js +8 -0
  266. package/dist/lib/surface-classes.d.ts +19 -0
  267. package/dist/lib/surface-classes.js +10 -0
  268. package/dist/lib/surface-context.d.ts +9 -0
  269. package/dist/lib/surface-context.js +8 -0
  270. package/dist/lib/use-controllable-state.d.ts +2 -0
  271. package/dist/lib/use-controllable-state.js +6 -0
  272. package/dist/lib/use-fluid-hover.d.ts +138 -0
  273. package/dist/lib/use-fluid-hover.js +14 -0
  274. package/dist/lib/use-is-mobile.d.ts +2 -0
  275. package/dist/lib/use-is-mobile.js +6 -0
  276. package/dist/lib/use-keyboard-nav-gate.d.ts +20 -0
  277. package/dist/lib/use-keyboard-nav-gate.js +7 -0
  278. package/dist/lib/use-merge-split.d.ts +47 -0
  279. package/dist/lib/use-merge-split.js +15 -0
  280. package/dist/lib/use-morph.d.ts +101 -0
  281. package/dist/lib/use-morph.js +20 -0
  282. package/dist/lib/use-touch-primary.d.ts +2 -0
  283. package/dist/lib/use-touch-primary.js +6 -0
  284. package/dist/lib/utils.d.ts +3 -0
  285. package/dist/lib/utils.js +6 -0
  286. package/dist/preset.d.ts +17 -0
  287. package/dist/preset.js +22 -2
  288. package/dist/primitives/code/index.js +7 -5
  289. package/dist/primitives/craft/index.js +9 -6
  290. package/dist/primitives/fluid-hover/index.js +9 -8
  291. package/dist/primitives/morph/index.js +15 -10
  292. package/dist/primitives/motion/index.js +10 -8
  293. package/dist/primitives/scroll-area/index.js +4 -3
  294. package/dist/primitives/separator/index.d.ts +25 -0
  295. package/dist/primitives/separator/index.js +9 -0
  296. package/dist/primitives/sizes/index.js +6 -4
  297. package/dist/primitives/skeleton/index.d.ts +35 -0
  298. package/dist/primitives/skeleton/index.js +12 -0
  299. package/dist/primitives/surface/index.js +6 -4
  300. package/dist/shared/{components-3mkepka5.js → components-0jmqhx2k.js} +28 -25
  301. package/dist/shared/components-124epb7f.js +53 -0
  302. package/dist/shared/{components-gz8zf3ec.js → components-1k0wv53d.js} +15 -43
  303. package/dist/shared/components-26snnbs9.js +322 -0
  304. package/dist/shared/{components-2dxkv6zm.js → components-2c0qcfx4.js} +8 -7
  305. package/dist/shared/{components-473g814w.js → components-2mt9te6d.js} +243 -137
  306. package/dist/shared/{components-hawvy2qz.js → components-2z48g4fp.js} +9 -30
  307. package/dist/shared/components-34txk9rj.js +35 -0
  308. package/dist/shared/components-3h92d94m.js +23 -0
  309. package/dist/shared/{components-zfnmn5d4.js → components-3sp623ae.js} +36 -31
  310. package/dist/shared/{components-jwst3vtj.js → components-3wxkjgqn.js} +186 -76
  311. package/dist/shared/{components-s2bz1xpk.js → components-49bsynbw.js} +8 -5
  312. package/dist/shared/components-4bwsd46w.js +71 -0
  313. package/dist/shared/components-4n4f5m3e.js +297 -0
  314. package/dist/shared/{components-0q0sqfh4.js → components-4y816hwp.js} +27 -49
  315. package/dist/shared/components-52ngsgca.js +399 -0
  316. package/dist/shared/{components-y9y0g8er.js → components-52r5763g.js} +22 -19
  317. package/dist/shared/components-559tf7yb.js +28 -0
  318. package/dist/shared/components-5mkga9hj.js +440 -0
  319. package/dist/shared/{components-kwa6mrf9.js → components-5s0nfqvw.js} +14 -9
  320. package/dist/shared/{components-dk0gjr5a.js → components-60atskhp.js} +19 -18
  321. package/dist/shared/{components-rr7a6ja9.js → components-658b26nb.js} +21 -18
  322. package/dist/shared/{components-xxvdw7k1.js → components-65vwfjs6.js} +37 -34
  323. package/dist/shared/{components-frw34b65.js → components-6j2fh74k.js} +114 -123
  324. package/dist/shared/components-6zg1yft7.js +167 -0
  325. package/dist/shared/components-6zng858q.js +53 -0
  326. package/dist/shared/components-72w4ge68.js +64 -0
  327. package/dist/shared/{components-cv16mryv.js → components-78x40gee.js} +13 -7
  328. package/dist/shared/{components-b2wyq2cq.js → components-7d4r7hh1.js} +39 -57
  329. package/dist/shared/components-7ntvvegr.js +316 -0
  330. package/dist/shared/components-8nwsqjn8.js +34 -0
  331. package/dist/shared/{components-5xc2j8jz.js → components-8tt3xbb5.js} +19 -18
  332. package/dist/shared/{components-vc7vq5nz.js → components-9as3rf68.js} +6 -3
  333. package/dist/shared/components-9mys9jna.js +255 -0
  334. package/dist/shared/components-adkarj8n.js +35 -0
  335. package/dist/shared/{components-b3c5et5b.js → components-ae9fncz4.js} +32 -31
  336. package/dist/shared/{components-hqeq3w9q.js → components-awra5sff.js} +24 -16
  337. package/dist/shared/components-b1e239g3.js +28 -0
  338. package/dist/shared/{components-q5xj5c16.js → components-b1f2bjf1.js} +9 -8
  339. package/dist/shared/{components-qmmjx1qw.js → components-bn42vq6d.js} +71 -72
  340. package/dist/shared/components-c96g82th.js +1444 -0
  341. package/dist/shared/components-c9yr4ed1.js +44 -0
  342. package/dist/shared/components-d3shbwqq.js +95 -0
  343. package/dist/shared/{components-jjw69c4c.js → components-dcc02hs2.js} +7 -4
  344. package/dist/shared/{components-men63rwj.js → components-de99ykt7.js} +3 -2
  345. package/dist/shared/components-dhgk6mbt.js +16 -0
  346. package/dist/shared/components-drxrtnp2.js +25 -0
  347. package/dist/shared/{components-6p8tfcd6.js → components-ds9fqat9.js} +21 -18
  348. package/dist/shared/{components-mjthm1kf.js → components-egy4tyjt.js} +11 -8
  349. package/dist/shared/components-f0xfpkhh.js +82 -0
  350. package/dist/shared/components-f7h57m6t.js +59 -0
  351. package/dist/shared/{components-d6xtjht3.js → components-fjkpbg6n.js} +33 -30
  352. package/dist/shared/{components-gc5vtq2f.js → components-gh0qeey3.js} +63 -29
  353. package/dist/shared/{components-39jq9va1.js → components-gvbapfen.js} +29 -24
  354. package/dist/shared/{components-5vgpnxgy.js → components-gzesg7hy.js} +19 -16
  355. package/dist/shared/components-h6p86be3.js +19 -0
  356. package/dist/shared/components-h83az0bz.js +33 -0
  357. package/dist/shared/components-jjrj6ymn.js +104 -0
  358. package/dist/shared/{components-wvp1m52g.js → components-jp5v0m0f.js} +15 -14
  359. package/dist/shared/{components-zrkzcayz.js → components-jqpefjsn.js} +54 -6
  360. package/dist/shared/{components-1z6tdadk.js → components-k74j4hwh.js} +20 -17
  361. package/dist/shared/components-kswxnm3h.js +24 -0
  362. package/dist/shared/{components-jbbmdr74.js → components-mkkbbgb7.js} +36 -17
  363. package/dist/shared/{components-pbmqctfa.js → components-mpw667rb.js} +169 -65
  364. package/dist/shared/{components-s5ngg32c.js → components-paa08427.js} +121 -47
  365. package/dist/shared/{components-vfbhxf5r.js → components-pd42emw3.js} +6 -5
  366. package/dist/shared/{components-hw9yk32z.js → components-pmfszv7a.js} +35 -23
  367. package/dist/shared/{components-chacnk61.js → components-pw06rbxq.js} +19 -19
  368. package/dist/shared/components-q03cktx8.js +246 -0
  369. package/dist/shared/{components-kvr7f9hy.js → components-qdm3yh26.js} +19 -10
  370. package/dist/shared/{components-27nnvrwe.js → components-qs4rkqsj.js} +15 -14
  371. package/dist/shared/{components-s083f38z.js → components-r6gbmkew.js} +22 -21
  372. package/dist/shared/{components-ta7shw28.js → components-r8eqs7gz.js} +4 -4
  373. package/dist/shared/{components-sn1rxx1d.js → components-rm02h9nt.js} +12 -11
  374. package/dist/shared/{components-c6s6b43g.js → components-rrbpmfx4.js} +3 -2
  375. package/dist/shared/{components-n7mjr357.js → components-tz1fv35x.js} +3 -2
  376. package/dist/shared/components-tz4gaf1v.js +23 -0
  377. package/dist/shared/{components-zx6b6yby.js → components-vf45j12c.js} +4 -18
  378. package/dist/shared/{components-3ky44908.js → components-vfjc9jzz.js} +2 -3
  379. package/dist/shared/{components-39vmfq9s.js → components-vsa625vk.js} +21 -20
  380. package/dist/shared/components-wawnmkph.js +500 -0
  381. package/dist/shared/components-wktrrb4m.js +38 -0
  382. package/dist/shared/{components-8snj490h.js → components-wwd363me.js} +78 -32
  383. package/dist/shared/{components-5pnw8183.js → components-xcwvgcrd.js} +23 -22
  384. package/dist/shared/components-xdqmt6ed.js +20 -0
  385. package/dist/shared/{components-h2ajmhwn.js → components-y1w3a6wx.js} +107 -35
  386. package/dist/shared/{components-ar66gnt0.js → components-y6578b8h.js} +37 -34
  387. package/dist/shared/{components-59efxjd9.js → components-yhgq0qxx.js} +13 -12
  388. package/dist/shared/{components-jthe1pwb.js → components-yz84gpkq.js} +101 -404
  389. package/dist/shared/components-z3v0n0de.js +11 -0
  390. package/dist/shared/{components-7r3etddy.js → components-z9n19fyk.js} +19 -54
  391. package/dist/shared/components-zbvjzh1p.js +141 -0
  392. package/dist/styles.css +241 -16
  393. package/dist/tailwind.css +13 -0
  394. package/dist/tokens.css +34 -0
  395. package/index.ts +60 -0
  396. package/lib/date-locale.ts +50 -0
  397. package/lib/field-classes.ts +115 -0
  398. package/lib/field-variants.ts +117 -0
  399. package/lib/fluid-hover-highlight.tsx +139 -0
  400. package/lib/focus-ring.tsx +46 -0
  401. package/lib/goo-indicator.tsx +261 -0
  402. package/lib/icon-context.tsx +310 -0
  403. package/lib/inert.ts +22 -0
  404. package/lib/morph-layers.tsx +102 -0
  405. package/lib/morph-part.tsx +49 -0
  406. package/lib/popup.ts +52 -0
  407. package/lib/reduced-motion.ts +19 -0
  408. package/lib/row-index.tsx +29 -0
  409. package/lib/row-order.tsx +115 -0
  410. package/lib/scroll-column.tsx +244 -0
  411. package/lib/shape-context.tsx +124 -0
  412. package/lib/size-context.tsx +66 -0
  413. package/lib/size-scale.ts +115 -0
  414. package/lib/slot.ts +25 -0
  415. package/lib/springs.ts +62 -0
  416. package/lib/surface-classes.ts +58 -0
  417. package/lib/surface-context.tsx +21 -0
  418. package/lib/use-controllable-state.ts +44 -0
  419. package/lib/use-fluid-hover.ts +541 -0
  420. package/lib/use-is-mobile.ts +25 -0
  421. package/lib/use-keyboard-nav-gate.ts +45 -0
  422. package/lib/use-merge-split.tsx +209 -0
  423. package/lib/use-morph.ts +557 -0
  424. package/lib/use-touch-primary.tsx +43 -0
  425. package/lib/utils.ts +37 -0
  426. package/package.json +177 -4
  427. package/preset.ts +196 -0
  428. package/primitives/code/examples.mdx +27 -0
  429. package/primitives/code/index.tsx +80 -0
  430. package/primitives/craft/demos.tsx +173 -0
  431. package/primitives/craft/examples.mdx +138 -0
  432. package/primitives/craft/index.tsx +76 -0
  433. package/primitives/fluid-hover/examples.mdx +84 -0
  434. package/primitives/fluid-hover/index.tsx +316 -0
  435. package/primitives/morph/examples.mdx +135 -0
  436. package/primitives/morph/index.tsx +192 -0
  437. package/primitives/motion/examples.mdx +100 -0
  438. package/primitives/motion/index.tsx +160 -0
  439. package/primitives/scroll-area/examples.mdx +58 -0
  440. package/primitives/scroll-area/index.tsx +200 -0
  441. package/primitives/separator/examples.mdx +30 -0
  442. package/primitives/separator/index.tsx +53 -0
  443. package/primitives/sizes/examples.mdx +167 -0
  444. package/primitives/sizes/index.tsx +89 -0
  445. package/primitives/skeleton/examples.mdx +51 -0
  446. package/primitives/skeleton/index.tsx +72 -0
  447. package/primitives/skeleton/skeleton.css +15 -0
  448. package/primitives/surface/examples.mdx +173 -0
  449. package/primitives/surface/index.tsx +109 -0
  450. package/primitives/surface/surface.css +176 -0
  451. package/dist/shared/components-0sn7ypjw.js +0 -61
  452. package/dist/shared/components-1434p1mh.js +0 -123
  453. package/dist/shared/components-a86dytt0.js +0 -986
  454. package/dist/shared/components-gf5s6ap9.js +0 -42
  455. package/dist/shared/components-hyq23phv.js +0 -310
  456. package/dist/shared/components-ng72k97e.js +0 -128
  457. package/dist/shared/components-raknhppb.js +0 -64
  458. package/dist/shared/components-vynjtrk6.js +0 -19
  459. package/dist/shared/components-wy9jpqny.js +0 -146
  460. /package/dist/shared/{components-1v7rxytx.js → components-3ey0hzpb.js} +0 -0
  461. /package/dist/shared/{components-ezxhrmer.js → components-jcnjay5g.js} +0 -0
@@ -0,0 +1,1383 @@
1
+ // biome-ignore-all lint: vendored upstream code keeps its own patterns (see the header)
2
+ /*
3
+ * Ported from Fluid Functionalism — github.com/mickadesign/fluid-functionalism
4
+ * `registry/default/input-message.tsx` @ b3587bdbd83fc66c2a6aae3817ffb856cb09260b
5
+ * MIT License © 2026 Micka Touillaud — fluidfunctionalism.com (notice: LICENSE.fluid-functionalism)
6
+ * Local modifications:
7
+ * - `"use client"` directive dropped (no RSC here).
8
+ * - `framer-motion` → `motion/react`; `@/lib/*` and `@/hooks/*` rewritten to
9
+ * `../../../lib/*` (`SizeProvider` → `../../../primitives/sizes`); `@/components/ui/fluid-hover-highlight` →
10
+ * `../../../lib/fluid-hover-highlight`; `@/registry/radix/{button,tooltip}` →
11
+ * `../../button` / `../../overlays/tooltip`; `@/registry/default/file-thumbnail` → `./file-thumbnail`
12
+ * (vendored beside this file).
13
+ * - Uncontrolled twins (Base UI shape, via `lib/use-controllable-state`), so the
14
+ * composer works with no state in the caller. The controlled API is unchanged — every
15
+ * `value` / `files` / `queue` / `status` prop still wins and still reports through its
16
+ * callback:
17
+ * · `value` / `onValueChange` are now optional and gain `defaultValue`;
18
+ * · `defaultFiles` seeds an internal file list — attachments are enabled by
19
+ * `onFilesChange` OR `defaultFiles` (upstream: `onFilesChange` alone);
20
+ * · `defaultQueue` seeds an internal queue — the queue is enabled by
21
+ * `onQueueChange` OR `defaultQueue`, still only alongside a status;
22
+ * · `defaultStatus` seeds an internal status (plus an optional `onStatusChange`);
23
+ * with the status uncontrolled the Stop control is offered even without
24
+ * `onStop` and flips the status to `"idle"` itself, which is the edge that
25
+ * auto-dispatches the head of the queue;
26
+ * · an uncontrolled draft / file list clears itself after `onSend`, which a
27
+ * controlled consumer does by hand.
28
+ * - React 18 / `noUncheckedIndexedAccess`: `useRef<HTMLDivElement | null>` where the ref is
29
+ * assigned, guards on `queue[0]`, `history[i]`, `suggestions[i]` and the swap in
30
+ * `moveQueued`, and `FilePreviewTile` turned into a `forwardRef` — it renders inside
31
+ * `<AnimatePresence mode="popLayout">`, which measures the exiting child through a ref
32
+ * (React 19 passes `ref` as a plain prop; React 18 warns and drops it).
33
+ * - modo item: TSDoc from the FF "InputMessage" docs page (its Playground section is
34
+ * skipped, and the transcript around the Attachments / Send Handler demos is dropped —
35
+ * `ChatMessage` is not part of this port).
36
+ * - Styling reads DS tokens (AGENTS.md styling): `text-[Npx]` →
37
+ * `text-<role>[-compact]`; `leading-[18px]` → `leading-4.5`; inline
38
+ * `fontVariationSettings` → `weight-*`; the hex focus-ring fallback →
39
+ * `ring-focus-ring` / `border-focus-ring`; literal colors → color tokens;
40
+ * `rounded-[Npx]` → radius tokens; `duration-80|120|160` and tier-length JS
41
+ * durations → `duration-<tier>` / `spring.*`.
42
+ */
43
+
44
+ import { AnimatePresence, motion, Reorder, useReducedMotion } from 'motion/react'
45
+ import {
46
+ type ChangeEvent,
47
+ forwardRef,
48
+ type HTMLAttributes,
49
+ type DragEvent as ReactDragEvent,
50
+ type KeyboardEvent as ReactKeyboardEvent,
51
+ type MouseEvent as ReactMouseEvent,
52
+ type ReactNode,
53
+ type TextareaHTMLAttributes,
54
+ useCallback,
55
+ useEffect,
56
+ useId,
57
+ useLayoutEffect,
58
+ useMemo,
59
+ useRef,
60
+ useState,
61
+ } from 'react'
62
+ import { FluidHoverHighlight } from '../../../lib/fluid-hover-highlight'
63
+ import { useIcon } from '../../../lib/icon-context'
64
+ import { useShape } from '../../../lib/shape-context'
65
+ import { type SizeVariant, useSize } from '../../../lib/size-context'
66
+ import { spring } from '../../../lib/springs'
67
+ import { surfaceClasses } from '../../../lib/surface-classes'
68
+ import { SurfaceProvider } from '../../../lib/surface-context'
69
+ import { useControllableState } from '../../../lib/use-controllable-state'
70
+ import { useFluidHover, useRegisterFluidHoverItem } from '../../../lib/use-fluid-hover'
71
+ import { cn } from '../../../lib/utils'
72
+ import { SizeProvider } from '../../../primitives/sizes'
73
+ import { Button } from '../../button'
74
+ import { Tooltip } from '../../overlays/tooltip'
75
+ import { FileThumbnail } from './file-thumbnail'
76
+
77
+ const useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
78
+
79
+ /**
80
+ * Measured layout height for one of the composer's collapsible regions
81
+ * (attachments, queue, suggestions).
82
+ *
83
+ * These animate to a self-measured PIXEL height rather than `height: "auto"`:
84
+ * motion resolves an "auto" target from the element's *visual* (transformed)
85
+ * size, so under a scaled ancestor the region springs out to scale× its real
86
+ * height and snaps back when "auto" lands, which reads as the region
87
+ * ballooning and then correcting. Same treatment as Accordion's content
88
+ * height.
89
+ *
90
+ * Returns a ref for the region's CONTENT element. The height it reports is the
91
+ * clipping parent's scrollHeight, so inner margins count (the parent's
92
+ * overflow-hidden makes it a block formatting context, so they don't collapse
93
+ * out) and the value stays correct while the animated height is mid-flight.
94
+ * Observing the child rather than the parent keeps the ResizeObserver out of a
95
+ * feedback loop with that animation.
96
+ */
97
+ function useRegionHeight() {
98
+ const roRef = useRef<ResizeObserver | null>(null)
99
+ const [height, setHeight] = useState<number | null>(null)
100
+ const ref = useCallback((el: HTMLElement | null) => {
101
+ roRef.current?.disconnect()
102
+ roRef.current = null
103
+ if (!el) return
104
+ const sync = () => {
105
+ const next = el.parentElement?.scrollHeight ?? el.offsetHeight
106
+ if (next > 0) setHeight(next)
107
+ }
108
+ sync()
109
+ const ro = new ResizeObserver(sync)
110
+ ro.observe(el)
111
+ roRef.current = ro
112
+ }, [])
113
+ return [ref, height] as const
114
+ }
115
+
116
+ // Touch devices have no hover, so hover-revealed affordances (like a queued
117
+ // row's × button) would never appear. `(hover: none)` flags those so they can
118
+ // be shown persistently instead. SSR-safe: starts false, resolves on mount.
119
+ function useIsTouch() {
120
+ const [isTouch, setIsTouch] = useState(false)
121
+ useEffect(() => {
122
+ const mq = window.matchMedia('(hover: none)')
123
+ const update = () => setIsTouch(mq.matches)
124
+ update()
125
+ mq.addEventListener('change', update)
126
+ return () => mq.removeEventListener('change', update)
127
+ }, [])
128
+ return isTouch
129
+ }
130
+
131
+ const DEFAULT_ACCEPT = 'image/png,image/jpeg,application/pdf'
132
+
133
+ // Stable empty seeds: `useControllableState` reads its default once, but a
134
+ // fresh literal on every render would still churn the identity it stores.
135
+ const NO_FILES: File[] = []
136
+ const NO_QUEUE: QueuedMessage[] = []
137
+
138
+ type InputMessageStatus = 'idle' | 'streaming'
139
+
140
+ interface InputMessageSlotContext {
141
+ /** Opens the native file picker via the hidden `<input type="file">`.
142
+ * Pass `acceptOverride` (e.g. `"image/*"`) to scope the picker to a
143
+ * subset of the component's accept types just for this invocation. */
144
+ openFilePicker: (acceptOverride?: string) => void
145
+ /** Currently-attached files. */
146
+ files: File[]
147
+ }
148
+
149
+ type InputMessageSlot = ReactNode | ((ctx: InputMessageSlotContext) => ReactNode)
150
+
151
+ /** A message held in the queue while the assistant is responding. Carries the
152
+ * trimmed text plus a snapshot of the files attached when it was queued, so
153
+ * double-click-to-edit can restore both. `id` is a stable key minted on enqueue. */
154
+ interface QueuedMessage {
155
+ id: string
156
+ text: string
157
+ files: File[]
158
+ }
159
+
160
+ interface InputMessageProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'> {
161
+ /** Step on the size ladder (see Sizes). Wins over the surrounding SizeProvider and propagates to the composer's rows, buttons and queued messages. */
162
+ size?: SizeVariant
163
+ /** Controlled textarea value. Pair with `onValueChange`; omit both for an uncontrolled composer. */
164
+ value?: string
165
+ /** Initial textarea value for an uncontrolled composer. Defaults to `""`. */
166
+ defaultValue?: string
167
+ /** Called with the new value on every textarea change. */
168
+ onValueChange?: (value: string) => void
169
+ /** Fires on Enter (without Shift) or send-button click, and when a queued message auto-dispatches. Receives the trimmed value, the attached files, and — for auto-dispatched queue items — `meta.queuedId`. Skipped when the value is empty and no files are attached. */
170
+ onSend?: (value: string, files: File[], meta?: { queuedId?: string }) => void
171
+ /** Placeholder shown when the value is empty. While a file is dragged over the component (and attachments are enabled) it swaps to "Drop files here to add to chat". Defaults to `"Ask me anything…"`. */
172
+ placeholder?: string
173
+ /** Content rendered in the bottom-left action area. May be a render-fn that receives `{ openFilePicker, files }` — `openFilePicker(acceptOverride?)` opens the native file picker. */
174
+ leftSlot?: InputMessageSlot
175
+ /** Content rendered in the bottom-right action area, before the built-in send button. Same render-fn shape as `leftSlot`. */
176
+ rightSlot?: InputMessageSlot
177
+ /** Disables the textarea, send button, and drag-and-drop. Defaults to `false`. */
178
+ disabled?: boolean
179
+ /** Minimum visible rows before the textarea grows. Defaults to 1. */
180
+ minRows?: number
181
+ /** Maximum visible rows before the textarea starts to scroll. Defaults to 8. */
182
+ maxRows?: number
183
+ /** When true, clicking anywhere on the surrounding container (outside of buttons / links / inputs) focuses the textarea. Defaults to `true`. */
184
+ clickToFocus?: boolean
185
+ /** Accessible label for the send button. Defaults to `"Send"`. */
186
+ sendLabel?: string
187
+ /** Controlled list of attached files. Pair with `onFilesChange` to enable drag-and-drop and the file-picker slot helper. */
188
+ files?: File[]
189
+ /** Initial attachments for an uncontrolled composer. Providing it (even as `[]`) enables attachment behavior on its own. */
190
+ defaultFiles?: File[]
191
+ /** Called when files are added (drag-drop or picker) or removed via the preview tile's × button. Duplicate drops of the same file (same name + size + lastModified) are silently de-duplicated. */
192
+ onFilesChange?: (files: File[]) => void
193
+ /** Accepted MIME types as a comma-separated string. Used by both the file picker and the drag-and-drop filter. Defaults to `"image/png,image/jpeg,application/pdf"`. */
194
+ accept?: string
195
+ /** Maximum number of attached files. Extra files beyond this limit are dropped. */
196
+ maxFiles?: number
197
+ /** Side length (in pixels) of each preview tile. Images use object-cover; PDFs render the first page via pdfjs; other types fall back to a centered icon. Defaults to 80. */
198
+ filePreviewSize?: number
199
+ /** Extra props forwarded to the underlying textarea (value, onChange, onKeyDown, disabled and placeholder are controlled by the component). */
200
+ textareaProps?: Omit<
201
+ TextareaHTMLAttributes<HTMLTextAreaElement>,
202
+ 'value' | 'onChange' | 'onKeyDown' | 'disabled' | 'placeholder'
203
+ >
204
+ /** Assistant response state. When `"streaming"`, the send button becomes a Stop control (empty draft) or a Queue action (non-empty draft). On the streaming→idle edge the next queued message auto-dispatches via `onSend`. Leave it and `defaultStatus` undefined for the send-immediately behavior. */
205
+ status?: InputMessageStatus
206
+ /** Initial status for an uncontrolled composer — the component then owns the streaming flag, and Stop flips it to `"idle"` itself. */
207
+ defaultStatus?: InputMessageStatus
208
+ /** Called whenever the status changes while it is uncontrolled (currently: the Stop control). */
209
+ onStatusChange?: (status: InputMessageStatus) => void
210
+ /** Fires when the Stop control is pressed (streaming + empty draft). Halt the current response and set status to `"idle"` — that edge immediately dispatches the next queued message. */
211
+ onStop?: () => void
212
+ /** Controlled queue of pending messages, rendered as reorderable rows above the textarea. Double-click (or Enter/F2) edits a row back into the composer; the × (or Delete) removes it; drag — or Alt+↑/↓ — reorders. Requires a status. */
213
+ queue?: QueuedMessage[]
214
+ /** Initial queue for an uncontrolled composer. Providing it (even as `[]`) enables the queue on its own, alongside `status` or `defaultStatus`. */
215
+ defaultQueue?: QueuedMessage[]
216
+ /** Called whenever the queue changes — enqueue, edit, delete, reorder, or auto-dispatch. Each QueuedMessage is `{ id, text, files }`. */
217
+ onQueueChange?: (queue: QueuedMessage[]) => void
218
+ /** Render the built-in reorderable queue rows above the textarea. Set to false to suppress them and render the queue yourself — enqueue and auto-dispatch still run. Defaults to `true`. */
219
+ showQueue?: boolean
220
+ /** Previously-sent messages, oldest first. With the textarea focused, ArrowUp (caret on the first line) recalls the previous message and walks backward; ArrowDown (caret on the last line) walks forward toward the in-progress draft. Editing or sending exits history mode. Defaults to `[]`. */
221
+ history?: string[]
222
+ /** Suggested prompt rendered as the placeholder (with a Tab keycap) while the draft is empty. Pressing Tab fills it into the composer — it doesn't send. Takes precedence over `placeholder`. */
223
+ placeholderSuggestion?: string
224
+ /** Suggested prompts listed under the action bar while the draft is empty. ArrowDown moves a highlight into the list (focus stays in the textarea), ArrowUp walks back up and out, Enter or click fills the highlighted prompt. Escape drops the highlight; typing collapses the list. */
225
+ suggestions?: string[]
226
+ /** Extra classes for the composer surface. */
227
+ className?: string
228
+ }
229
+
230
+ // ─── File preview tile ────────────────────────────────────────────────────
231
+ // Composer-row tile: a FileThumbnail wrapped with enter/exit motion and a
232
+ // hover-revealed remove (×) button.
233
+ interface FilePreviewTileProps {
234
+ file: File
235
+ onRemove: () => void
236
+ size: number
237
+ }
238
+
239
+ // React 18: the tile is rendered inside `<AnimatePresence mode="popLayout">`,
240
+ // which measures the exiting child through a ref. Upstream can be a plain
241
+ // function component (React 19 passes `ref` as a prop); here it has to be a
242
+ // forwardRef, or React logs "Function components cannot be given refs" and the
243
+ // pop-out measurement never lands.
244
+ const FilePreviewTile = forwardRef<HTMLDivElement, FilePreviewTileProps>(function FilePreviewTile(
245
+ { file, onRemove, size },
246
+ ref,
247
+ ) {
248
+ const XIcon = useIcon('x')
249
+
250
+ return (
251
+ <motion.div
252
+ ref={ref}
253
+ // `layout` animates sibling tiles into the gap when one is removed.
254
+ // Enter: spring.fast (0.08s) — the small-state-flip tier.
255
+ // Exit: 0.06s linear — exits should be slightly faster than enter.
256
+ layout
257
+ initial={{ opacity: 0, scale: 0.9 }}
258
+ animate={{ opacity: 1, scale: 1 }}
259
+ exit={{ opacity: 0, scale: 0.9, transition: spring.fast.exit }}
260
+ transition={spring.fast}
261
+ // `cursor-default` opts out of the parent's `cursor-text` so hovering
262
+ // a preview tile doesn't look like it'll land in the textarea.
263
+ className="relative shrink-0 cursor-default group/tile"
264
+ >
265
+ <FileThumbnail file={file} size={size} />
266
+ <Tooltip content="Remove" side="top">
267
+ <button
268
+ type="button"
269
+ onClick={e => {
270
+ e.stopPropagation()
271
+ onRemove()
272
+ }}
273
+ aria-label={`Remove ${file.name}`}
274
+ // Pinned to the light scheme (`light` class: dark circle + light X)
275
+ // regardless of theme — the close badge needs to read as a "delete
276
+ // affordance" over arbitrary image/PDF content, so it sits at a fixed
277
+ // contrast instead of flipping with the surrounding surface.
278
+ className="light absolute top-1 right-1 w-5 h-5 rounded-full bg-foreground text-background opacity-0 group-hover/tile:opacity-100 transition-opacity duration-fast flex items-center justify-center cursor-pointer outline-none focus-visible:opacity-100 focus-visible:ring-1 focus-visible:ring-focus-ring"
279
+ >
280
+ <XIcon size={12} strokeWidth={2.5} />
281
+ </button>
282
+ </Tooltip>
283
+ </motion.div>
284
+ )
285
+ })
286
+
287
+ // ─── Queued message row ───────────────────────────────────────────────────
288
+ // A pending message in the queue: a recessed, draggable row that reads as
289
+ // "staged, not live". Double-click (or Enter/F2) edits it back into the
290
+ // composer; the hover-revealed × (or Delete) removes it; drag — or Alt+↑/↓ —
291
+ // reorders. Top of the list is next to dispatch.
292
+ interface QueuedRowProps {
293
+ item: QueuedMessage
294
+ index: number
295
+ total: number
296
+ reduceMotion: boolean
297
+ isTouch: boolean
298
+ onEdit: (item: QueuedMessage) => void
299
+ onRemove: (item: QueuedMessage) => void
300
+ onMove: (item: QueuedMessage, dir: -1 | 1) => void
301
+ }
302
+
303
+ function QueuedRow({ item, index, total, reduceMotion, isTouch, onEdit, onRemove, onMove }: QueuedRowProps) {
304
+ const XIcon = useIcon('x')
305
+ const ImageIcon = useIcon('image')
306
+ const compactStep = useSize().variant === 'compact'
307
+ const fileCount = item.files.length
308
+ const label = item.text || `${fileCount} attachment${fileCount === 1 ? '' : 's'}`
309
+
310
+ return (
311
+ <Reorder.Item
312
+ value={item}
313
+ layout
314
+ // Enter: spring-fast chip category. Exit slightly faster (0.06s linear).
315
+ // Reduced-motion drops the scale.
316
+ initial={reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.97 }}
317
+ animate={{ opacity: 1, scale: 1 }}
318
+ exit={reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.97, transition: spring.fast.exit }}
319
+ transition={spring.fast}
320
+ aria-label={`Queued message ${index + 1} of ${total}: ${label}`}
321
+ tabIndex={0}
322
+ onDoubleClick={() => onEdit(item)}
323
+ onKeyDown={e => {
324
+ if (e.key === 'Enter' || e.key === 'F2') {
325
+ e.preventDefault()
326
+ onEdit(item)
327
+ } else if (e.key === 'Delete' || e.key === 'Backspace') {
328
+ e.preventDefault()
329
+ onRemove(item)
330
+ } else if (e.altKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
331
+ e.preventDefault()
332
+ onMove(item, e.key === 'ArrowUp' ? -1 : 1)
333
+ }
334
+ }}
335
+ className={cn(
336
+ // Fixed height (was py-1.5 around a 19.5px line box ≈ 31.5px) so the
337
+ // text-box trim on the label doesn't shrink the row.
338
+ 'group/qrow flex items-center gap-2 rounded-lg bg-muted',
339
+ compactStep ? 'h-7 px-2 text-body-compact' : 'h-8 px-2.5 text-body',
340
+ 'text-foreground/85 select-none outline-none',
341
+ 'cursor-grab active:cursor-grabbing',
342
+ 'focus-visible:ring-1 focus-visible:ring-focus-ring',
343
+ 'weight-normal',
344
+ )}
345
+ >
346
+ {fileCount > 0 && (
347
+ <span className="flex shrink-0 items-center gap-0.5 text-muted-foreground">
348
+ <ImageIcon size={13} />
349
+ {item.text && <span className="tabular-nums">{fileCount}</span>}
350
+ </span>
351
+ )}
352
+ {/* py-1/-my-1 keeps truncate's overflow-hidden from clipping
353
+ ascenders and descenders outside the trimmed box. */}
354
+ <span className="min-w-0 flex-1 truncate [text-box:trim-both_cap_alphabetic] py-1 -my-1">{label}</span>
355
+ <Tooltip content="Remove" side="top">
356
+ <button
357
+ type="button"
358
+ // Stop the pointer-down from starting a Reorder drag, and the click
359
+ // from bubbling to the row's double-click/edit handler.
360
+ onPointerDown={e => e.stopPropagation()}
361
+ onClick={e => {
362
+ e.stopPropagation()
363
+ onRemove(item)
364
+ }}
365
+ aria-label={`Remove queued message: ${label}`}
366
+ className={cn(
367
+ 'shrink-0 flex h-5 w-5 items-center justify-center rounded-full',
368
+ 'text-muted-foreground hover:text-foreground hover:bg-hover',
369
+ // Hover devices reveal × on row-hover; touch has no hover, so keep
370
+ // it persistently visible there.
371
+ isTouch ? 'opacity-100' : 'opacity-0 group-hover/qrow:opacity-100 focus-visible:opacity-100',
372
+ 'transition-opacity duration-fast cursor-pointer outline-none',
373
+ 'focus-visible:ring-1 focus-visible:ring-focus-ring',
374
+ )}
375
+ >
376
+ <XIcon size={13} strokeWidth={2.5} />
377
+ </button>
378
+ </Tooltip>
379
+ </Reorder.Item>
380
+ )
381
+ }
382
+
383
+ // ─── Suggestion row ───────────────────────────────────────────────────────
384
+ // A suggested prompt in the listbox under the action bar. Registers itself
385
+ // with the fluid-hover system in an effect (MenuItem's pattern — an
386
+ // inline ref callback would re-register every render and keep the hook's
387
+ // measurement pass from ever settling). The highlight itself is the parent's
388
+ // sliding overlay, so the row only recolors its text when active.
389
+ interface SuggestionRowProps {
390
+ text: string
391
+ index: number
392
+ active: boolean
393
+ /** Show a ↓ keycap hint in the icon slot — the first row displays it while
394
+ * no row is highlighted, signposting that ArrowDown enters the list. */
395
+ keyHint: boolean
396
+ optionId: string
397
+ registerItem: (index: number, element: HTMLElement | null) => void
398
+ onSelect: () => void
399
+ }
400
+
401
+ function SuggestionRow({ text, index, active, keyHint, optionId, registerItem, onSelect }: SuggestionRowProps) {
402
+ const EnterIcon = useIcon('corner-down-left')
403
+ const ArrowDownIcon = useIcon('arrow-down')
404
+ const compactStep = useSize().variant === 'compact'
405
+ const ref = useRef<HTMLDivElement>(null)
406
+
407
+ useRegisterFluidHoverItem(registerItem, index, ref)
408
+
409
+ return (
410
+ <div
411
+ ref={ref}
412
+ id={optionId}
413
+ role="option"
414
+ aria-selected={active}
415
+ onClick={onSelect}
416
+ className={cn(
417
+ 'relative flex cursor-pointer items-center gap-2',
418
+ // Text size mirrors the composer's textarea/placeholder (the rows
419
+ // read as prompt candidates, not metadata); heights follow the
420
+ // QueuedRow step ladder.
421
+ compactStep ? 'h-7 px-2 text-subtitle-compact' : 'h-8 px-2.5 text-subtitle',
422
+ 'text-muted-foreground transition-colors duration-fast',
423
+ active && 'text-foreground',
424
+ 'weight-normal',
425
+ )}
426
+ >
427
+ <span className="min-w-0 flex-1 truncate [text-box:trim-both_cap_alphabetic] py-1 -my-1">{text}</span>
428
+ {/* One icon slot: ↵ on the highlighted row; on the first row a muted ↓
429
+ takes the same slot while nothing is highlighted, signposting the
430
+ keyboard path into the list. */}
431
+ {!active && keyHint ? (
432
+ <ArrowDownIcon size={13} className="shrink-0 text-muted-foreground/70 transition-opacity duration-fast" />
433
+ ) : (
434
+ <EnterIcon
435
+ size={13}
436
+ className={cn('shrink-0 transition-opacity duration-fast', active ? 'opacity-100' : 'opacity-0')}
437
+ />
438
+ )}
439
+ </div>
440
+ )
441
+ }
442
+
443
+ // ─── InputMessage ─────────────────────────────────────────────────────────
444
+
445
+ /**
446
+ * Chat-style message composer with an auto-resizing textarea, flexible
447
+ * left/right action slots, and a built-in send button on a Surface-2
448
+ * substrate.
449
+ *
450
+ * The textarea grows from `minRows` to `maxRows` and then scrolls, Enter
451
+ * sends while Shift+Enter breaks the line, and the whole panel is one
452
+ * hairline ring that recolours in place for hover, focus and drag instead of
453
+ * layering a second border. Attachments drop anywhere on the composer (or
454
+ * come from `openFilePicker` in a slot) and preview as tiles — images
455
+ * object-cover, PDFs render their first page. `suggestions` open a listbox
456
+ * under the action bar that ↓/↑ walk without moving focus, and
457
+ * `placeholderSuggestion` writes a ghost prompt with a Tab keycap. With a
458
+ * `status` the send button morphs into Stop (empty draft) or Queue (draft
459
+ * typed while streaming); queued rows reorder by drag or Alt+↑/↓, edit back
460
+ * into the composer on double-click, and the head auto-dispatches on the
461
+ * streaming→idle edge.
462
+ *
463
+ * Every controlled prop has an uncontrolled twin — `defaultValue`,
464
+ * `defaultFiles`, `defaultQueue`, `defaultStatus` — so a composer can hold
465
+ * its own draft, attachments and queue with no state in the caller.
466
+ *
467
+ * @example {@include ./examples.mdx}
468
+ */
469
+ const InputMessage = forwardRef<HTMLDivElement, InputMessageProps>(
470
+ (
471
+ {
472
+ size,
473
+ value: valueProp,
474
+ defaultValue,
475
+ onValueChange,
476
+ onSend,
477
+ placeholder = 'Ask me anything…',
478
+ leftSlot,
479
+ rightSlot,
480
+ disabled,
481
+ minRows = 1,
482
+ maxRows = 8,
483
+ clickToFocus = true,
484
+ sendLabel = 'Send',
485
+ files: filesProp,
486
+ defaultFiles,
487
+ onFilesChange,
488
+ accept = DEFAULT_ACCEPT,
489
+ maxFiles,
490
+ filePreviewSize = 80,
491
+ textareaProps,
492
+ status: statusProp,
493
+ defaultStatus,
494
+ onStatusChange,
495
+ onStop,
496
+ queue: queueProp,
497
+ defaultQueue,
498
+ onQueueChange,
499
+ showQueue = true,
500
+ history = [],
501
+ placeholderSuggestion,
502
+ suggestions,
503
+ className,
504
+ style,
505
+ ...props
506
+ },
507
+ ref,
508
+ ) => {
509
+ const shape = useShape()
510
+ const compactStep = useSize(size).variant === 'compact'
511
+ const ArrowUpIcon = useIcon('arrow-up')
512
+ const reduceMotion = useReducedMotion() ?? false
513
+ const isTouch = useIsTouch()
514
+
515
+ const textareaRef = useRef<HTMLTextAreaElement>(null)
516
+ const fileInputRef = useRef<HTMLInputElement>(null)
517
+ const [focusVisible, setFocusVisible] = useState(false)
518
+ const [dragOver, setDragOver] = useState(false)
519
+ const [hovered, setHovered] = useState(false)
520
+
521
+ // Split out onFocus/onBlur so the rest-spread onto the textarea can't
522
+ // clobber the composed handlers below.
523
+ const {
524
+ onFocus: _textareaOnFocus,
525
+ onBlur: _textareaOnBlur,
526
+ 'aria-describedby': textareaDescribedBy,
527
+ ...restTextareaProps
528
+ } = textareaProps ?? {}
529
+
530
+ // ── Controlled / uncontrolled state ───────────────────────────────
531
+ // Each pair keeps upstream's controlled contract (the prop wins, the
532
+ // callback still fires) and gains an internal fallback, so the composer
533
+ // runs with no state in the caller.
534
+ const valueControlled = valueProp !== undefined
535
+ const [value, setValue] = useControllableState<string>(valueProp, defaultValue ?? '', onValueChange)
536
+
537
+ const filesControlled = filesProp !== undefined
538
+ const supportsFiles = onFilesChange !== undefined || defaultFiles !== undefined
539
+ const [filesArr, setFiles] = useControllableState<File[]>(filesProp, defaultFiles ?? NO_FILES, onFilesChange)
540
+
541
+ // Queue is active only when a status exists (controlled or seeded) and a
542
+ // queue is wired — same opt-in shape as `supportsFiles`.
543
+ const statusEnabled = statusProp !== undefined || defaultStatus !== undefined
544
+ const [statusValue, setStatus] = useControllableState<InputMessageStatus>(
545
+ statusProp,
546
+ defaultStatus ?? 'idle',
547
+ onStatusChange,
548
+ )
549
+ const status = statusEnabled ? statusValue : undefined
550
+ const streaming = status === 'streaming'
551
+
552
+ const [queueArr, setQueue] = useControllableState<QueuedMessage[]>(
553
+ queueProp,
554
+ defaultQueue ?? NO_QUEUE,
555
+ onQueueChange,
556
+ )
557
+ // Always-current view of the queue, so enqueue/edit/remove/move read the
558
+ // latest value even if a handler closure is stale (e.g. two submits land
559
+ // before the controlled `queue` prop round-trips back).
560
+ const queueRef = useRef(queueArr)
561
+ queueRef.current = queueArr
562
+ const supportsQueue = statusEnabled && (onQueueChange !== undefined || defaultQueue !== undefined)
563
+ const [liveMsg, setLiveMsg] = useState('')
564
+
565
+ // Sent-message history navigation (readline-style). `historyIndex` is null
566
+ // when not browsing; `draftBeforeHistory` stashes the in-progress text so
567
+ // ArrowDown past the newest entry restores it.
568
+ const [historyIndex, setHistoryIndex] = useState<number | null>(null)
569
+ const draftBeforeHistory = useRef('')
570
+
571
+ // Suggested prompts. The list only shows while the draft is empty, and
572
+ // `activeSuggestion` is the highlighted row — focus never leaves the
573
+ // textarea (aria-activedescendant points at the highlighted option).
574
+ // Highlight state lives in the fluid-hover system so pointer and
575
+ // keyboard drive the same sliding bg-hover overlay (Dropdown's pattern):
576
+ // mouse movement resolves the nearest row, ↓/↑ set the index directly.
577
+ const suggestionsArr = useMemo(() => suggestions ?? [], [suggestions])
578
+ const suggestionsOpen = suggestionsArr.length > 0 && value === ''
579
+ const suggestionListRef = useRef<HTMLDivElement | null>(null)
580
+ // Pixel heights for the three collapsible regions (see useRegionHeight).
581
+ const [filesRegionRef, filesRegionHeight] = useRegionHeight()
582
+ const [queueRegionRef, queueRegionHeight] = useRegionHeight()
583
+ const [suggestionsRegionRef, suggestionsRegionHeight] = useRegionHeight()
584
+ const suggestionHover = useFluidHover(suggestionListRef)
585
+ const {
586
+ activeIndex: activeSuggestion,
587
+ setActiveIndex: setActiveSuggestion,
588
+ handlers: suggestionHandlers,
589
+ registerItem: registerSuggestion,
590
+ remeasure,
591
+ } = suggestionHover
592
+
593
+ // Rows stay registered while the list is closed, so their rects are from
594
+ // a hidden layout: re-measure on open. The highlight waits for it.
595
+ useEffect(() => {
596
+ if (suggestionsOpen) remeasure()
597
+ }, [suggestionsOpen, remeasure])
598
+ const suggestionListId = useId()
599
+ const ghostHintId = useId()
600
+ const showGhost = !!placeholderSuggestion && value === '' && !(dragOver && supportsFiles)
601
+
602
+ useEffect(() => {
603
+ if (!suggestionsOpen) setActiveSuggestion(null)
604
+ }, [suggestionsOpen, setActiveSuggestion])
605
+
606
+ // Fill a suggested prompt into the composer (Tab / Enter / click). Fills
607
+ // only — the user still reviews and sends.
608
+ const acceptSuggestion = useCallback(
609
+ (text: string) => {
610
+ setActiveSuggestion(null)
611
+ setHistoryIndex(null)
612
+ setValue(text)
613
+ requestAnimationFrame(() => {
614
+ const el = textareaRef.current
615
+ if (!el) return
616
+ el.focus()
617
+ el.setSelectionRange(el.value.length, el.value.length)
618
+ })
619
+ },
620
+ [setValue, setActiveSuggestion],
621
+ )
622
+
623
+ // Parsed line-height, cached per textarea element — getComputedStyle on
624
+ // every keystroke is needless work when the value only changes with font
625
+ // or zoom changes.
626
+ const lineHeightCache = useRef<{ el: HTMLTextAreaElement; value: number } | null>(null)
627
+
628
+ const resizeTextarea = useCallback(() => {
629
+ const el = textareaRef.current
630
+ if (!el) return
631
+ el.style.height = 'auto'
632
+ let cache = lineHeightCache.current
633
+ if (!cache || cache.el !== el) {
634
+ const lineHeight = parseFloat(getComputedStyle(el).lineHeight)
635
+ cache = { el, value: Number.isNaN(lineHeight) ? 20 : lineHeight }
636
+ lineHeightCache.current = cache
637
+ }
638
+ const min = cache.value * minRows
639
+ const max = cache.value * maxRows
640
+ const next = Math.min(Math.max(el.scrollHeight, min), max)
641
+ el.style.height = `${next}px`
642
+ el.style.overflowY = el.scrollHeight > max ? 'auto' : 'hidden'
643
+ }, [minRows, maxRows])
644
+
645
+ useIsoLayoutEffect(() => {
646
+ resizeTextarea()
647
+ }, [value, resizeTextarea])
648
+
649
+ // Re-measure when the textarea's width changes. The mount-time pass can
650
+ // run while an ancestor is still laid out at (near-)zero width — the
651
+ // wrapped placeholder then reads as many lines and pins the height at
652
+ // maxRows until the next value change. Width-gated so the observer
653
+ // doesn't loop on its own height writes.
654
+ useEffect(() => {
655
+ const el = textareaRef.current
656
+ if (!el || typeof ResizeObserver === 'undefined') return
657
+ let lastWidth = el.offsetWidth
658
+ const ro = new ResizeObserver(() => {
659
+ const width = el.offsetWidth
660
+ if (width === lastWidth) return
661
+ lastWidth = width
662
+ resizeTextarea()
663
+ })
664
+ ro.observe(el)
665
+ return () => ro.disconnect()
666
+ }, [resizeTextarea])
667
+
668
+ const trimmed = value.trim()
669
+ const canSend = !disabled && (trimmed.length > 0 || filesArr.length > 0)
670
+
671
+ // Edge = the box-shadow's 1px ring, recoloured in place per state so the
672
+ // stroke gains contrast without ever appearing to thicken (no second
673
+ // border band layered beside it). The drop (`0 1px 1px`) is kept so the
674
+ // composer holds its lift across states. Applied inline (not via a
675
+ // `shadow-*` utility, which mangles multi-layer arbitrary values) with the
676
+ // precedence drag > focus > hover; when none are active, the className's
677
+ // `shadow-surface-2` supplies the resting edge.
678
+ const EDGE_DROP = '0 1px 1px -0.5px var(--shadow-color)'
679
+ const edgeShadow = dragOver
680
+ ? `0 0 0 1px var(--brand), ${EDGE_DROP}`
681
+ : focusVisible
682
+ ? `0 0 0 1px color-mix(in oklab, var(--foreground) 20%, transparent), ${EDGE_DROP}`
683
+ : hovered && clickToFocus && !disabled
684
+ ? `0 0 0 1px var(--border), ${EDGE_DROP}`
685
+ : undefined
686
+
687
+ const handleSend = useCallback(() => {
688
+ if (!canSend) return
689
+ setHistoryIndex(null)
690
+ // While the assistant is streaming, a submit enqueues instead of sending:
691
+ // snapshot the draft (text + currently-attached files) into a queue item,
692
+ // then clear the composer and keep focus.
693
+ if (streaming && supportsQueue) {
694
+ const item: QueuedMessage = {
695
+ id: crypto.randomUUID(),
696
+ text: trimmed,
697
+ files: filesArr,
698
+ }
699
+ setQueue([...queueRef.current, item])
700
+ setValue('')
701
+ if (supportsFiles) setFiles([])
702
+ requestAnimationFrame(() => textareaRef.current?.focus())
703
+ return
704
+ }
705
+ onSend?.(trimmed, filesArr)
706
+ // Local addition: an uncontrolled draft / file list clears itself, the
707
+ // way a controlled consumer clears its own state inside `onSend`.
708
+ if (!valueControlled) setValue('')
709
+ if (supportsFiles && !filesControlled) setFiles([])
710
+ }, [
711
+ canSend,
712
+ streaming,
713
+ supportsQueue,
714
+ onSend,
715
+ trimmed,
716
+ filesArr,
717
+ setQueue,
718
+ setValue,
719
+ supportsFiles,
720
+ setFiles,
721
+ valueControlled,
722
+ filesControlled,
723
+ ])
724
+
725
+ // Stop halts the response. With the status uncontrolled the component owns
726
+ // that flag, so it flips itself to "idle" — the edge the auto-dispatch
727
+ // below watches for.
728
+ const statusControlled = statusProp !== undefined
729
+ const handleStop = useCallback(() => {
730
+ onStop?.()
731
+ if (!statusControlled) setStatus('idle')
732
+ }, [onStop, statusControlled, setStatus])
733
+
734
+ // Auto-dispatch: on the streaming → idle edge (whether the response
735
+ // finished on its own or the user pressed Stop), fire the head of the
736
+ // queue and drop it. The consumer is expected to set status back to
737
+ // "streaming" inside onSend, which re-arms this for the next item.
738
+ const prevStatusRef = useRef(status)
739
+ useEffect(() => {
740
+ const prev = prevStatusRef.current
741
+ prevStatusRef.current = status
742
+ if (!supportsQueue) return
743
+ if (prev === 'streaming' && status === 'idle' && queueArr.length > 0) {
744
+ const [next, ...rest] = queueArr
745
+ if (!next) return
746
+ setQueue(rest)
747
+ onSend?.(next.text, next.files, { queuedId: next.id })
748
+ setLiveMsg(`Message sent.${rest.length ? ` ${rest.length} still queued.` : ''}`)
749
+ }
750
+ }, [status, supportsQueue, queueArr, setQueue, onSend])
751
+
752
+ // ── Queue item actions ────────────────────────────────────────────
753
+ const editQueued = useCallback(
754
+ (item: QueuedMessage) => {
755
+ if (!supportsQueue) return
756
+ // Silent replace: pull the item out of the queue into the composer,
757
+ // overwriting any current draft. Re-sending re-queues it to the end.
758
+ setHistoryIndex(null)
759
+ setValue(item.text)
760
+ if (supportsFiles) {
761
+ setFiles(maxFiles != null ? item.files.slice(0, maxFiles) : item.files)
762
+ }
763
+ setQueue(queueRef.current.filter(q => q.id !== item.id))
764
+ requestAnimationFrame(() => {
765
+ const el = textareaRef.current
766
+ if (!el) return
767
+ el.focus()
768
+ el.setSelectionRange(el.value.length, el.value.length)
769
+ })
770
+ },
771
+ [supportsQueue, supportsFiles, setValue, setFiles, maxFiles, setQueue],
772
+ )
773
+
774
+ const removeQueued = useCallback(
775
+ (item: QueuedMessage) => setQueue(queueRef.current.filter(q => q.id !== item.id)),
776
+ [setQueue],
777
+ )
778
+
779
+ const moveQueued = useCallback(
780
+ (item: QueuedMessage, dir: -1 | 1) => {
781
+ const cur = queueRef.current
782
+ const i = cur.findIndex(q => q.id === item.id)
783
+ const j = i + dir
784
+ if (i < 0 || j < 0 || j >= cur.length) return
785
+ const next = [...cur]
786
+ const a = next[i]
787
+ const b = next[j]
788
+ if (!a || !b) return
789
+ next[i] = b
790
+ next[j] = a
791
+ setQueue(next)
792
+ },
793
+ [setQueue],
794
+ )
795
+
796
+ // Send button morph: Stop (streaming + empty draft) → Queue (streaming +
797
+ // draft) → Send (idle). Send and Queue share the arrow-up glyph; only the
798
+ // Stop⇄arrow swap animates. An uncontrolled status can always stop.
799
+ const canStop = onStop !== undefined || !statusControlled
800
+ const buttonMode: 'send' | 'queue' | 'stop' = !streaming
801
+ ? 'send'
802
+ : canSend && supportsQueue
803
+ ? 'queue'
804
+ : canStop
805
+ ? 'stop'
806
+ : 'send'
807
+ const buttonLabel = buttonMode === 'stop' ? 'Stop' : buttonMode === 'queue' ? 'Queue message' : sendLabel
808
+
809
+ const setCaretEnd = useCallback(() => {
810
+ requestAnimationFrame(() => {
811
+ const el = textareaRef.current
812
+ if (el) el.setSelectionRange(el.value.length, el.value.length)
813
+ })
814
+ }, [])
815
+
816
+ const handleKeyDown = useCallback(
817
+ (e: ReactKeyboardEvent<HTMLTextAreaElement>) => {
818
+ if (e.nativeEvent.isComposing) return
819
+
820
+ // Suggested prompts: plain ArrowDown moves the highlight into / down
821
+ // the list, ArrowUp walks it back up (then out, returning to plain
822
+ // textarea behavior), Enter fills the highlighted prompt, Escape
823
+ // drops the highlight. With no highlight, ArrowUp still falls through
824
+ // to history recall below.
825
+ if (suggestionsOpen && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) {
826
+ if (e.key === 'ArrowDown') {
827
+ e.preventDefault()
828
+ setActiveSuggestion(prev => (prev == null ? 0 : Math.min(prev + 1, suggestionsArr.length - 1)))
829
+ return
830
+ }
831
+ if (activeSuggestion != null) {
832
+ if (e.key === 'ArrowUp') {
833
+ e.preventDefault()
834
+ setActiveSuggestion(activeSuggestion === 0 ? null : activeSuggestion - 1)
835
+ return
836
+ }
837
+ if (e.key === 'Enter') {
838
+ e.preventDefault()
839
+ const picked = suggestionsArr[activeSuggestion]
840
+ if (picked !== undefined) acceptSuggestion(picked)
841
+ return
842
+ }
843
+ if (e.key === 'Escape') {
844
+ e.preventDefault()
845
+ setActiveSuggestion(null)
846
+ return
847
+ }
848
+ }
849
+ }
850
+
851
+ // Tab fills the suggested placeholder prompt into the empty composer
852
+ // (Shift+Tab still moves focus backward, and once the draft is
853
+ // non-empty Tab resumes normal focus traversal).
854
+ if (e.key === 'Tab' && !e.shiftKey && placeholderSuggestion && value === '') {
855
+ e.preventDefault()
856
+ acceptSuggestion(placeholderSuggestion)
857
+ return
858
+ }
859
+
860
+ // Readline-style history. Only plain ArrowUp/ArrowDown navigate (no
861
+ // modifiers), and only when the caret is on the first/last line so
862
+ // multi-line editing still works normally.
863
+ if (
864
+ history.length > 0 &&
865
+ (e.key === 'ArrowUp' || e.key === 'ArrowDown') &&
866
+ !e.shiftKey &&
867
+ !e.altKey &&
868
+ !e.metaKey &&
869
+ !e.ctrlKey
870
+ ) {
871
+ const el = e.currentTarget
872
+ const caret = el.selectionStart ?? 0
873
+ const end = el.selectionEnd ?? caret
874
+ if (e.key === 'ArrowUp' && !value.slice(0, caret).includes('\n')) {
875
+ const start = historyIndex == null ? history.length : historyIndex
876
+ if (start > 0) {
877
+ e.preventDefault()
878
+ if (historyIndex == null) draftBeforeHistory.current = value
879
+ const ni = start - 1
880
+ setHistoryIndex(ni)
881
+ setValue(history[ni] ?? '')
882
+ setCaretEnd()
883
+ }
884
+ return
885
+ }
886
+ if (e.key === 'ArrowDown' && historyIndex != null && !value.slice(end).includes('\n')) {
887
+ e.preventDefault()
888
+ const ni = historyIndex + 1
889
+ if (ni >= history.length) {
890
+ setHistoryIndex(null)
891
+ setValue(draftBeforeHistory.current)
892
+ } else {
893
+ setHistoryIndex(ni)
894
+ setValue(history[ni] ?? '')
895
+ }
896
+ setCaretEnd()
897
+ return
898
+ }
899
+ }
900
+
901
+ if (e.key === 'Enter' && !e.shiftKey) {
902
+ e.preventDefault()
903
+ handleSend()
904
+ }
905
+ },
906
+ [
907
+ history,
908
+ value,
909
+ historyIndex,
910
+ setValue,
911
+ setCaretEnd,
912
+ handleSend,
913
+ suggestionsOpen,
914
+ suggestionsArr,
915
+ activeSuggestion,
916
+ setActiveSuggestion,
917
+ acceptSuggestion,
918
+ placeholderSuggestion,
919
+ ],
920
+ )
921
+
922
+ const handleContainerMouseDown = useCallback(
923
+ (e: ReactMouseEvent<HTMLDivElement>) => {
924
+ if (!clickToFocus || disabled) return
925
+ const target = e.target as HTMLElement
926
+ if (target === textareaRef.current) return
927
+ if (target.closest('button, a, input, select, textarea, [contenteditable], [role="button"], [data-im-queue]')) {
928
+ return
929
+ }
930
+ e.preventDefault()
931
+ textareaRef.current?.focus()
932
+ },
933
+ [clickToFocus, disabled],
934
+ )
935
+
936
+ // ── File helpers ──────────────────────────────────────────────────
937
+ const acceptTokens = useMemo(
938
+ () =>
939
+ accept
940
+ .split(',')
941
+ .map(s => s.trim())
942
+ .filter(Boolean),
943
+ [accept],
944
+ )
945
+
946
+ const matchesAccept = useCallback(
947
+ (file: File) =>
948
+ acceptTokens.some(token => {
949
+ if (token.endsWith('/*')) return file.type.startsWith(token.slice(0, -1))
950
+ if (token.startsWith('.')) return file.name.toLowerCase().endsWith(token.toLowerCase())
951
+ return file.type === token
952
+ }),
953
+ [acceptTokens],
954
+ )
955
+
956
+ const addFiles = useCallback(
957
+ (incoming: File[]) => {
958
+ if (!supportsFiles) return
959
+ // Identity key for dedup: name + size + lastModified is unique enough
960
+ // to catch "user dropped the same file twice" without false positives
961
+ // on legitimately distinct files (different bytes ⇒ different size).
962
+ const fingerprint = (f: File) => `${f.name}-${f.size}-${f.lastModified}`
963
+ const existing = new Set(filesArr.map(fingerprint))
964
+ const accepted: File[] = []
965
+ for (const f of incoming) {
966
+ if (!matchesAccept(f)) continue
967
+ const fp = fingerprint(f)
968
+ if (existing.has(fp)) continue
969
+ existing.add(fp)
970
+ accepted.push(f)
971
+ }
972
+ if (!accepted.length) return
973
+ const next = [...filesArr, ...accepted]
974
+ setFiles(maxFiles != null ? next.slice(0, maxFiles) : next)
975
+ },
976
+ [supportsFiles, setFiles, filesArr, matchesAccept, maxFiles],
977
+ )
978
+
979
+ const removeFile = useCallback(
980
+ (idx: number) => {
981
+ if (!supportsFiles) return
982
+ setFiles(filesArr.filter((_, i) => i !== idx))
983
+ },
984
+ [supportsFiles, setFiles, filesArr],
985
+ )
986
+
987
+ const openFilePicker = useCallback(
988
+ (overrideAccept?: string) => {
989
+ const el = fileInputRef.current
990
+ if (!el) return
991
+ // Temporarily narrow `accept` for this invocation (e.g. "image/*").
992
+ // Reset after the click so subsequent native invocations still honor
993
+ // the component-level accept.
994
+ if (overrideAccept) {
995
+ el.accept = overrideAccept
996
+ el.click()
997
+ // Restore on next tick — the picker dialog reads `accept` synchronously.
998
+ queueMicrotask(() => {
999
+ if (fileInputRef.current) fileInputRef.current.accept = accept
1000
+ })
1001
+ return
1002
+ }
1003
+ el.click()
1004
+ },
1005
+ [accept],
1006
+ )
1007
+
1008
+ // ── Slot rendering ────────────────────────────────────────────────
1009
+ const slotCtx = useMemo<InputMessageSlotContext>(
1010
+ () => ({ openFilePicker, files: filesArr }),
1011
+ [openFilePicker, filesArr],
1012
+ )
1013
+ const leftContent = typeof leftSlot === 'function' ? leftSlot(slotCtx) : leftSlot
1014
+ const rightContent = typeof rightSlot === 'function' ? rightSlot(slotCtx) : rightSlot
1015
+
1016
+ // ── Drag-and-drop ────────────────────────────────────────────────
1017
+ const handleDragOver = useCallback(
1018
+ (e: ReactDragEvent<HTMLDivElement>) => {
1019
+ if (!supportsFiles || disabled) return
1020
+ // Only treat as a file drag — text/HTML drags shouldn't trigger.
1021
+ if (!Array.from(e.dataTransfer.types).includes('Files')) return
1022
+ e.preventDefault()
1023
+ e.dataTransfer.dropEffect = 'copy'
1024
+ setDragOver(true)
1025
+ },
1026
+ [supportsFiles, disabled],
1027
+ )
1028
+
1029
+ const handleDragLeave = useCallback((e: ReactDragEvent<HTMLDivElement>) => {
1030
+ const wrapper = e.currentTarget
1031
+ const next = e.relatedTarget as Node | null
1032
+ if (next && wrapper.contains(next)) return
1033
+ setDragOver(false)
1034
+ }, [])
1035
+
1036
+ const handleDrop = useCallback(
1037
+ (e: ReactDragEvent<HTMLDivElement>) => {
1038
+ e.preventDefault()
1039
+ setDragOver(false)
1040
+ if (!supportsFiles || disabled) return
1041
+ addFiles(Array.from(e.dataTransfer.files))
1042
+ },
1043
+ [supportsFiles, disabled, addFiles],
1044
+ )
1045
+
1046
+ const handleFileInputChange = useCallback(
1047
+ (e: ChangeEvent<HTMLInputElement>) => {
1048
+ if (!e.target.files) return
1049
+ addFiles(Array.from(e.target.files))
1050
+ e.target.value = '' // Allow re-selecting the same file.
1051
+ },
1052
+ [addFiles],
1053
+ )
1054
+
1055
+ const composer = (
1056
+ <div
1057
+ ref={ref}
1058
+ onMouseDown={handleContainerMouseDown}
1059
+ onDragOver={handleDragOver}
1060
+ onDragLeave={handleDragLeave}
1061
+ onDrop={handleDrop}
1062
+ className={cn(
1063
+ // The edge is the box-shadow's hairline ring (from surface-2), not a
1064
+ // border. State changes recolor that same 1px ring in place rather
1065
+ // than layering a second colored border beside it — so hover / focus
1066
+ // bump *contrast* without ever appearing to thicken the stroke.
1067
+ 'flex flex-col gap-1 p-2 transition-[box-shadow,color] duration-fast',
1068
+ surfaceClasses(2, 2),
1069
+ shape.container,
1070
+ clickToFocus && !disabled && 'cursor-text',
1071
+ disabled && 'opacity-50 pointer-events-none',
1072
+ className,
1073
+ )}
1074
+ style={edgeShadow ? { boxShadow: edgeShadow, ...style } : style}
1075
+ onMouseEnter={() => setHovered(true)}
1076
+ onMouseLeave={() => setHovered(false)}
1077
+ {...props}
1078
+ >
1079
+ <SurfaceProvider value={2}>
1080
+ {supportsFiles && (
1081
+ <input
1082
+ ref={fileInputRef}
1083
+ type="file"
1084
+ accept={accept}
1085
+ multiple={maxFiles == null || maxFiles > 1}
1086
+ className="hidden"
1087
+ onChange={handleFileInputChange}
1088
+ aria-hidden="true"
1089
+ tabIndex={-1}
1090
+ />
1091
+ )}
1092
+
1093
+ {/* Attached files preview row — sits above the textarea.
1094
+ The outer motion.div animates the row's height (collapsing the
1095
+ whole component height) when files appear / disappear.
1096
+ The inner `mode="popLayout"` AnimatePresence pulls a removing
1097
+ tile out of layout flow so siblings can slide into the gap
1098
+ without fighting its exit anim. Keys are purely file-identity
1099
+ (no index) so removing the first file doesn't re-key — and
1100
+ remount — every surviving sibling. */}
1101
+ <AnimatePresence initial={false}>
1102
+ {filesArr.length > 0 && (
1103
+ <motion.div
1104
+ key="preview-row"
1105
+ initial={{ height: 0, opacity: 0 }}
1106
+ animate={{ height: filesRegionHeight ?? 0, opacity: 1 }}
1107
+ exit={{ height: 0, opacity: 0 }}
1108
+ transition={{ ...spring.moderate, bounce: 0 }}
1109
+ className="overflow-hidden"
1110
+ >
1111
+ <div ref={filesRegionRef} className="flex flex-wrap gap-2 pb-1">
1112
+ <AnimatePresence initial={false} mode="popLayout">
1113
+ {filesArr.map((file, i) => (
1114
+ <FilePreviewTile
1115
+ key={`${file.name}-${file.size}-${file.lastModified}`}
1116
+ file={file}
1117
+ onRemove={() => removeFile(i)}
1118
+ size={filePreviewSize}
1119
+ />
1120
+ ))}
1121
+ </AnimatePresence>
1122
+ </div>
1123
+ </motion.div>
1124
+ )}
1125
+ </AnimatePresence>
1126
+
1127
+ {/* Queued messages — reorderable rows above the textarea. The outer
1128
+ motion.div collapses the region height when the queue empties;
1129
+ the Reorder.Group handles drag-reorder (top = next to dispatch)
1130
+ and AnimatePresence handles per-row enter/exit. */}
1131
+ {supportsQueue && showQueue && (
1132
+ <AnimatePresence initial={false}>
1133
+ {queueArr.length > 0 && (
1134
+ <motion.div
1135
+ key="queue-row"
1136
+ initial={{ height: 0, opacity: 0 }}
1137
+ animate={{ height: queueRegionHeight ?? 0, opacity: 1 }}
1138
+ exit={{ height: 0, opacity: 0 }}
1139
+ transition={{ ...spring.moderate, bounce: 0 }}
1140
+ className="overflow-hidden"
1141
+ >
1142
+ <Reorder.Group
1143
+ ref={queueRegionRef}
1144
+ axis="y"
1145
+ values={queueArr}
1146
+ onReorder={(next: QueuedMessage[]) => setQueue(next)}
1147
+ data-im-queue
1148
+ className="flex flex-col gap-1 pb-1"
1149
+ >
1150
+ <AnimatePresence initial={false}>
1151
+ {queueArr.map((item, i) => (
1152
+ <QueuedRow
1153
+ key={item.id}
1154
+ item={item}
1155
+ index={i}
1156
+ total={queueArr.length}
1157
+ reduceMotion={reduceMotion}
1158
+ isTouch={isTouch}
1159
+ onEdit={editQueued}
1160
+ onRemove={removeQueued}
1161
+ onMove={moveQueued}
1162
+ />
1163
+ ))}
1164
+ </AnimatePresence>
1165
+ </Reorder.Group>
1166
+ </motion.div>
1167
+ )}
1168
+ </AnimatePresence>
1169
+ )}
1170
+
1171
+ <div className="relative">
1172
+ <textarea
1173
+ ref={textareaRef}
1174
+ value={value}
1175
+ onChange={e => {
1176
+ // Real typing exits history mode (recall sets the value
1177
+ // programmatically, which doesn't fire onChange) and drops
1178
+ // any suggestion highlight.
1179
+ setHistoryIndex(null)
1180
+ setActiveSuggestion(null)
1181
+ setValue(e.target.value)
1182
+ }}
1183
+ onKeyDown={handleKeyDown}
1184
+ // Compose the consumer's textareaProps handlers with the internal
1185
+ // focus-visible tracking (the spread below would otherwise
1186
+ // overwrite these).
1187
+ onFocus={e => {
1188
+ if (e.target.matches(':focus-visible')) setFocusVisible(true)
1189
+ textareaProps?.onFocus?.(e)
1190
+ }}
1191
+ onBlur={e => {
1192
+ setFocusVisible(false)
1193
+ setActiveSuggestion(null)
1194
+ textareaProps?.onBlur?.(e)
1195
+ }}
1196
+ placeholder={
1197
+ dragOver && supportsFiles
1198
+ ? 'Drop files here to add to chat'
1199
+ : placeholderSuggestion
1200
+ ? undefined // the ghost overlay below renders it
1201
+ : placeholder
1202
+ }
1203
+ disabled={disabled}
1204
+ rows={minRows}
1205
+ aria-label={textareaProps?.['aria-label'] ?? 'Message'}
1206
+ aria-describedby={
1207
+ [showGhost ? ghostHintId : null, textareaDescribedBy].filter(Boolean).join(' ') || undefined
1208
+ }
1209
+ aria-activedescendant={activeSuggestion != null ? `${suggestionListId}-${activeSuggestion}` : undefined}
1210
+ className={cn(
1211
+ 'w-full resize-none rounded-none bg-transparent outline-none',
1212
+ 'text-foreground placeholder:text-muted-foreground',
1213
+ compactStep ? 'text-subtitle-compact leading-4.5 px-1.5 py-1.5' : 'text-subtitle leading-5 px-2 py-2',
1214
+ 'weight-normal',
1215
+ )}
1216
+ {...restTextareaProps}
1217
+ />
1218
+ {/* Ghost placeholder: the suggested prompt with a Tab keycap. A
1219
+ real overlay (not the native placeholder) so the keycap can
1220
+ render inline after the text; typography mirrors the textarea
1221
+ exactly so it sits where typed text will. */}
1222
+ {showGhost && (
1223
+ <div
1224
+ aria-hidden="true"
1225
+ className={cn(
1226
+ 'pointer-events-none absolute inset-0 overflow-hidden text-muted-foreground',
1227
+ // Mirror the textarea's step typography exactly so the ghost
1228
+ // sits where typed text will.
1229
+ compactStep ? 'text-subtitle-compact leading-4.5 px-1.5 py-1.5' : 'text-subtitle leading-5 px-2 py-2',
1230
+ 'weight-normal',
1231
+ )}
1232
+ >
1233
+ {/* One flex line: a suggestion longer than the field truncates
1234
+ with an ellipsis instead of wrapping into the clip (the
1235
+ overlay is inset-0 over a possibly single-row textarea),
1236
+ and the Tab chip never gets cut. */}
1237
+ <span className="flex max-w-full items-center gap-1.5">
1238
+ <span className="min-w-0 truncate">{placeholderSuggestion}</span>
1239
+ <kbd
1240
+ className={cn(
1241
+ 'inline-flex shrink-0 -translate-y-px items-center rounded-box border border-border bg-background px-1 font-sans text-muted-foreground',
1242
+ compactStep ? 'h-4 text-micro-compact' : 'h-[18px] text-micro',
1243
+ )}
1244
+ >
1245
+ Tab
1246
+ </kbd>
1247
+ </span>
1248
+ </div>
1249
+ )}
1250
+ {showGhost && (
1251
+ <span id={ghostHintId} className="sr-only">
1252
+ Suggested prompt: {placeholderSuggestion}. Press Tab to fill the composer with it.
1253
+ </span>
1254
+ )}
1255
+ </div>
1256
+ <div
1257
+ className={cn(
1258
+ 'flex items-center justify-between',
1259
+ // The footer's controls sit one notch below the composer's step:
1260
+ // slot content is consumer-authored (usually sm/icon-sm pinned
1261
+ // Buttons), so the compact step scales any button in the row —
1262
+ // send button included — down to 24px via a scoped override.
1263
+ compactStep ? 'gap-1.5 [&_button]:h-6 [&_button.w-7]:w-6 [&_button]:text-caption-compact' : 'gap-2',
1264
+ )}
1265
+ >
1266
+ <div className="flex items-center gap-1.5 min-w-0">{leftContent}</div>
1267
+ <div className="flex items-center gap-1.5 shrink-0">
1268
+ {rightContent}
1269
+ <Button
1270
+ type="button"
1271
+ variant="primary"
1272
+ size="icon-sm"
1273
+ onClick={buttonMode === 'stop' ? handleStop : handleSend}
1274
+ disabled={buttonMode === 'stop' ? disabled : !canSend}
1275
+ aria-label={buttonLabel}
1276
+ >
1277
+ <AnimatePresence mode="wait" initial={false}>
1278
+ <motion.span
1279
+ key={buttonMode === 'stop' ? 'stop' : 'arrow'}
1280
+ initial={reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.6 }}
1281
+ animate={{ opacity: 1, scale: 1 }}
1282
+ exit={reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.6, transition: spring.fast.exit }}
1283
+ transition={spring.fast}
1284
+ className="flex items-center justify-center leading-none"
1285
+ >
1286
+ {buttonMode === 'stop' ? (
1287
+ <span className="h-3 w-3 rounded-glyph bg-current" />
1288
+ ) : (
1289
+ // Override icon-sm's small 14px svg — the send glyph reads
1290
+ // better a touch larger. `size` matches the attribute to
1291
+ // the CSS so the svg box stays centered.
1292
+ <ArrowUpIcon
1293
+ size={compactStep ? 15 : 19}
1294
+ className={cn('block', compactStep ? '!h-[15px] !w-[15px]' : '!h-[19px] !w-[19px]')}
1295
+ />
1296
+ )}
1297
+ </motion.span>
1298
+ </AnimatePresence>
1299
+ </Button>
1300
+ </div>
1301
+ </div>
1302
+
1303
+ {/* Suggested prompts — a listbox under the action bar, shown while
1304
+ the draft is empty. ↓/↑ move the highlight without moving focus
1305
+ (the textarea's aria-activedescendant tracks it); Enter or click
1306
+ fills the composer. The outer motion.div collapses the region's
1307
+ height once typing hides the list; -mx-2 cancels the container
1308
+ padding so the divider runs the composer's full width. Pointer
1309
+ and keyboard share one bg-hover overlay that springs between
1310
+ row rects (fluid-hover, same as Dropdown). */}
1311
+ {suggestionsArr.length > 0 && (
1312
+ <AnimatePresence initial={false}>
1313
+ {suggestionsOpen && (
1314
+ <motion.div
1315
+ key="suggestions"
1316
+ initial={{ height: 0, opacity: 0 }}
1317
+ animate={{ height: suggestionsRegionHeight ?? 0, opacity: 1 }}
1318
+ // Height-only exit: the region clips shut bottom-up under
1319
+ // overflow-hidden, so the divider holds its place until the
1320
+ // space actually closes (a simultaneous opacity fade made it
1321
+ // vanish mid-collapse, which read as a height glitch).
1322
+ exit={{ height: 0 }}
1323
+ transition={{ ...spring.moderate, bounce: 0 }}
1324
+ // -mt-1 cancels the container's gap-1 above this region and
1325
+ // the listbox's mt-2 restores it INSIDE the collapsible
1326
+ // area — otherwise that 4px gap sits outside the height
1327
+ // animation and snaps away only at unmount.
1328
+ className="-mx-2 -mt-1 overflow-hidden"
1329
+ >
1330
+ <div
1331
+ ref={el => {
1332
+ suggestionListRef.current = el
1333
+ suggestionsRegionRef(el)
1334
+ }}
1335
+ role="listbox"
1336
+ id={suggestionListId}
1337
+ aria-label="Suggested prompts"
1338
+ onMouseEnter={suggestionHandlers.onMouseEnter}
1339
+ onMouseMove={suggestionHandlers.onMouseMove}
1340
+ onMouseLeave={suggestionHandlers.onMouseLeave}
1341
+ onClick={suggestionHandlers.onClick}
1342
+ className="relative mt-2 flex flex-col border-t border-border/60 px-1.5 pt-1.5"
1343
+ >
1344
+ {/* Hover / keyboard highlight: one overlay sliding
1345
+ between rows instead of per-row backgrounds. */}
1346
+ <FluidHoverHighlight hover={suggestionHover} className={shape.bg} />
1347
+ {suggestionsArr.map((s, i) => (
1348
+ <SuggestionRow
1349
+ key={`${s}-${i}`}
1350
+ text={s}
1351
+ index={i}
1352
+ active={i === activeSuggestion}
1353
+ keyHint={i === 0 && activeSuggestion == null}
1354
+ optionId={`${suggestionListId}-${i}`}
1355
+ registerItem={registerSuggestion}
1356
+ onSelect={() => acceptSuggestion(s)}
1357
+ />
1358
+ ))}
1359
+ </div>
1360
+ </motion.div>
1361
+ )}
1362
+ </AnimatePresence>
1363
+ )}
1364
+ {/* Politely announces auto-dispatch of queued messages. */}
1365
+ <span className="sr-only" role="status" aria-live="polite">
1366
+ {liveMsg}
1367
+ </span>
1368
+ </SurfaceProvider>
1369
+ </div>
1370
+ )
1371
+
1372
+ // A size prop pins the whole composer — inner buttons, rows and queued
1373
+ // messages included — to one ladder step (matches InputGroup).
1374
+ return size ? <SizeProvider size={size}>{composer}</SizeProvider> : composer
1375
+ },
1376
+ )
1377
+
1378
+ InputMessage.displayName = 'InputMessage'
1379
+
1380
+ export { FileThumbnail } from './file-thumbnail'
1381
+ export type { InputMessageProps, InputMessageSlotContext, InputMessageStatus, QueuedMessage }
1382
+ export { InputMessage }
1383
+ export default InputMessage