@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
package/src/diff-view.tsx CHANGED
@@ -59,6 +59,7 @@ import { cva } from "class-variance-authority";
59
59
  import type { BundledLanguage, ThemedToken } from "shiki";
60
60
  import { highlightCode } from "./code-block";
61
61
  import { Shimmer } from "./shimmer";
62
+ import { getThemeScope } from "./_theme-scope-store";
62
63
 
63
64
  // ─── Public types ───────────────────────────────────────────────────────────
64
65
  //
@@ -139,41 +140,27 @@ const MARKER_TONE: Record<"add" | "del" | "context", string> = {
139
140
  context: "text-muted-foreground",
140
141
  };
141
142
 
142
- // ─── Theme scope (mirrors code-block.tsx's private helper — not a fork of it) ──
143
-
144
- const getThemeScope = (el: Element | null): Element | null =>
145
- el?.closest("[data-theme]") ??
146
- (typeof document !== "undefined" ? document.documentElement : null);
143
+ // `getThemeScope` (nearest `[data-theme]` ancestor, defaulting to `<html>`)
144
+ // comes from the shared `_theme-scope-store` module — the exact same helper
145
+ // `code-block.tsx` uses, not a second copy of it.
147
146
 
148
147
  // ─── Intra-line syntax highlighting ────────────────────────────────────────
149
148
 
150
149
  type TokenizedResult = NonNullable<ReturnType<typeof highlightCode>>;
151
150
 
152
151
  /**
153
- * Highlights only the lines that are real source (`add` / `del` / `context`)
154
- * as one combined document, so Shiki sees genuine surrounding context instead
155
- * of tokenizing each line in isolation, then maps the result back onto the
156
- * original line indices. `hunk` / `meta` lines are diff headers, not code, and
157
- * are excluded from the document entirely.
152
+ * Highlights ONE side's combined document (see `useDiffTokens` below) and
153
+ * returns the raw Shiki result once it resolves. `isStreaming` is forwarded
154
+ * to `highlightCode`'s `skipCache` — a streaming diff re-tokenizes a growing
155
+ * document on every line, and every intermediate value is never seen again,
156
+ * so permanently caching it is pure waste (perf review 1.4a/§3.2).
158
157
  */
