@elabs-ai/components-ai 4.1.0 → 5.0.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 (527) hide show
  1. package/README.md +3 -3
  2. package/dist/{_audio-player-media-chrome-T3XVXWRZ.js → _audio-player-media-chrome.js} +1 -1
  3. package/dist/_code-block-theme.js +12 -0
  4. package/dist/_code-block-theme.js.map +1 -0
  5. package/dist/{_flow-boundary-SHNWLQG5.js → _flow-boundary.js} +1 -1
  6. package/dist/_flow-boundary.js.map +1 -0
  7. package/dist/_flow-lazy.js +8 -0
  8. package/dist/_flow-lazy.js.map +1 -0
  9. package/dist/_lazy-boundary-conformance.js +2 -0
  10. package/dist/_lazy-boundary-conformance.js.map +1 -0
  11. package/dist/_lazy-cjk.js +13 -0
  12. package/dist/_lazy-cjk.js.map +1 -0
  13. package/dist/_lazy-engine-boundary.js +9 -0
  14. package/dist/_lazy-engine-boundary.js.map +1 -0
  15. package/dist/_lazy-math.js +13 -0
  16. package/dist/_lazy-math.js.map +1 -0
  17. package/dist/_lazy-mermaid.js +12 -0
  18. package/dist/_lazy-mermaid.js.map +1 -0
  19. package/dist/_mermaid-error-panel.js +9 -0
  20. package/dist/_mermaid-error-panel.js.map +1 -0
  21. package/dist/{_persona-rive-JEG44YHX.js → _persona-rive.js} +19 -24
  22. package/dist/_persona-rive.js.map +1 -0
  23. package/dist/_streamdown-i18n.js +21 -0
  24. package/dist/_streamdown-i18n.js.map +1 -0
  25. package/dist/_streamdown-safety.js +10 -0
  26. package/dist/_streamdown-safety.js.map +1 -0
  27. package/dist/_theme-scope-store.js +13 -0
  28. package/dist/_theme-scope-store.js.map +1 -0
  29. package/dist/a2ui/a2ui-surface.js +17 -0
  30. package/dist/a2ui/a2ui-surface.js.map +1 -0
  31. package/dist/a2ui/core/catalog.generated.js +10 -0
  32. package/dist/a2ui/core/catalog.generated.js.map +1 -0
  33. package/dist/a2ui/core/complete-json.js +10 -0
  34. package/dist/a2ui/core/complete-json.js.map +1 -0
  35. package/dist/a2ui/core/index.js +37 -0
  36. package/dist/a2ui/core/index.js.map +1 -0
  37. package/dist/a2ui/core/schema.js +11 -0
  38. package/dist/a2ui/core/schema.js.map +1 -0
  39. package/dist/a2ui/core/spec.js +10 -0
  40. package/dist/a2ui/core/spec.js.map +1 -0
  41. package/dist/a2ui/core/validate.js +16 -0
  42. package/dist/a2ui/core/validate.js.map +1 -0
  43. package/dist/a2ui/index.js +54 -0
  44. package/dist/a2ui/index.js.map +1 -0
  45. package/dist/a2ui/ui-catalog.js +18 -0
  46. package/dist/a2ui/ui-catalog.js.map +1 -0
  47. package/dist/agent-event.js +10 -0
  48. package/dist/agent-event.js.map +1 -0
  49. package/dist/agent-timeline.js +11 -0
  50. package/dist/agent-timeline.js.map +1 -0
  51. package/dist/agent.js +25 -0
  52. package/dist/agent.js.map +1 -0
  53. package/dist/artifact.js +23 -0
  54. package/dist/artifact.js.map +1 -0
  55. package/dist/asset-preview.js +22 -0
  56. package/dist/asset-preview.js.map +1 -0
  57. package/dist/attachments.js +33 -0
  58. package/dist/attachments.js.map +1 -0
  59. package/dist/audio-player.js +30 -0
  60. package/dist/audio-player.js.map +1 -0
  61. package/dist/audio-visualizer.js +9 -0
  62. package/dist/audio-visualizer.js.map +1 -0
  63. package/dist/canvas.js +10 -0
  64. package/dist/canvas.js.map +1 -0
  65. package/dist/chain-of-thought.js +22 -0
  66. package/dist/chain-of-thought.js.map +1 -0
  67. package/dist/chat-greeting.js +8 -0
  68. package/dist/chat-greeting.js.map +1 -0
  69. package/dist/chat-shell.js +9 -0
  70. package/dist/chat-shell.js.map +1 -0
  71. package/dist/checkpoint.js +13 -0
  72. package/dist/checkpoint.js.map +1 -0
  73. package/dist/chunk-2XOIG5TG.js +72 -0
  74. package/dist/chunk-2XOIG5TG.js.map +1 -0
  75. package/dist/chunk-34CQ2VHM.js +172 -0
  76. package/dist/chunk-34CQ2VHM.js.map +1 -0
  77. package/dist/chunk-3TDHPXZZ.js +60 -0
  78. package/dist/chunk-3TDHPXZZ.js.map +1 -0
  79. package/dist/chunk-3XNPMDZY.js +478 -0
  80. package/dist/chunk-3XNPMDZY.js.map +1 -0
  81. package/dist/chunk-3YPT3V7L.js +56 -0
  82. package/dist/chunk-3YPT3V7L.js.map +1 -0
  83. package/dist/chunk-475UEU2Q.js +87 -0
  84. package/dist/chunk-475UEU2Q.js.map +1 -0
  85. package/dist/chunk-4CBTII2I.js +95 -0
  86. package/dist/chunk-4CBTII2I.js.map +1 -0
  87. package/dist/chunk-4XRNIZMG.js +22 -0
  88. package/dist/chunk-4XRNIZMG.js.map +1 -0
  89. package/dist/chunk-5NNWUKK5.js +2 -0
  90. package/dist/chunk-5NNWUKK5.js.map +1 -0
  91. package/dist/chunk-5UOMU27Z.js +164 -0
  92. package/dist/chunk-5UOMU27Z.js.map +1 -0
  93. package/dist/chunk-5VGYZ62I.js +264 -0
  94. package/dist/chunk-5VGYZ62I.js.map +1 -0
  95. package/dist/chunk-6IQPH2OS.js +21 -0
  96. package/dist/chunk-6IQPH2OS.js.map +1 -0
  97. package/dist/chunk-6PB2MXCI.js +482 -0
  98. package/dist/chunk-6PB2MXCI.js.map +1 -0
  99. package/dist/chunk-7FD5CZEU.js +215 -0
  100. package/dist/chunk-7FD5CZEU.js.map +1 -0
  101. package/dist/chunk-7RL6KZDF.js +266 -0
  102. package/dist/chunk-7RL6KZDF.js.map +1 -0
  103. package/dist/chunk-ADOYAGGK.js +482 -0
  104. package/dist/chunk-ADOYAGGK.js.map +1 -0
  105. package/dist/chunk-AXMNJMMH.js +984 -0
  106. package/dist/chunk-AXMNJMMH.js.map +1 -0
  107. package/dist/chunk-BA3AEMOU.js +21 -0
  108. package/dist/chunk-BA3AEMOU.js.map +1 -0
  109. package/dist/chunk-BBHFOYML.js +336 -0
  110. package/dist/chunk-BBHFOYML.js.map +1 -0
  111. package/dist/chunk-BHUVISIY.js +221 -0
  112. package/dist/chunk-BHUVISIY.js.map +1 -0
  113. package/dist/chunk-BM2LWERA.js +100 -0
  114. package/dist/chunk-BM2LWERA.js.map +1 -0
  115. package/dist/chunk-CMOYXNPJ.js +211 -0
  116. package/dist/chunk-CMOYXNPJ.js.map +1 -0
  117. package/dist/chunk-CZP3RHC4.js +74 -0
  118. package/dist/chunk-CZP3RHC4.js.map +1 -0
  119. package/dist/chunk-DDG5JHQT.js +112 -0
  120. package/dist/chunk-DDG5JHQT.js.map +1 -0
  121. package/dist/chunk-DJODXO7I.js +38 -0
  122. package/dist/chunk-DJODXO7I.js.map +1 -0
  123. package/dist/chunk-DWJDJLPU.js +192 -0
  124. package/dist/chunk-DWJDJLPU.js.map +1 -0
  125. package/dist/chunk-DZW6LM22.js +45 -0
  126. package/dist/chunk-DZW6LM22.js.map +1 -0
  127. package/dist/chunk-ELGCZLZF.js +305 -0
  128. package/dist/chunk-ELGCZLZF.js.map +1 -0
  129. package/dist/chunk-EV3U6ZZB.js +209 -0
  130. package/dist/chunk-EV3U6ZZB.js.map +1 -0
  131. package/dist/chunk-EY7LVUKQ.js +87 -0
  132. package/dist/chunk-EY7LVUKQ.js.map +1 -0
  133. package/dist/chunk-F4C74F7M.js +230 -0
  134. package/dist/chunk-F4C74F7M.js.map +1 -0
  135. package/dist/chunk-FERYDFZA.js +108 -0
  136. package/dist/chunk-FERYDFZA.js.map +1 -0
  137. package/dist/chunk-FUIYG2DS.js +166 -0
  138. package/dist/chunk-FUIYG2DS.js.map +1 -0
  139. package/dist/chunk-GKYZT3LA.js +136 -0
  140. package/dist/chunk-GKYZT3LA.js.map +1 -0
  141. package/dist/chunk-H42LRUQ4.js +102 -0
  142. package/dist/chunk-H42LRUQ4.js.map +1 -0
  143. package/dist/chunk-HVXM2G5F.js +222 -0
  144. package/dist/chunk-HVXM2G5F.js.map +1 -0
  145. package/dist/chunk-I4RDAGVY.js +53 -0
  146. package/dist/chunk-I4RDAGVY.js.map +1 -0
  147. package/dist/chunk-IQJG4IDX.js +208 -0
  148. package/dist/chunk-IQJG4IDX.js.map +1 -0
  149. package/dist/chunk-IX2PXG4Y.js +144 -0
  150. package/dist/chunk-IX2PXG4Y.js.map +1 -0
  151. package/dist/chunk-IXICSOOP.js +287 -0
  152. package/dist/chunk-IXICSOOP.js.map +1 -0
  153. package/dist/chunk-JY5HSN4D.js +84 -0
  154. package/dist/chunk-JY5HSN4D.js.map +1 -0
  155. package/dist/chunk-LMNSBOBG.js +19 -0
  156. package/dist/chunk-LMNSBOBG.js.map +1 -0
  157. package/dist/chunk-MBI7D6IL.js +42 -0
  158. package/dist/chunk-MBI7D6IL.js.map +1 -0
  159. package/dist/chunk-MLFMYCPH.js +192 -0
  160. package/dist/chunk-MLFMYCPH.js.map +1 -0
  161. package/dist/chunk-MOK66734.js +33 -0
  162. package/dist/chunk-MOK66734.js.map +1 -0
  163. package/dist/chunk-MXTAWPYM.js +196 -0
  164. package/dist/chunk-MXTAWPYM.js.map +1 -0
  165. package/dist/chunk-NCL2UZKI.js +96 -0
  166. package/dist/chunk-NCL2UZKI.js.map +1 -0
  167. package/dist/chunk-NET4YLRY.js +107 -0
  168. package/dist/chunk-NET4YLRY.js.map +1 -0
  169. package/dist/chunk-NIG67CFM.js +329 -0
  170. package/dist/chunk-NIG67CFM.js.map +1 -0
  171. package/dist/chunk-NMVNNO3L.js +10 -0
  172. package/dist/chunk-NMVNNO3L.js.map +1 -0
  173. package/dist/chunk-NP7VID4K.js +2 -0
  174. package/dist/chunk-NP7VID4K.js.map +1 -0
  175. package/dist/chunk-NPUCMAG2.js +382 -0
  176. package/dist/chunk-NPUCMAG2.js.map +1 -0
  177. package/dist/chunk-NSCLRHES.js +102 -0
  178. package/dist/chunk-NSCLRHES.js.map +1 -0
  179. package/dist/chunk-OEUXHVFW.js +86 -0
  180. package/dist/chunk-OEUXHVFW.js.map +1 -0
  181. package/dist/chunk-ONBHFIMZ.js +137 -0
  182. package/dist/chunk-ONBHFIMZ.js.map +1 -0
  183. package/dist/chunk-OPH2HHL2.js +228 -0
  184. package/dist/chunk-OPH2HHL2.js.map +1 -0
  185. package/dist/chunk-OQERHIQP.js +287 -0
  186. package/dist/chunk-OQERHIQP.js.map +1 -0
  187. package/dist/chunk-P6YJAZFG.js +103 -0
  188. package/dist/chunk-P6YJAZFG.js.map +1 -0
  189. package/dist/chunk-PG7ERDOD.js +65 -0
  190. package/dist/chunk-PG7ERDOD.js.map +1 -0
  191. package/dist/chunk-PIB5JBMA.js +90 -0
  192. package/dist/chunk-PIB5JBMA.js.map +1 -0
  193. package/dist/chunk-PQ5BPIWQ.js +22 -0
  194. package/dist/chunk-PQ5BPIWQ.js.map +1 -0
  195. package/dist/chunk-PQBF6ZIQ.js +55 -0
  196. package/dist/chunk-PQBF6ZIQ.js.map +1 -0
  197. package/dist/chunk-PTWCTSZL.js +173 -0
  198. package/dist/chunk-PTWCTSZL.js.map +1 -0
  199. package/dist/chunk-QJUNPVOS.js +101 -0
  200. package/dist/chunk-QJUNPVOS.js.map +1 -0
  201. package/dist/chunk-QUDZOTRV.js +43 -0
  202. package/dist/chunk-QUDZOTRV.js.map +1 -0
  203. package/dist/chunk-RB7G65JC.js +213 -0
  204. package/dist/chunk-RB7G65JC.js.map +1 -0
  205. package/dist/chunk-RPHAOR7P.js +45 -0
  206. package/dist/chunk-RPHAOR7P.js.map +1 -0
  207. package/dist/chunk-RTCSH2AN.js +331 -0
  208. package/dist/chunk-RTCSH2AN.js.map +1 -0
  209. package/dist/chunk-S3ISPW3A.js +28 -0
  210. package/dist/chunk-S3ISPW3A.js.map +1 -0
  211. package/dist/chunk-SEVN656N.js +36 -0
  212. package/dist/chunk-SEVN656N.js.map +1 -0
  213. package/dist/chunk-SKVG672B.js +64 -0
  214. package/dist/chunk-SKVG672B.js.map +1 -0
  215. package/dist/chunk-SPQIU7GA.js +236 -0
  216. package/dist/chunk-SPQIU7GA.js.map +1 -0
  217. package/dist/chunk-SSGJ6MEM.js +1076 -0
  218. package/dist/chunk-SSGJ6MEM.js.map +1 -0
  219. package/dist/chunk-SSKZ2AED.js +118 -0
  220. package/dist/chunk-SSKZ2AED.js.map +1 -0
  221. package/dist/chunk-T56JFGXI.js +561 -0
  222. package/dist/chunk-T56JFGXI.js.map +1 -0
  223. package/dist/chunk-TCB6O74A.js +137 -0
  224. package/dist/chunk-TCB6O74A.js.map +1 -0
  225. package/dist/chunk-TTEYPO6X.js +57 -0
  226. package/dist/chunk-TTEYPO6X.js.map +1 -0
  227. package/dist/chunk-TWPHAJNM.js +462 -0
  228. package/dist/chunk-TWPHAJNM.js.map +1 -0
  229. package/dist/chunk-TXJJLP4O.js +336 -0
  230. package/dist/chunk-TXJJLP4O.js.map +1 -0
  231. package/dist/chunk-TZ6TB7VM.js +245 -0
  232. package/dist/chunk-TZ6TB7VM.js.map +1 -0
  233. package/dist/chunk-UBLKJJ7K.js +278 -0
  234. package/dist/chunk-UBLKJJ7K.js.map +1 -0
  235. package/dist/chunk-UCJ6IUFS.js +52 -0
  236. package/dist/chunk-UCJ6IUFS.js.map +1 -0
  237. package/dist/chunk-UCKFJ3QF.js +197 -0
  238. package/dist/chunk-UCKFJ3QF.js.map +1 -0
  239. package/dist/chunk-UPXSLILN.js +20 -0
  240. package/dist/chunk-UPXSLILN.js.map +1 -0
  241. package/dist/chunk-V4DORGWJ.js +92 -0
  242. package/dist/chunk-V4DORGWJ.js.map +1 -0
  243. package/dist/chunk-V6E5DYZD.js +80 -0
  244. package/dist/chunk-V6E5DYZD.js.map +1 -0
  245. package/dist/chunk-VLLYKF3P.js +96 -0
  246. package/dist/chunk-VLLYKF3P.js.map +1 -0
  247. package/dist/chunk-VLNBTOH6.js +43 -0
  248. package/dist/chunk-VLNBTOH6.js.map +1 -0
  249. package/dist/chunk-WMDSMSPL.js +303 -0
  250. package/dist/chunk-WMDSMSPL.js.map +1 -0
  251. package/dist/chunk-WU5BLGTF.js +149 -0
  252. package/dist/chunk-WU5BLGTF.js.map +1 -0
  253. package/dist/chunk-WYTZVSGK.js +245 -0
  254. package/dist/chunk-WYTZVSGK.js.map +1 -0
  255. package/dist/chunk-X47QMM6K.js +187 -0
  256. package/dist/chunk-X47QMM6K.js.map +1 -0
  257. package/dist/chunk-XNBKO32Y.js +174 -0
  258. package/dist/chunk-XNBKO32Y.js.map +1 -0
  259. package/dist/chunk-YHUSW7D2.js +40 -0
  260. package/dist/chunk-YHUSW7D2.js.map +1 -0
  261. package/dist/chunk-YSKD7UXX.js +126 -0
  262. package/dist/chunk-YSKD7UXX.js.map +1 -0
  263. package/dist/chunk-YYRDGWOU.js +72 -0
  264. package/dist/chunk-YYRDGWOU.js.map +1 -0
  265. package/dist/chunk-ZIGZMWM7.js +19 -0
  266. package/dist/chunk-ZIGZMWM7.js.map +1 -0
  267. package/dist/chunk-ZKRTUKNP.js +40 -0
  268. package/dist/chunk-ZKRTUKNP.js.map +1 -0
  269. package/dist/code-block.js +38 -0
  270. package/dist/code-block.js.map +1 -0
  271. package/dist/commit.js +51 -0
  272. package/dist/commit.js.map +1 -0
  273. package/dist/composer.js +15 -0
  274. package/dist/composer.js.map +1 -0
  275. package/dist/confirmation.js +53 -0
  276. package/dist/confirmation.js.map +1 -0
  277. package/dist/connection.js +8 -0
  278. package/dist/connection.js.map +1 -0
  279. package/dist/context-panel.js +25 -0
  280. package/dist/context-panel.js.map +1 -0
  281. package/dist/controls.js +10 -0
  282. package/dist/controls.js.map +1 -0
  283. package/dist/conversation.js +19 -0
  284. package/dist/conversation.js.map +1 -0
  285. package/dist/diff-view.js +15 -0
  286. package/dist/diff-view.js.map +1 -0
  287. package/dist/edge.js +10 -0
  288. package/dist/edge.js.map +1 -0
  289. package/dist/environment-variables.js +29 -0
  290. package/dist/environment-variables.js.map +1 -0
  291. package/dist/file-tree.js +23 -0
  292. package/dist/file-tree.js.map +1 -0
  293. package/dist/gallery.js +9 -0
  294. package/dist/gallery.js.map +1 -0
  295. package/dist/grouped-parts.js +21 -0
  296. package/dist/grouped-parts.js.map +1 -0
  297. package/dist/image.js +9 -0
  298. package/dist/image.js.map +1 -0
  299. package/dist/index.d.ts +403 -108
  300. package/dist/index.js +671 -13205
  301. package/dist/index.js.map +1 -1
  302. package/dist/inline-citation.js +39 -0
  303. package/dist/inline-citation.js.map +1 -0
  304. package/dist/jsx-preview.js +17 -0
  305. package/dist/jsx-preview.js.map +1 -0
  306. package/dist/markdown-view.js +17 -0
  307. package/dist/markdown-view.js.map +1 -0
  308. package/dist/message-compare.js +12 -0
  309. package/dist/message-compare.js.map +1 -0
  310. package/dist/message-edit.js +19 -0
  311. package/dist/message-edit.js.map +1 -0
  312. package/dist/message-feedback.js +11 -0
  313. package/dist/message-feedback.js.map +1 -0
  314. package/dist/message-form-spec.js +38 -0
  315. package/dist/message-form-spec.js.map +1 -0
  316. package/dist/message-form.js +26 -0
  317. package/dist/message-form.js.map +1 -0
  318. package/dist/message-table-spec.js +26 -0
  319. package/dist/message-table-spec.js.map +1 -0
  320. package/dist/message-table.js +12 -0
  321. package/dist/message-table.js.map +1 -0
  322. package/dist/message.js +51 -0
  323. package/dist/message.js.map +1 -0
  324. package/dist/mic-selector.js +27 -0
  325. package/dist/mic-selector.js.map +1 -0
  326. package/dist/model-provider-logo.js +13 -0
  327. package/dist/model-provider-logo.js.map +1 -0
  328. package/dist/motion-config.js +9 -0
  329. package/dist/motion-config.js.map +1 -0
  330. package/dist/node.js +22 -0
  331. package/dist/node.js.map +1 -0
  332. package/dist/open-in-chat.js +31 -0
  333. package/dist/open-in-chat.js.map +1 -0
  334. package/dist/package-info.js +25 -0
  335. package/dist/package-info.js.map +1 -0
  336. package/dist/panel.js +10 -0
  337. package/dist/panel.js.map +1 -0
  338. package/dist/part-groups.js +18 -0
  339. package/dist/part-groups.js.map +1 -0
  340. package/dist/permission-mode-select.js +9 -0
  341. package/dist/permission-mode-select.js.map +1 -0
  342. package/dist/persona-sources.js +8 -0
  343. package/dist/persona-sources.js.map +1 -0
  344. package/dist/persona.js +14 -0
  345. package/dist/persona.js.map +1 -0
  346. package/dist/plan.js +32 -0
  347. package/dist/plan.js.map +1 -0
  348. package/dist/prompt-input-effort.js +9 -0
  349. package/dist/prompt-input-effort.js.map +1 -0
  350. package/dist/prompt-input-mode.js +10 -0
  351. package/dist/prompt-input-mode.js.map +1 -0
  352. package/dist/prompt-input-slash.js +12 -0
  353. package/dist/prompt-input-slash.js.map +1 -0
  354. package/dist/prompt-input.js +89 -0
  355. package/dist/prompt-input.js.map +1 -0
  356. package/dist/queue.js +37 -0
  357. package/dist/queue.js.map +1 -0
  358. package/dist/reasoning.js +23 -0
  359. package/dist/reasoning.js.map +1 -0
  360. package/dist/sandbox.js +28 -0
  361. package/dist/sandbox.js.map +1 -0
  362. package/dist/schema-display.js +33 -0
  363. package/dist/schema-display.js.map +1 -0
  364. package/dist/selection-toolbar.js +11 -0
  365. package/dist/selection-toolbar.js.map +1 -0
  366. package/dist/session-header.js +9 -0
  367. package/dist/session-header.js.map +1 -0
  368. package/dist/session-status-bar.js +9 -0
  369. package/dist/session-status-bar.js.map +1 -0
  370. package/dist/shimmer.js +9 -0
  371. package/dist/shimmer.js.map +1 -0
  372. package/dist/snippet.js +17 -0
  373. package/dist/snippet.js.map +1 -0
  374. package/dist/sources.js +17 -0
  375. package/dist/sources.js.map +1 -0
  376. package/dist/speech-input.js +9 -0
  377. package/dist/speech-input.js.map +1 -0
  378. package/dist/stack-trace.js +27 -0
  379. package/dist/stack-trace.js.map +1 -0
  380. package/dist/suggestion.js +16 -0
  381. package/dist/suggestion.js.map +1 -0
  382. package/dist/task.js +18 -0
  383. package/dist/task.js.map +1 -0
  384. package/dist/test-results.js +43 -0
  385. package/dist/test-results.js.map +1 -0
  386. package/dist/token-usage.js +27 -0
  387. package/dist/token-usage.js.map +1 -0
  388. package/dist/tool-result-card.js +9 -0
  389. package/dist/tool-result-card.js.map +1 -0
  390. package/dist/tool.js +27 -0
  391. package/dist/tool.js.map +1 -0
  392. package/dist/toolbar.js +10 -0
  393. package/dist/toolbar.js.map +1 -0
  394. package/dist/transcription.js +11 -0
  395. package/dist/transcription.js.map +1 -0
  396. package/dist/turn-status.js +9 -0
  397. package/dist/turn-status.js.map +1 -0
  398. package/dist/use-audio-level.js +9 -0
  399. package/dist/use-audio-level.js.map +1 -0
  400. package/dist/voice-selector.js +47 -0
  401. package/dist/voice-selector.js.map +1 -0
  402. package/dist/web-preview.js +19 -0
  403. package/dist/web-preview.js.map +1 -0
  404. package/package.json +12 -10
  405. package/schemas/a2ui-surface.v1.schema.json +5425 -0
  406. package/src/__contract__/audio-visualizer.contract.test.tsx +49 -0
  407. package/src/__contract__/chat-shell.contract.test.tsx +49 -0
  408. package/src/__contract__/grouped-parts.contract.test.tsx +49 -0
  409. package/src/__contract__/image.contract.test.tsx +49 -0
  410. package/src/__contract__/markdown-view.contract.test.tsx +49 -0
  411. package/src/__contract__/message-feedback.contract.test.tsx +49 -0
  412. package/src/__contract__/message-form.contract.test.tsx +49 -0
  413. package/src/__contract__/message-table.contract.test.tsx +49 -0
  414. package/src/__contract__/model-provider-logo.contract.test.tsx +49 -0
  415. package/src/__contract__/persona.contract.test.tsx +49 -0
  416. package/src/__contract__/prompt-input-effort.contract.test.tsx +49 -0
  417. package/src/__contract__/prompt-input-mode.contract.test.tsx +49 -0
  418. package/src/_chat-shell-rail.tsx +4 -2
  419. package/src/_flow-boundary.tsx +2 -2
  420. package/src/_lazy-cjk.test.ts +43 -0
  421. package/src/_lazy-cjk.ts +73 -0
  422. package/src/_lazy-math.test.ts +63 -0
  423. package/src/_lazy-math.ts +90 -0
  424. package/src/_persona-rive.tsx +27 -41
  425. package/src/_streamdown-i18n.ts +73 -21
  426. package/src/_streamdown-safety.ts +2 -2
  427. package/src/_theme-scope-store.test.ts +83 -0
  428. package/src/_theme-scope-store.ts +103 -0
  429. package/src/a2ui/a2ui-surface.stories.tsx +662 -0
  430. package/src/a2ui/a2ui-surface.test.tsx +201 -0
  431. package/src/a2ui/a2ui-surface.tsx +354 -0
  432. package/src/a2ui/catalog.source.json +772 -0
  433. package/src/a2ui/core/a2ui-core.test.ts +178 -0
  434. package/src/a2ui/core/catalog.generated.ts +1006 -0
  435. package/src/a2ui/core/complete-json.ts +81 -0
  436. package/src/a2ui/core/index.ts +10 -0
  437. package/src/a2ui/core/schema.ts +111 -0
  438. package/src/a2ui/core/spec.ts +148 -0
  439. package/src/a2ui/core/validate.ts +266 -0
  440. package/src/a2ui/index.ts +7 -0
  441. package/src/a2ui/ui-catalog.tsx +225 -0
  442. package/src/a2ui/wave1-chart-spec.test.ts +87 -0
  443. package/src/a2ui/wave2-chart-spec.test.ts +108 -0
  444. package/src/agent-event.tsx +1 -1
  445. package/src/agent.tsx +18 -13
  446. package/src/agentic-workspace.stories.tsx +6 -45
  447. package/src/artifact.tsx +2 -2
  448. package/src/asset-preview.test.tsx +40 -0
  449. package/src/asset-preview.tsx +83 -12
  450. package/src/attachments.tsx +7 -4
  451. package/src/chat-shell.test.tsx +40 -10
  452. package/src/chat-shell.tsx +90 -44
  453. package/src/chat.stories.tsx +47 -42
  454. package/src/code-block.test.tsx +100 -1
  455. package/src/code-block.tsx +166 -103
  456. package/src/commit.tsx +30 -41
  457. package/src/composer.stories.tsx +4 -5
  458. package/src/composer.test.tsx +45 -0
  459. package/src/composer.tsx +104 -21
  460. package/src/confirmation.tsx +1 -1
  461. package/src/context-panel.stories.tsx +1 -1
  462. package/src/context-panel.tsx +2 -2
  463. package/src/conversation.stories.tsx +24 -0
  464. package/src/conversation.test.tsx +69 -0
  465. package/src/conversation.tsx +47 -8
  466. package/src/diff-view.stories.tsx +79 -0
  467. package/src/diff-view.test.tsx +52 -2
  468. package/src/diff-view.tsx +92 -36
  469. package/src/environment-variables.tsx +20 -37
  470. package/src/file-tree.test.tsx +21 -0
  471. package/src/file-tree.tsx +33 -16
  472. package/src/gallery.stories.tsx +42 -0
  473. package/src/gallery.tsx +2 -2
  474. package/src/grouped-parts.stories.tsx +23 -0
  475. package/src/grouped-parts.tsx +1 -1
  476. package/src/index.ts +1 -0
  477. package/src/inline-citation.tsx +5 -5
  478. package/src/jsx-preview.tsx +151 -43
  479. package/src/markdown-view.test.tsx +7 -3
  480. package/src/markdown-view.tsx +8 -1
  481. package/src/message-compare.stories.tsx +40 -0
  482. package/src/message-compare.tsx +2 -1
  483. package/src/message-form.stories.tsx +36 -3
  484. package/src/message-form.test.tsx +8 -2
  485. package/src/message-form.tsx +16 -8
  486. package/src/message-table.stories.tsx +26 -2
  487. package/src/message-table.test.tsx +7 -0
  488. package/src/message-table.tsx +9 -5
  489. package/src/message.test.tsx +73 -3
  490. package/src/message.tsx +36 -12
  491. package/src/model-provider-logo.test.tsx +57 -3
  492. package/src/model-provider-logo.tsx +45 -11
  493. package/src/open-in-chat.tsx +50 -29
  494. package/src/package-info.tsx +23 -20
  495. package/src/prompt-input-slash.stories.tsx +1 -1
  496. package/src/prompt-input.test.tsx +67 -1
  497. package/src/prompt-input.tsx +42 -4
  498. package/src/queue.tsx +4 -4
  499. package/src/reasoning.stories.tsx +27 -0
  500. package/src/reasoning.tsx +17 -6
  501. package/src/sandbox.tsx +3 -3
  502. package/src/schema-display.test.tsx +56 -0
  503. package/src/schema-display.tsx +68 -37
  504. package/src/session-header.stories.tsx +35 -1
  505. package/src/session-header.tsx +1 -1
  506. package/src/snippet.test.tsx +6 -2
  507. package/src/snippet.tsx +14 -34
  508. package/src/sources.stories.tsx +30 -0
  509. package/src/sources.tsx +4 -1
  510. package/src/speech-input.test.tsx +109 -0
  511. package/src/speech-input.tsx +31 -3
  512. package/src/stack-trace.tsx +20 -36
  513. package/src/streamdown-i18n.test.tsx +23 -0
  514. package/src/templates-ai-assistant.stories.tsx +151 -87
  515. package/src/test-results.tsx +47 -27
  516. package/src/token-usage.tsx +6 -6
  517. package/src/tool.stories.tsx +39 -0
  518. package/src/tool.test.tsx +65 -0
  519. package/src/tool.tsx +61 -25
  520. package/src/toolbar.tsx +0 -10
  521. package/src/transcription.tsx +1 -1
  522. package/src/voice-selector.tsx +5 -5
  523. package/src/web-preview.test.tsx +51 -1
  524. package/src/web-preview.tsx +39 -8
  525. package/dist/_flow-boundary-SHNWLQG5.js.map +0 -1
  526. package/dist/_persona-rive-JEG44YHX.js.map +0 -1
  527. /package/dist/{_audio-player-media-chrome-T3XVXWRZ.js.map → _audio-player-media-chrome.js.map} +0 -0