159
- function useDiffTokens(
160
- lines: DiffLine[],
158
+ function useSideTokens(
159
+ combinedCode: string,
161
160
  language: BundledLanguage | undefined,
162
161
  scopeEl: Element | null,
163
- ): Map<number, ThemedToken[]> | null {
164
- const codeIndices = useMemo(
165
- () =>
166
- lines
167
- .map((line, index) => ({ line, index }))
168
- .filter(({ line }) => line.type !== "hunk" && line.type !== "meta")
169
- .map(({ index }) => index),
170
- [lines],
171
- );
172
- const combinedCode = useMemo(
173
- () => codeIndices.map((index) => lines[index]?.text ?? "").join("\n"),
174
- [codeIndices, lines],
175
- );
176
-
162
+ isStreaming: boolean,
163
+ ): TokenizedResult | null {
177
164
  const [result, setResult] = useState<TokenizedResult | null>(null);
178
165
 
179
166
  useEffect(() => {
@@ -189,22 +176,90 @@ function useDiffTokens(
189
176
  if (!cancelled) setResult(r);
190
177
  },
191
178
  scopeEl,
179
+ isStreaming,
192
180
  );
193
181
  if (cached) setResult(cached);
194
182
  return () => {
195
183
  cancelled = true;
196
184
  };
197
- }, [combinedCode, language, scopeEl]);
185
+ }, [combinedCode, language, scopeEl, isStreaming]);
186
+
187
+ return result;
188
+ }
189
+
190
+ /**
191
+ * Highlights the OLD file (`del` + `context` lines) and the NEW file (`add` +
192
+ * `context` lines) as two SEPARATE Shiki documents, then maps each back onto
193
+ * the original line indices. `hunk` / `meta` lines are diff headers, not
194
+ * code, and are excluded from both documents.
195
+ *
196
+ * The two sides used to be combined into ONE document (every real line,
197
+ * `del`/`add`/`context` alike, joined in original order). That let a
198
+ * still-open construct on one side leak its tokenizer STATE across the
199
+ * boundary onto the other — a deleted, unterminated `/*` opened a block
200
+ * comment that swallowed the immediately-following ADDED line as commented-out
201
+ * text, even though the two lines never coexist in either real version of the
202
+ * file. Tokenizing old/new as their own documents means Shiki only ever sees
203
+ * genuine same-version context around a construct.
204
+ */
205
+ function useDiffTokens(
206
+ lines: DiffLine[],
207
+ language: BundledLanguage | undefined,
208
+ scopeEl: Element | null,
209
+ isStreaming: boolean,
210
+ ): Map<number, ThemedToken[]> | null {
211
+ const oldIndices = useMemo(
212
+ () =>
213
+ lines
214
+ .map((line, index) => ({ line, index }))
215
+ .filter(({ line }) => line.type === "del" || line.type === "context")
216
+ .map(({ index }) => index),
217
+ [lines],
218
+ );
219
+ const newIndices = useMemo(
220
+ () =>
221
+ lines
222
+ .map((line, index) => ({ line, index }))
223
+ .filter(({ line }) => line.type === "add" || line.type === "context")
224
+ .map(({ index }) => index),
225
+ [lines],
226
+ );
227
+
228
+ const oldCode = useMemo(
229
+ () => oldIndices.map((index) => lines[index]?.text ?? "").join("\n"),
230
+ [oldIndices, lines],
231
+ );
232
+ const newCode = useMemo(
233
+ () => newIndices.map((index) => lines[index]?.text ?? "").join("\n"),
234
+ [newIndices, lines],
235
+ );
236
+
237
+ const oldResult = useSideTokens(oldCode, language, scopeEl, isStreaming);
238
+ const newResult = useSideTokens(newCode, language, scopeEl, isStreaming);
198
239
 
199
240
  return useMemo(() => {
200
- if (!language || !result) return null;
241
+ if (!language) return null;
201
242
  const map = new Map<number, ThemedToken[]>();
202
- codeIndices.forEach((originalIndex, i) => {
203
- const tokenLine = result.tokens[i];
204
- if (tokenLine) map.set(originalIndex, tokenLine);
205
- });
243
+ // `del` lines exist ONLY in the old-file document.
244
+ if (oldResult) {
245
+ oldIndices.forEach((originalIndex, i) => {
246
+ if (lines[originalIndex]?.type !== "del") return;
247
+ const tokenLine = oldResult.tokens[i];
248
+ if (tokenLine) map.set(originalIndex, tokenLine);
249
+ });
250
+ }
251
+ // `add` AND `context` lines come from the new-file document — a `context`
252
+ // line reads identically in both versions, and picking one consistently
253
+ // (rather than whichever side happened to resolve last) keeps its color
254
+ // stable across re-highlights.
255
+ if (newResult) {
256
+ newIndices.forEach((originalIndex, i) => {
257
+ const tokenLine = newResult.tokens[i];
258
+ if (tokenLine) map.set(originalIndex, tokenLine);
259
+ });
260
+ }
206
261
  return map;
207
- }, [codeIndices, result, language]);
262
+ }, [oldIndices, newIndices, oldResult, newResult, language, lines]);
208
263
  }
209
264
 
210
265
  /** Renders pre-highlighted tokens; colour only (a diff row doesn't need bold/italic/underline). */
@@ -241,7 +296,7 @@ function CollapsedRow({ hiddenCount, onExpand }: { hiddenCount: number; onExpand
241
296
  <button
242
297
  type="button"
243
298
  onClick={onExpand}
244
- className="col-start-4 w-fit rounded-sm px-1 text-info-text text-meta hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
299
+ className="col-start-4 w-fit rounded-sm px-1 text-info-text text-meta hover:underline focus-ring"
245
300
  >
246
301
  {t("ai.diffView.showMore", { count: hiddenCount })}
247
302
  </button>
@@ -431,6 +486,7 @@ function DiffViewHeader({
431
486
  }) {
432
487
  const { t } = useLocale();
433
488
  return (
489
+ // header-band-exempt: a file header inside the diff card, not shell chrome
434
490
  <div
435
491
  data-slot="diff-view-header"
436
492
  className="flex items-center justify-between gap-3 border-b bg-muted/80 px-3 py-2 text-meta text-muted-foreground"
@@ -524,7 +580,7 @@ function DiffViewPager({
524
580
  tabIndex={0}
525
581
  onScroll={updatePercent}
526
582
  onKeyDown={onKeyDown}
527
- className="max-h-[32rem] overflow-y-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
583
+ className="max-h-[32rem] overflow-y-auto focus-ring-inset"
528
584
  >
529
585
  {children}
530
586
  </div>
@@ -573,7 +629,7 @@ export const DiffView = forwardRef<HTMLDivElement, DiffViewProps>(function DiffV
573
629
  () => (maxLines ? lines.slice(0, maxLines) : lines),
574
630
  [lines, maxLines],
575
631
  );
576
- const tokens = useDiffTokens(clippedLines, language, scopeEl);
632
+ const tokens = useDiffTokens(clippedLines, language, scopeEl, isStreaming);
577
633
  const { rows, expand } = useDiffRows(clippedLines, contextLines);
578
634
 
579
635
  const RowComponent = variant === "split" ? SplitRow : InlineRow;
@@ -4,18 +4,10 @@ import { Badge } from "@elabs-ai/components-ui";
4
4
  import { Button } from "@elabs-ai/components-ui";
5
5
  import { Switch } from "@elabs-ai/components-ui";
6
6
  import { cn } from "@elabs-ai/components-ui/lib/cn";
7
- import { useLocale } from "@elabs-ai/components-ui";
7
+ import { useCopyToClipboard, useLocale } from "@elabs-ai/components-ui";
8
8
  import { CheckIcon, CopyIcon, EyeIcon, EyeOffIcon } from "lucide-react";
9
9
  import type { ComponentProps, HTMLAttributes } from "react";
10
- import {
11
- createContext,
12
- useCallback,
13
- useContext,
14
- useEffect,
15
- useMemo,
16
- useRef,
17
- useState,
18
- } from "react";
10
+ import { createContext, useCallback, useContext, useMemo, useState } from "react";
19
11
 
20
12
  interface EnvironmentVariablesContextType {
21
13
  showValues: boolean;
@@ -86,7 +78,7 @@ export const EnvironmentVariablesTitle = ({
86
78
  children,
87
79
  ...props
88
80
  }: EnvironmentVariablesTitleProps) => (
89
- <h3 className={cn("font-medium text-sm", className)} {...props}>
81
+ <h3 className={cn("font-medium text-body", className)} {...props}>
90
82
  {children ?? "Environment Variables"}
91
83
  </h3>
92
84
  );
@@ -102,7 +94,7 @@ export const EnvironmentVariablesToggle = ({
102
94
 
103
95
  return (
104
96
  <div className={cn("flex items-center gap-2", className)}>
105
- <span className="text-muted-foreground text-xs">
97
+ <span className="text-muted-foreground text-meta">
106
98
  {showValues ? <EyeIcon size={14} /> : <EyeOffIcon size={14} />}
107
99
  </span>
108
100
  <Switch
@@ -159,7 +151,7 @@ export const EnvironmentVariableName = ({
159
151
  const { name } = useContext(EnvironmentVariableContext);
160
152
 
161
153
  return (
162
- <span className={cn("font-mono text-sm", className)} {...props}>
154
+ <span className={cn("font-mono text-body", className)} {...props}>
163
155
  {children ?? name}
164
156
  </span>
165
157
  );
@@ -180,7 +172,7 @@ export const EnvironmentVariableValue = ({
180
172
  return (
181
173
  <span
182
174
  className={cn(
183
- "font-mono text-muted-foreground text-sm",
175
+ "font-mono text-muted-foreground text-body",
184
176
  !showValues && "select-none",
185
177
  className,
186
178
  )}
@@ -234,15 +226,19 @@ export type EnvironmentVariableCopyButtonProps = ComponentProps<typeof Button> &
234
226
  export const EnvironmentVariableCopyButton = ({
235
227
  onCopy,
236
228
  onError,
237
- timeout = 2000,
229
+ timeout,
238
230
  copyFormat = "value",
239
231
  children,
240
232
  className,
233
+ "aria-label": ariaLabel,
241
234
  ...props
242
235
  }: EnvironmentVariableCopyButtonProps) => {
243
- const [isCopied, setIsCopied] = useState(false);
244
- const timeoutRef = useRef<number>(0);
236
+ const { t } = useLocale();
245
237
  const { name, value } = useContext(EnvironmentVariableContext);
238
+ // Shared implementation (`@elabs-ai/components-ui`) instead of a private
239
+ // copy of the same copy-to-clipboard state machine (issue-workflow.md
240
+ // dedupe finding) — see `CodeBlockCopyButton` for the reference usage.
241
+ const { copied: isCopied, copy } = useCopyToClipboard({ resetAfterMs: timeout ?? 2000 });
246
242
 
247
243
  const getTextToCopy = useCallback((): string => {
248
244
  const formatMap = {
@@ -254,32 +250,19 @@ export const EnvironmentVariableCopyButton = ({
254
250
  }, [name, value, copyFormat]);
255
251
 
256
252
  const copyToClipboard = useCallback(async () => {
257
- if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
258
- onError?.(new Error("Clipboard API not available"));
259
- return;
260
- }
261
-
262
- try {
263
- await navigator.clipboard.writeText(getTextToCopy());
264
- setIsCopied(true);
253
+ const ok = await copy(getTextToCopy());
254
+ if (ok) {
265
255
  onCopy?.();
266
- timeoutRef.current = window.setTimeout(() => setIsCopied(false), timeout);
267
- } catch (error) {
268
- onError?.(error as Error);
256
+ } else {
257
+ onError?.(new Error("Clipboard API not available"));
269
258
  }
270
- }, [getTextToCopy, onCopy, onError, timeout]);
271
-
272
- useEffect(
273
- () => () => {
274
- window.clearTimeout(timeoutRef.current);
275
- },
276
- [],
277
- );
259
+ }, [copy, getTextToCopy, onCopy, onError]);
278
260
 
279
261
  const Icon = isCopied ? CheckIcon : CopyIcon;
280
262
 
281
263
  return (
282
264
  <Button
265
+ aria-label={ariaLabel ?? t("copy")}
283
266
  className={cn("size-6 shrink-0", className)}
284
267
  onClick={copyToClipboard}
285
268
  size="icon"
@@ -298,7 +281,7 @@ export const EnvironmentVariableRequired = ({
298
281
  children,
299
282
  ...props
300
283
  }: EnvironmentVariableRequiredProps) => (
301
- <Badge className={cn("text-xs", className)} variant="secondary" {...props}>
284
+ <Badge className={cn("text-meta", className)} variant="secondary" {...props}>
302
285
  {children ?? "Required"}
303
286
  </Badge>
304
287
  );
@@ -25,6 +25,27 @@ describe("FileTree folder icons", () => {
25
25
  });
26
26
  });
27
27
 
28
+ describe("FileTreeFolder expand/collapse control (accessibility.md — icon-only controls need aria-label)", () => {
29
+ it("names the chevron toggle, and flips the name with expanded state", async () => {
30
+ const user = userEvent.setup();
31
+ render(
32
+ <FileTree defaultExpanded={new Set()}>
33
+ <FileTreeFolder path="src" name="src">
34
+ <FileTreeFile path="src/index.ts" name="index.ts" />
35
+ </FileTreeFolder>
36
+ </FileTree>,
37
+ );
38
+
39
+ const toggle = screen.getByRole("button", { name: "Expand src" });
40
+ expect(toggle).toBeInTheDocument();
41
+
42
+ await user.click(toggle);
43
+
44
+ expect(screen.getByRole("button", { name: "Collapse src" })).toBeInTheDocument();
45
+ expect(screen.queryByRole("button", { name: "Expand src" })).not.toBeInTheDocument();
46
+ });
47
+ });
48
+
28
49
  describe("FileTree variant axis (#193, research 04 §4 ASSET-1)", () => {
29
50
  it("keeps the IDE look by default (code variant — non-breaking)", () => {
30
51
  const { container } = render(
package/src/file-tree.tsx CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  CollapsibleContent,
6
6
  CollapsibleTrigger,
7
7
  fileIconFor,
8
+ useLocale,
8
9
  } from "@elabs-ai/components-ui";
9
10
  import { cn } from "@elabs-ai/components-ui/lib/cn";
10
11
  import { cva, type VariantProps } from "class-variance-authority";
@@ -50,7 +51,7 @@ const FileTreeContext = createContext<FileTreeContextType>({
50
51
  const fileTreeVariants = cva("", {
51
52
  variants: {
52
53
  variant: {
53
- code: "rounded-lg border bg-background font-mono text-sm",
54
+ code: "rounded-lg border bg-background font-mono text-body",
54
55
  document: "text-body",
55
56
  },
56
57
  },
@@ -158,6 +159,7 @@ export const FileTreeFolder = ({
158
159
  children,
159
160
  ...props
160
161
  }: FileTreeFolderProps) => {
162
+ const { t } = useLocale();
161
163
  const { expandedPaths, togglePath, selectedPath, onSelect } = useContext(FileTreeContext);
162
164
  const isExpanded = expandedPaths.has(path);
163
165
  const isSelected = selectedPath === path;
@@ -184,10 +186,15 @@ export const FileTreeFolder = ({
184
186
  >
185
187
  <CollapsibleTrigger asChild>
186
188
  <button
189
+ aria-label={t(
190
+ isExpanded ? "ai.fileTree.collapseFolder" : "ai.fileTree.expandFolder",
191
+ { name },
192
+ )}
187
193
  className="flex shrink-0 cursor-pointer items-center border-none bg-transparent p-0"
188
194
  type="button"
189
195
  >
190
196
  <ChevronRightIcon
197
+ aria-hidden="true"
191
198
  className={cn(
192
199
  "size-4 shrink-0 text-muted-foreground transition-transform",
193
200
  isExpanded && "rotate-90",
@@ -353,6 +360,7 @@ export type ProducedAssetTreeProps = Omit<
353
360
  */
354
361
  export const ProducedAssetTree = forwardRef<HTMLDivElement, ProducedAssetTreeProps>(
355
362
  function ProducedAssetTree({ assets, selectedId, onSelect, ...props }, ref) {
363
+ const { t } = useLocale();
356
364
  const assetsById = useMemo(() => new Map(assets.map((asset) => [asset.id, asset])), [assets]);
357
365
  const handleSelect = useCallback(
358
366
  (path: string) => {
@@ -361,6 +369,19 @@ export const ProducedAssetTree = forwardRef<HTMLDivElement, ProducedAssetTreePro
361
369
  },
362
370
  [assetsById, onSelect],
363
371
  );
372
+ // Empty: a plain note, not an empty `role="tree"` — a tree with no
373
+ // `treeitem` fails `aria-required-children` and announces nothing useful.
374
+ if (assets.length === 0) {
375
+ return (
376
+ <div
377
+ ref={ref}
378
+ data-slot="file-tree-empty"
379
+ className={cn("px-2 py-1 text-body text-muted-foreground", props.className)}
380
+ >
381
+ {t("ai.fileTree.noAssetsProduced")}
382
+ </div>
383
+ );
384
+ }
364
385
  return (
365
386
  <FileTree
366
387
  ref={ref}
@@ -369,21 +390,17 @@ export const ProducedAssetTree = forwardRef<HTMLDivElement, ProducedAssetTreePro
369
390
  onSelect={handleSelect}
370
391
  {...props}
371
392
  >
372
- {assets.length === 0 ? (
373
- <p className="px-2 py-1 text-body text-muted-foreground">No assets produced yet.</p>
374
- ) : (
375
- assets.map((asset) => {
376
- const Icon = assetIcon(asset);
377
- return (
378
- <FileTreeFile key={asset.id} path={asset.id} name={asset.name}>
379
- <FileTreeIcon>
380
- <Icon aria-hidden="true" className="size-4 text-muted-foreground" />
381
- </FileTreeIcon>
382
- <FileTreeName>{asset.name}</FileTreeName>
383
- </FileTreeFile>
384
- );
385
- })
386
- )}
393
+ {assets.map((asset) => {
394
+ const Icon = assetIcon(asset);
395
+ return (
396
+ <FileTreeFile key={asset.id} path={asset.id} name={asset.name}>
397
+ <FileTreeIcon>
398
+ <Icon aria-hidden="true" className="size-4 text-muted-foreground" />
399
+ </FileTreeIcon>
400
+ <FileTreeName>{asset.name}</FileTreeName>
401
+ </FileTreeFile>
402
+ );
403
+ })}
387
404
  </FileTree>
388
405
  );
389
406
  },
@@ -180,3 +180,45 @@ export const PartialSet: Story = {
180
180
  await expect(canvas.queryByText(/more/i)).not.toBeInTheDocument();
181
181
  },
182
182
  };
183
+
184
+ /**
185
+ * Focus indicator on image thumbnails (#313): Tab to an image button and
186
+ * assert the compound indicator's OUTLINE layer — `outlineStyle: "solid"`
187
+ * sourced from `--ring-contour`. The pre-#67 `focus-visible:outline-none
188
+ * focus-visible:ring-2 focus-visible:ring-ring` pattern also paints a
189
+ * non-"none" `boxShadow` (Tailwind's `ring-2` box-shadow trick), so asserting
190
+ * boxShadow alone passes on the legacy class string too; the outline layer is
191
+ * the one thing only `focus-ring`/`focus-ring-inset` add.
192
+ */
193
+ export const FocusIndicator: Story = {
194
+ name: "Keyboard focus",
195
+ args: { images: IMAGES.slice(0, 4) },
196
+ play: async ({ canvas, userEvent }) => {
197
+ const buttons = canvas.getAllByRole("button", { name: /fjord|desert/i });
198
+ await userEvent.tab();
199
+ await expect(buttons[0]).toHaveFocus();
200
+ const focused = getComputedStyle(buttons[0]);
201
+ await expect(focused.boxShadow).not.toBe("none");
202
+ await expect(focused.outlineStyle).toBe("solid");
203
+ await expect(parseFloat(focused.outlineWidth)).toBeGreaterThan(0);
204
+ },
205
+ };
206
+
207
+ /**
208
+ * Focus indicator on the overflow ("+N more") tile (#313): a separate DOM
209
+ * node from the plain thumbnails, styled the same way — never covered by any
210
+ * prior test (`gallery.tsx:409`, `GalleryOverflowTile`).
211
+ */
212
+ export const OverflowFocusIndicator: Story = {
213
+ name: "Overflow tile keyboard focus",
214
+ args: { images: IMAGES },
215
+ play: async ({ canvas, userEvent }) => {
216
+ const overflow = canvas.getByRole("button", { name: /show all 12 images/i });
217
+ overflow.focus();
218
+ await expect(overflow).toHaveFocus();
219
+ const focused = getComputedStyle(overflow);
220
+ await expect(focused.boxShadow).not.toBe("none");
221
+ await expect(focused.outlineStyle).toBe("solid");
222
+ await expect(parseFloat(focused.outlineWidth)).toBeGreaterThan(0);
223
+ },
224
+ };
package/src/gallery.tsx CHANGED
@@ -372,7 +372,7 @@ function GalleryTile({ image, index }: { image: GalleryImage; index: number }) {
372
372
  type="button"
373
373
  onClick={() => actions.open(index)}
374
374
  aria-label={label}
375
- className="block w-full rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
375
+ className="block w-full rounded-lg focus-ring"
376
376
  >
377
377
  <AspectRatio ratio={aspectRatio} className="overflow-hidden rounded-lg">
378
378
  {/* Decorative: the button carries the accessible name. */}
@@ -406,7 +406,7 @@ function GalleryOverflowTile() {
406
406
  type="button"
407
407
  onClick={() => actions.open(maxVisible)}
408
408
  aria-label={`Show all ${total} images`}
409
- className="block w-full rounded-lg border border-border bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
409
+ className="block w-full rounded-lg border border-border bg-muted focus-ring"
410
410
  >
411
411
  <AspectRatio ratio={aspectRatio} className="overflow-hidden rounded-lg">
412
412
  <span className="flex size-full items-center justify-center text-subtitle text-muted-foreground transition-colors duration-fast ease-standard group-hover:text-foreground motion-reduce:transition-none">
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, userEvent } from "storybook/test";
2
3
  import { GroupedParts } from "./grouped-parts";
3
4
  import { groupPartByType, type GroupablePart } from "./part-groups";
4
5
 
@@ -102,3 +103,25 @@ export const DarkTheme: Story = {
102
103
  ),
103
104
  ],
104
105
  };
106
+
107
+ /**
108
+ * Focus indicator on group triggers (#313): Tab to a collapsible trigger and
109
+ * assert the compound indicator's OUTLINE layer (`outlineStyle: "solid"`,
110
+ * `--ring-contour`) that `focus-ring-inset` adds. `boxShadow !== "none"`
111
+ * alone also passes on the legacy `focus-visible:outline-none
112
+ * focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset`
113
+ * pattern (Tailwind's `ring-2` still paints a box-shadow), so it does not
114
+ * lock the fix.
115
+ */
116
+ export const FocusIndicator: Story = {
117
+ args: { parts },
118
+ play: async ({ canvas, userEvent }) => {
119
+ const triggers = canvas.getAllByRole("button");
120
+ await userEvent.tab();
121
+ await expect(triggers[0]).toHaveFocus();
122
+ const focused = getComputedStyle(triggers[0]);
123
+ await expect(focused.boxShadow).not.toBe("none");
124
+ await expect(focused.outlineStyle).toBe("solid");
125
+ await expect(parseFloat(focused.outlineWidth)).toBeGreaterThan(0);
126
+ },
127
+ };
@@ -87,7 +87,7 @@ function DefaultGroup({
87
87
  className={cn(
88
88
  "group flex w-full items-center gap-2 p-3 text-body text-muted-foreground",
89
89
  "transition-colors duration-fast ease-standard hover:text-foreground motion-reduce:transition-none",
90
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
90
+ "focus-ring-inset",
91
91
  )}
92
92
  >
93
93
  <BrainIcon aria-hidden="true" className="size-4 shrink-0" />
package/src/index.ts CHANGED
@@ -47,6 +47,7 @@ export * from "./gallery";
47
47
  export * from "./grouped-parts";
48
48
  export * from "./image";
49
49
  export * from "./inline-citation";
50
+ export * from "./a2ui";
50
51
  export * from "./jsx-preview";
51
52
  // Mermaid loads on first diagram render, not in the entry chunk. `preloadMermaid`
52
53
  // warms it early when a surface is known to render diagrams.
@@ -208,7 +208,7 @@ export const InlineCitationCarouselIndex = ({
208
208
  return (
209
209
  <div
210
210
  className={cn(
211
- "flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-xs",
211
+ "flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-meta",
212
212
  className,
213
213
  )}
214
214
  {...props}
@@ -289,10 +289,10 @@ export const InlineCitationSource = ({
289
289
  ...props
290
290
  }: InlineCitationSourceProps) => (
291
291
  <div className={cn("space-y-1", className)} {...props}>
292
- {title && <h4 className="truncate font-medium text-sm leading-tight">{title}</h4>}
293
- {url && <p className="truncate break-all text-muted-foreground text-xs">{url}</p>}
292
+ {title && <h4 className="truncate font-medium text-body leading-tight">{title}</h4>}
293
+ {url && <p className="truncate break-all text-muted-foreground text-meta">{url}</p>}
294
294
  {description && (
295
- <p className="line-clamp-3 text-muted-foreground text-sm leading-relaxed">{description}</p>
295
+ <p className="line-clamp-3 text-muted-foreground text-body leading-relaxed">{description}</p>
296
296
  )}
297
297
  {children}
298
298
  </div>
@@ -306,7 +306,7 @@ export const InlineCitationQuote = ({
306
306
  ...props
307
307
  }: InlineCitationQuoteProps) => (
308
308
  <blockquote
309
- className={cn("border-muted border-s-2 ps-3 text-muted-foreground text-sm italic", className)}
309
+ className={cn("border-muted border-s-2 ps-3 text-muted-foreground text-body italic", className)}
310
310
  {...props}
311
311
  >
312
312
  {children}