@@ -30,6 +30,7 @@ import {
30
30
  TableHead,
31
31
  TableHeader,
32
32
  TableRow,
33
+ useLocale,
33
34
  } from "@elabs-ai/components-ui";
34
35
  import { cn } from "@elabs-ai/components-ui/lib/cn";
35
36
  import type { HTMLAttributes, ReactNode } from "react";
@@ -72,18 +73,87 @@ interface ParsedCsv {
72
73
  rows: string[][];
73
74
  }
74
75
 
75
- /** Minimal preview-grade CSV split (no quoted-comma handling — it's a glance, not a grid). */
76
+ /**
77
+ * A small RFC 4180-ish CSV tokenizer: handles quoted fields (commas and
78
+ * newlines inside quotes don't end the field/row) and the `""` escaped-quote
79
+ * convention. Single pass over the raw string — no line pre-split, so a
80
+ * quoted field spanning multiple lines survives intact. Still preview-grade
81
+ * (unquoted cells are trimmed for the glance table), not a full CSV grammar.
82
+ */
83
+ function parseCsvRows(content: string): string[][] {
84
+ const rows: string[][] = [];
85
+ let row: string[] = [];
86
+ let field = "";
87
+ let wasQuoted = false;
88
+ let inQuotes = false;
89
+
90
+ const endField = () => {
91
+ row.push(wasQuoted ? field : field.trim());
92
+ field = "";
93
+ wasQuoted = false;
94
+ };
95
+ const endRow = () => {
96
+ endField();
97
+ rows.push(row);
98
+ row = [];
99
+ };
100
+
101
+ for (let i = 0; i < content.length; i++) {
102
+ const ch = content[i];
103
+
104
+ if (inQuotes) {
105
+ if (ch === '"') {
106
+ if (content[i + 1] === '"') {
107
+ field += '"';
108
+ i += 1;
109
+ } else {
110
+ inQuotes = false;
111
+ }
112
+ } else {
113
+ field += ch;
114
+ }
115
+ continue;
116
+ }
117
+
118
+ if (ch === '"' && field === "") {
119
+ inQuotes = true;
120
+ wasQuoted = true;
121
+ continue;
122
+ }
123
+
124
+ if (ch === ",") {
125
+ endField();
126
+ continue;
127
+ }
128
+
129
+ if (ch === "\r") {
130
+ continue;
131
+ }
132
+
133
+ if (ch === "\n") {
134
+ endRow();
135
+ continue;
136
+ }
137
+
138
+ field += ch;
139
+ }
140
+
141
+ // A trailing field/row with no terminating newline.
142
+ if (field !== "" || row.length > 0) {
143
+ endRow();
144
+ }
145
+
146
+ return rows;
147
+ }
148
+
76
149
  function parseCsv(content: string): ParsedCsv {
77
- const lines = content
78
- .trim()
79
- .split(/\r?\n/)
80
- .filter((line) => line.length > 0);
81
- const [head = "", ...rest] = lines;
82
- const split = (line: string) => line.split(",").map((cell) => cell.trim());
83
- return { header: split(head), rows: rest.map(split) };
150
+ const rows = parseCsvRows(content.trim()).filter((r) => !(r.length === 1 && r[0] === ""));
151
+ const [header = [], ...rest] = rows;
152
+ return { header, rows: rest };
84
153
  }
85
154
 
86
155
  const CsvPreview = ({ content }: { content: string }) => {
156
+ const { t } = useLocale();
87
157
  const { header, rows } = useMemo(() => parseCsv(content), [content]);
88
158
  return (
89
159
  <div className="flex flex-col gap-2">
@@ -109,7 +179,7 @@ const CsvPreview = ({ content }: { content: string }) => {
109
179
  </TableBody>
110
180
  </Table>
111
181
  <p className="text-meta text-muted-foreground">
112
- {rows.length} {rows.length === 1 ? "row" : "rows"}
182
+ {t("ai.assetPreview.rowCount", { count: rows.length })}
113
183
  </p>
114
184
  </div>
115
185
  );
@@ -135,6 +205,7 @@ export const AssetPreview = ({
135
205
  className,
136
206
  ...props
137
207
  }: AssetPreviewProps) => {
208
+ const { t } = useLocale();
138
209
  const [mode, setMode] = useState<AssetPreviewMode>(defaultMode);
139
210
  const injectedRenderer = useAssetPreviewRenderer();
140
211
  const content = asset.content ?? "";
@@ -150,7 +221,7 @@ export const AssetPreview = ({
150
221
  if (injected !== null) {
151
222
  body = injected;
152
223
  } else if (asset.type !== "image" && content.length === 0) {
153
- body = <p className="text-body text-muted-foreground">No preview available…</p>;
224
+ body = <p className="text-body text-muted-foreground">{t("ai.assetPreview.noPreview")}</p>;
154
225
  } else if (showRaw) {
155
226
  body = <CodeBlock code={content} language={assetLanguage(asset)} wrap />;
156
227
  } else {
@@ -197,7 +268,7 @@ export const AssetPreview = ({
197
268
  className={cn(!showRaw && "bg-accent text-accent-foreground")}
198
269
  onClick={() => setMode("preview")}
199
270
  >
200
- Preview
271
+ {t("ai.assetPreview.preview")}
201
272
  </Button>
202
273
  <Button
203
274
  type="button"
@@ -207,7 +278,7 @@ export const AssetPreview = ({
207
278
  className={cn(showRaw && "bg-accent text-accent-foreground")}
208
279
  onClick={() => setMode("raw")}
209
280
  >
210
- Raw
281
+ {t("ai.assetPreview.raw")}
211
282
  </Button>
212
283
  </ArtifactActions>
213
284
  ) : null}
@@ -192,8 +192,8 @@ export const Attachment = ({ data, onRemove, className, children, ...props }: At
192
192
  variant === "inline" && [
193
193
  "flex h-8 cursor-pointer select-none items-center gap-1.5",
194
194
  "rounded-md border border-border px-1.5",
195
- "font-medium text-sm transition-colors duration-fast ease-standard motion-reduce:transition-none",
196
- "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
195
+ "font-medium text-body transition-colors duration-fast ease-standard motion-reduce:transition-none",
196
+ "hover:bg-accent/50 hover:text-accent-foreground",
197
197
  ],
198
198
  variant === "list" && [
199
199
  "flex w-full items-center gap-3 rounded-lg border p-3",
@@ -283,7 +283,7 @@ export const AttachmentInfo = ({
283
283
  <div className={cn("min-w-0 flex-1", className)} {...props}>
284
284
  <span className="block truncate">{label}</span>
285
285
  {showMediaType && data.mediaType && (
286
- <span className="block truncate text-muted-foreground text-xs">{data.mediaType}</span>
286
+ <span className="block truncate text-muted-foreground text-meta">{data.mediaType}</span>
287
287
  )}
288
288
  </div>
289
289
  );
@@ -385,7 +385,10 @@ export type AttachmentEmptyProps = HTMLAttributes<HTMLDivElement>;
385
385
 
386
386
  export const AttachmentEmpty = ({ className, children, ...props }: AttachmentEmptyProps) => (
387
387
  <div
388
- className={cn("flex items-center justify-center p-4 text-muted-foreground text-sm", className)}
388
+ className={cn(
389
+ "flex items-center justify-center p-4 text-muted-foreground text-body",
390
+ className,
391
+ )}
389
392
  {...props}
390
393
  >
391
394
  {children ?? "No attachments"}
@@ -58,8 +58,8 @@ describe("ChatShell — slots", () => {
58
58
 
59
59
  it("omits the header row entirely when no header is supplied", () => {
60
60
  const { container } = render(<ChatShell>t</ChatShell>);
61
- // An empty header would still occupy its fixed h-12 row.
62
- expect(container.querySelector(".h-12")).toBeNull();
61
+ // An empty header would still occupy its fixed h-header row.
62
+ expect(container.querySelector(".h-header")).toBeNull();
63
63
  });
64
64
 
65
65
  it("omits the aside rail entirely when no aside is supplied", () => {
@@ -91,16 +91,46 @@ describe("ChatShell — variants", () => {
91
91
  expect(container.querySelector(".border-t")).toBeNull();
92
92
  });
93
93
 
94
- it("makes the bare variant's edge scrims inert (aria-hidden AND pointer-events-none)", () => {
95
- const { container } = render(<ChatShell variant="bare">t</ChatShell>);
96
- const scrims = container.querySelectorAll("[aria-hidden='true']");
97
- expect(scrims).toHaveLength(2);
98
- for (const scrim of scrims) expect(scrim).toHaveClass("pointer-events-none");
94
+ it("makes the edge scrims inert (aria-hidden AND pointer-events-none) in both variants", () => {
95
+ for (const variant of ["card", "bare"] as const) {
96
+ const { container, unmount } = render(<ChatShell variant={variant}>t</ChatShell>);
97
+ const scrims = container.querySelectorAll("[aria-hidden='true']");
98
+ expect(scrims).toHaveLength(2);
99
+ for (const scrim of scrims) expect(scrim).toHaveClass("pointer-events-none");
100
+ unmount();
101
+ }
99
102
  });
103
+ });
100
104
 
101
- it("renders no scrims in the card variant (there is a real divider instead)", () => {
102
- const { container } = render(<ChatShell>t</ChatShell>);
103
- expect(container.querySelectorAll("[aria-hidden='true']")).toHaveLength(0);
105
+ describe("ChatShell — floating composer", () => {
106
+ it("floats the composer over the transcript instead of stacking a footer bar", () => {
107
+ const { container } = render(
108
+ <ChatShell composer={<textarea aria-label="Message" />}>
109
+ <p>transcript</p>
110
+ </ChatShell>,
111
+ );
112
+ const layer = container.querySelector("[data-slot='chat-shell-composer']");
113
+ expect(layer).toHaveClass("absolute", "bottom-0", "pb-4");
114
+ // The layer passes pointer events through to the transcript; only the
115
+ // composer column takes input.
116
+ expect(layer).toHaveClass("pointer-events-none");
117
+ expect(screen.getByRole("textbox", { name: "Message" }).parentElement).toHaveClass(
118
+ "pointer-events-auto",
119
+ "max-w-(--chat-composer-column)",
120
+ );
121
+ expect(container.querySelector(".border-t")).toBeNull();
122
+ });
123
+
124
+ it("publishes the composer height so the transcript can scroll clear of it", () => {
125
+ const { container, rerender } = render(
126
+ <ChatShell composer={<textarea aria-label="Message" />}>t</ChatShell>,
127
+ );
128
+ const root = container.firstChild as HTMLElement;
129
+ // jsdom has no layout, so the measured height is 0 — the contract is that
130
+ // the variable is written at all.
131
+ expect(root.style.getPropertyValue("--chat-composer-inset")).toBe("0px");
132
+ rerender(<ChatShell>t</ChatShell>);
133
+ expect(root.style.getPropertyValue("--chat-composer-inset")).toBe("");
104
134
  });
105
135
  });
106
136
 
@@ -1,25 +1,31 @@
1
1
  "use client";
2
2
 
3
3
  import type { ReactNode } from "react";
4
+ import { useLayoutEffect, useRef } from "react";
4
5
  import { cn } from "@elabs-ai/components-ui/lib/cn";
5
6
 
6
7
  export interface ChatShellProps {
7
8
  /** Scrollable transcript — typically a <Conversation>. */
8
9
  children: ReactNode;
9
- /** Sticky composer — typically a <Composer> (the standard chat input). */
10
+ /**
11
+ * Floating composer — typically a <Composer> (the standard chat input). It
12
+ * floats, padded, over the bottom of the transcript, centred at
13
+ * `--chat-composer-column` (default `--container-3xl`) under a transcript
14
+ * column of `--chat-column` (default `--container-4xl`, read by
15
+ * `ConversationContent`); turns scroll up behind it and fade out.
16
+ */
10
17
  composer?: ReactNode;
11
18
  /** Optional header row (title, model picker, actions). */
12
19
  header?: ReactNode;
13
20
  /** Optional right-side context rail (sources, files, memory). */
14
21
  aside?: ReactNode;
15
22
  /**
16
- * Layout treatment:
17
- * - `"card"` (default) — a self-contained rounded, bordered chat card. Use
18
- * when the shell sits on a page among other content and needs its own frame.
19
- * - `"bare"` — an immersive pane that fills its container with NO frame: the
20
- * transcript fades under the header and above the composer (a token-driven
21
- * top/bottom scrim) and the composer floats on the pane background instead
22
- * of a hard bordered bar. Use when the shell already lives inside a bounded
23
+ * Frame treatment (the transcript/composer layout is the same in both):
24
+ * - `"card"` (default) — a self-contained rounded, bordered chat card with a
25
+ * divider under the header. Use when the shell sits on a page among other
26
+ * content and needs its own frame.
27
+ * - `"bare"` — an immersive pane that fills its container with NO frame and
28
+ * no header divider. Use when the shell already lives inside a bounded
23
29
  * region (an AppShell `SidebarInset`, a full-page workspace) so it doesn't
24
30
  * draw a redundant second frame.
25
31
  */
@@ -28,14 +34,15 @@ export interface ChatShellProps {
28
34
  }
29
35
 
30
36
  /**
31
- * Layout shell for an AI chat built from the AI Elements: a header, a scrollable
32
- * transcript (give it a <Conversation>, which is `flex-1`), a sticky composer
33
- * (a <Composer>, the standard chat input), and an optional context rail. Fills
37
+ * Layout shell for an AI chat built from the AI Elements: a header, a
38
+ * full-height scrollable transcript (give it a <Conversation>, which is
39
+ * `flex-1`), a composer floating over the transcript's bottom edge (a
40
+ * <Composer>, the standard chat input), and an optional context rail. Fills
34
41
  * its parent's height.
35
42
  *
36
- * Two layout modes (`variant`): a framed `"card"` for in-page use, and a
37
- * frameless `"bare"` immersive pane for when the shell already sits inside a
38
- * bounded region and a second frame would just read as boxy.
43
+ * Two frames (`variant`): a framed `"card"` for in-page use, and a frameless
44
+ * `"bare"` immersive pane for when the shell already sits inside a bounded
45
+ * region and a second frame would just read as boxy.
39
46
  */
40
47
  export function ChatShell({
41
48
  children,
@@ -46,50 +53,89 @@ export function ChatShell({
46
53
  className,
47
54
  }: ChatShellProps) {
48
55
  const bare = variant === "bare";
56
+ const rootRef = useRef<HTMLDivElement>(null);
57
+ const composerRef = useRef<HTMLDivElement>(null);
58
+ const hasComposer = composer != null;
59
+
60
+ // The composer floats OVER the transcript, so the transcript has to
61
+ // know how tall it is — `ConversationContent` pads its bottom by
62
+ // `--chat-composer-inset` (so the last turn scrolls clear of the composer)
63
+ // and `ConversationScrollButton` sits just above it. Written straight onto
64
+ // the root's style: no React state, no re-render per resize.
65
+ useLayoutEffect(() => {
66
+ const root = rootRef.current;
67
+ const node = composerRef.current;
68
+ if (!root) return;
69
+ if (!node) {
70
+ root.style.removeProperty("--chat-composer-inset");
71
+ return;
72
+ }
73
+ const sync = () =>
74
+ root.style.setProperty("--chat-composer-inset", `${node.getBoundingClientRect().height}px`);
75
+ sync();
76
+ if (typeof ResizeObserver === "undefined") return;
77
+ const observer = new ResizeObserver(sync);
78
+ observer.observe(node);
79
+ return () => observer.disconnect();
80
+ }, [hasComposer]);
81
+
49
82
  return (
50
83
  <div
84
+ ref={rootRef}
85
+ data-slot="chat-shell"
86
+ data-variant={variant}
51
87
  className={cn(
52
- "flex h-full min-h-0 w-full overflow-hidden bg-background",
88
+ // Reading columns: the transcript (`ConversationContent`) reads a
89
+ // little wider than the composer beneath it. Override per app, e.g.
90
+ // `className="[--chat-column:var(--container-5xl)]"`.
91
+ "flex h-full min-h-0 w-full overflow-hidden bg-background [--chat-column:var(--container-4xl)] [--chat-composer-column:var(--container-3xl)]",
53
92
  bare ? null : "rounded-xl border",
54
93
  className,
55
94
  )}
56
95
  >
57
96
  <div className="flex min-h-0 min-w-0 flex-1 flex-col">
58
97
  {header ? (
59
- <div className={cn("flex h-12 shrink-0 items-center px-4", bare ? null : "border-b")}>
98
+ <div className={cn("flex h-header shrink-0 items-center px-4", bare ? null : "border-b")}>
60
99
  {header}
61
100
  </div>
62
101
  ) : null}
63
102
 
64
- {bare ? (
65
- // Immersive transcript: the scroll area fades at both edges so content
66
- // dissolves under the header and above the composer instead of hitting
67
- // a hard bordered bar. The overlays are token-driven (`from-background`)
68
- // so they fade to the shell's own ground in every theme, and inert
69
- // (`pointer-events-none` + `aria-hidden`) so they never block scroll,
70
- // steal a tap, or get announced.
71
- <div className="relative flex min-h-0 flex-1 flex-col">
72
- {children}
73
- <div
74
- aria-hidden
75
- className="pointer-events-none absolute inset-x-0 top-0 z-10 h-8 bg-gradient-to-b from-background to-transparent"
76
- />
103
+ {/* The transcript runs the FULL height of the pane and the composer
104
+ floats over its bottom edge, so turns scroll up behind the composer
105
+ and dissolve under the top edge instead of stopping at a hard bar.
106
+ The scrims are token-driven (`from-background`) so they fade to the
107
+ shell's own ground in every theme, and inert (`pointer-events-none`
108
+ + `aria-hidden`) so they never block scroll, steal a tap, or get
109
+ announced. */}
110
+ <div className="relative flex min-h-0 flex-1 flex-col">
111
+ {children}
112
+ <div
113
+ aria-hidden
114
+ className="pointer-events-none absolute inset-x-0 top-0 z-10 h-8 bg-gradient-to-b from-background to-transparent"
115
+ />
116
+ <div
117
+ aria-hidden
118
+ data-slot="chat-shell-scrim"
119
+ // Solid under the composer, fading out a little above it.
120
+ className="pointer-events-none absolute inset-x-0 bottom-0 z-10 bg-gradient-to-t from-background from-50% to-transparent"
121
+ style={{ height: "calc(var(--chat-composer-inset, 0px) + 3rem)" }}
122
+ />
123
+ {composer ? (
124
+ // The float layer passes pointer events through, so the
125
+ // transcript's scrollbar and the gutters beside the composer stay
126
+ // live; only the composer itself takes input. DOM order still puts
127
+ // it after the transcript (tab order follows reading order).
77
128
  <div
78
- aria-hidden
79
- className="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-12 bg-gradient-to-t from-background to-transparent"
80
- />
81
- </div>
82
- ) : (
83
- <div className="flex min-h-0 flex-1 flex-col">{children}</div>
84
- )}
85
-
86
- {composer ? (
87
- // Bare: the composer floats on the pane ground (transparent, no
88
- // divider) above the fade; card: the classic bordered footer.
89
- <div className={cn("shrink-0", bare ? "relative z-20 px-4 pb-4" : "border-t p-3")}>
90
- {composer}
91
- </div>
92
- ) : null}
129
+ ref={composerRef}
130
+ data-slot="chat-shell-composer"
131
+ className="pointer-events-none absolute inset-x-0 bottom-0 z-20 px-4 pb-4"
132
+ >
133
+ <div className="pointer-events-auto mx-auto w-full max-w-(--chat-composer-column)">
134
+ {composer}
135
+ </div>
136
+ </div>
137
+ ) : null}
138
+ </div>
93
139
  </div>
94
140
  {aside ? <div className="hidden w-80 shrink-0 border-s lg:block">{aside}</div> : null}
95
141
  </div>
@@ -1,5 +1,6 @@
1
1
  import { useState } from "react";
2
2
  import type { Meta, StoryObj } from "@storybook/react";
3
+ import { ChatShell } from "./chat-shell";
3
4
  import { Conversation, ConversationContent, ConversationScrollButton } from "./conversation";
4
5
  import { Message, MessageContent, MessageResponse } from "./message";
5
6
  import { Composer } from "./composer";
@@ -45,45 +46,49 @@ function ChatExample() {
45
46
  };
46
47
 
47
48
  return (
48
- <div className="mx-auto flex h-[640px] max-w-2xl flex-col overflow-hidden rounded-xl border">
49
- <Conversation className="flex-1">
50
- <ConversationContent>
51
- {messages.map((m) => (
52
- <Message from={m.role} key={m.id}>
53
- <MessageContent>
54
- {m.reasoning ? (
55
- <Reasoning>
56
- <ReasoningTrigger />
57
- <ReasoningContent>{m.reasoning}</ReasoningContent>
58
- </Reasoning>
59
- ) : null}
60
- <MessageResponse>{m.text}</MessageResponse>
61
- {m.role === "assistant" ? (
62
- <Sources>
63
- <SourcesTrigger count={2} />
64
- <SourcesContent>
65
- <Source href="https://example.com" title="Deploy log — wk 23" />
66
- <Source href="https://example.com" title="Billing runbook" />
67
- </SourcesContent>
68
- </Sources>
69
- ) : null}
70
- </MessageContent>
71
- </Message>
72
- ))}
73
- </ConversationContent>
74
- <ConversationScrollButton />
75
- </Conversation>
76
-
77
- <Composer
78
- className="m-3 mt-0"
79
- placeholder="Ask about deploys…"
80
- status={status === "submitted" ? "Generating…" : "Awaiting your input"}
81
- sendStatus={status}
82
- onSubmit={(message) => {
83
- const text = message.text?.trim();
84
- if (text) send(text);
85
- }}
86
- />
49
+ <div className="mx-auto h-[640px] max-w-4xl overflow-hidden rounded-xl border bg-background">
50
+ <ChatShell
51
+ variant="bare"
52
+ composer={
53
+ <Composer
54
+ placeholder="Ask about deploys…"
55
+ status={status === "submitted" ? "Generating…" : "Awaiting your input"}
56
+ sendStatus={status}
57
+ onSubmit={(message) => {
58
+ const text = message.text?.trim();
59
+ if (text) send(text);
60
+ }}
61
+ />
62
+ }
63
+ >
64
+ <Conversation className="flex-1">
65
+ <ConversationContent>
66
+ {messages.map((m) => (
67
+ <Message from={m.role} key={m.id}>
68
+ <MessageContent>
69
+ {m.reasoning ? (
70
+ <Reasoning>
71
+ <ReasoningTrigger />
72
+ <ReasoningContent>{m.reasoning}</ReasoningContent>
73
+ </Reasoning>
74
+ ) : null}
75
+ <MessageResponse>{m.text}</MessageResponse>
76
+ {m.role === "assistant" ? (
77
+ <Sources>
78
+ <SourcesTrigger count={2} />
79
+ <SourcesContent>
80
+ <Source href="https://example.com" title="Deploy log — wk 23" />
81
+ <Source href="https://example.com" title="Billing runbook" />
82
+ </SourcesContent>
83
+ </Sources>
84
+ ) : null}
85
+ </MessageContent>
86
+ </Message>
87
+ ))}
88
+ </ConversationContent>
89
+ <ConversationScrollButton />
90
+ </Conversation>
91
+ </ChatShell>
87
92
  </div>
88
93
  );
89
94
  }
@@ -100,9 +105,9 @@ const meta = {
100
105
  "composed from the @elabs-ai/components-ai grammar: a scrolling " +
101
106
  "[Conversation](?path=/story/ai-conversation--default) of " +
102
107
  "[Messages](?path=/story/ai-message--presets) with reasoning and sources, plus a " +
103
- "[Composer](?path=/story/ai-composer--default). Reach for " +
104
- "[ChatShell](?path=/story/ai-chatshell--default) when you want that header + " +
105
- "transcript + composer layout as one component instead of assembling it yourself.",
108
+ "[Composer](?path=/story/ai-composer--default), laid out by " +
109
+ "[ChatShell](?path=/story/ai-chatshell--default): the transcript scrolls behind the " +
110
+ "floating composer and both sit in centred reading columns.",
106
111
  },
107
112
  },
108
113
  },
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import type { CSSProperties } from "react";
5
5
  import { afterEach, describe, expect, it } from "vitest";
6
6
  import { render, waitFor } from "@testing-library/react";
7
- import { CodeBlock } from "./code-block";
7
+ import { CodeBlock, CodeBlockCopyButton, highlightCode } from "./code-block";
8
8
 
9
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
10
 
@@ -189,3 +189,102 @@ describe("CodeBlock isStreaming (#269, loading-states.md)", () => {
189
189
  expect(container.querySelector('[role="status"]')).toBeNull();
190
190
  });
191
191
  });
192
+
193
+ describe("CodeBlockCopyButton accessible name (a11y review)", () => {
194
+ it("has an aria-label so the icon-only button has an accessible name", () => {
195
+ const { container } = render(
196
+ <CodeBlock code="const a = 1;" language="tsx">
197
+ <CodeBlockCopyButton />
198
+ </CodeBlock>,
199
+ );
200
+ expect(container.querySelector("button")).toHaveAccessibleName("Copy");
201
+ });
202
+
203
+ it("lets a consumer override the label", () => {
204
+ const { container } = render(
205
+ <CodeBlock code="const a = 1;" language="tsx">
206
+ <CodeBlockCopyButton aria-label="Copy snippet" />
207
+ </CodeBlock>,
208
+ );
209
+ expect(container.querySelector("button")).toHaveAccessibleName("Copy snippet");
210
+ });
211
+ });
212
+
213
+ // ─── `highlightCode` awaited helper for the perf-fix regression locks below ──
214
+
215
+ function highlightAsync(
216
+ code: string,
217
+ language: Parameters<typeof highlightCode>[1],
218
+ el?: Element | null,
219
+ skipCache = false,
220
+ ): Promise<NonNullable<ReturnType<typeof highlightCode>>> {
221
+ return new Promise((resolve) => {
222
+ const cached = highlightCode(code, language, (result) => resolve(result), el, skipCache);
223
+ if (cached) resolve(cached);
224
+ });
225
+ }
226
+
227
+ const flattenTokenContent = (result: NonNullable<ReturnType<typeof highlightCode>>): string =>
228
+ result.tokens.map((line) => line.map((token) => token.content).join("")).join("\n");
229
+
230
+ describe("highlightCode cache key — no hash collision (perf review 1.4b/1.4c)", () => {
231
+ it("does not collide two >200-char strings sharing a length, prefix and suffix but differing in the middle", async () => {
232
+ const prefix = "a".repeat(150);
233
+ const suffix = "b".repeat(150);
234
+ // Same length, same first/last 100 chars — the OLD `length:first100:last100`
235
+ // cache key collided on exactly this shape (a mid-string edit), showing
236
+ // whichever of the two resolved second on top of BOTH code blocks.
237
+ const codeA = `${prefix}1${suffix}`;
238
+ const codeB = `${prefix}2${suffix}`;
239
+
240
+ await highlightAsync(codeA, "tsx");
241
+ await highlightAsync(codeB, "tsx");
242
+
243
+ // Re-requesting codeA must still return a SYNCHRONOUS cache hit for its
244
+ // OWN content — never codeB's.
245
+ const againA = highlightCode(codeA, "tsx");
246
+ expect(againA).not.toBeNull();
247
+ expect(flattenTokenContent(againA!)).toBe(codeA);
248
+ expect(flattenTokenContent(againA!)).not.toBe(codeB);
249
+ });
250
+ });
251
+
252
+ describe("highlightCode tokensCache is bounded (perf review 1.4a — LRU ~200 entries)", () => {
253
+ it("evicts the least-recently-used entry once the cache exceeds its cap", async () => {
254
+ const codes = Array.from({ length: 205 }, (_, i) => `const lruEvictionProbe_${i} = ${i};`);
255
+
256
+ for (const code of codes) {
257
+ // Sequential on purpose: each must actually populate the cache (and
258
+ // become the most-recently-used entry) before the next one runs.
259
+
260
+ await highlightAsync(code, "tsx");
261
+ }
262
+
263
+ // The very first entry inserted is the least-recently-used once 205 > the
264
+ // 200-entry cap — it must have been evicted, so re-requesting it is a
265
+ // cache MISS (highlightCode returns null synchronously and kicks off a
266
+ // fresh async highlight instead).
267
+ expect(highlightCode(codes[0] as string, "tsx")).toBeNull();
268
+
269
+ // The most recently inserted entry is still a synchronous cache hit.
270
+ expect(highlightCode(codes[204] as string, "tsx")).not.toBeNull();
271
+ });
272
+ });
273
+
274
+ describe("highlightCode skipCache (perf review 1.4a — never permanently cache mid-stream)", () => {
275
+ it("does not persist a highlight into the shared cache when skipCache is set", async () => {
276
+ const code = "const streamingSkipCacheProbe = 'a';";
277
+ await highlightAsync(code, "tsx", undefined, true);
278
+
279
+ // A later, non-streaming request for the EXACT same code is a cache
280
+ // MISS — the streaming pass never wrote it in.
281
+ expect(highlightCode(code, "tsx")).toBeNull();
282
+ });
283
+
284
+ it("still persists an ordinary (non-streaming) highlight", async () => {
285
+ const code = "const nonStreamingCacheProbe = 'b';";
286
+ await highlightAsync(code, "tsx");
287
+
288
+ expect(highlightCode(code, "tsx")).not.toBeNull();
289
+ });
290
+ });