@kitn.ai/ui 0.25.2 → 0.26.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 (409) hide show
  1. package/README.md +2 -2
  2. package/dist/components/attachment-types.d.ts +46 -1
  3. package/dist/components/attachments.d.ts +31 -0
  4. package/dist/components/audio-visualizer/fit-scale.d.ts +50 -0
  5. package/dist/components/audio-visualizer/index.d.ts +15 -1
  6. package/dist/components/audio-visualizer/variant-bar.d.ts +24 -1
  7. package/dist/components/chat-thread.d.ts +3 -41
  8. package/dist/components/code-block.d.ts +22 -0
  9. package/dist/components/conversation-item.d.ts +54 -1
  10. package/dist/components/conversation-list.d.ts +65 -0
  11. package/dist/components/form.d.ts +42 -0
  12. package/dist/components/message.d.ts +7 -0
  13. package/dist/components/toast.d.ts +2 -1
  14. package/dist/components/voice-input.d.ts +8 -0
  15. package/dist/components/voice-output.d.ts +12 -1
  16. package/dist/components/workspace-shell.d.ts +87 -0
  17. package/dist/{create-tween-B5Y4Im8G.js → create-tween-BFT6c-Y8.js} +1 -1
  18. package/dist/{create-tween-4z1XYLkR.js → create-tween-CJNlzoaO.js} +1 -1
  19. package/dist/{create-tween-CUzOlaMS.js → create-tween-CQED4T0g.js} +1 -1
  20. package/dist/custom-elements.json +2931 -2572
  21. package/dist/diagnostics/hook.d.ts +81 -0
  22. package/dist/diagnostics/index.d.ts +7 -0
  23. package/dist/diagnostics/report-request.d.ts +61 -0
  24. package/dist/diagnostics.d.ts +5 -0
  25. package/dist/diagnostics.js +209 -0
  26. package/dist/elements/agent-card.js +1 -1
  27. package/dist/elements/artifact.js +1 -1
  28. package/dist/elements/attachments.js +1 -1
  29. package/dist/elements/audio-visualizer.js +1 -1
  30. package/dist/elements/autoloader.js +1 -1
  31. package/dist/elements/avatar.js +1 -1
  32. package/dist/elements/badge.js +1 -1
  33. package/dist/elements/button.js +1 -1
  34. package/dist/elements/card.js +1 -1
  35. package/dist/elements/cards.js +1 -1
  36. package/dist/elements/chain-of-thought.js +1 -1
  37. package/dist/elements/chat-scope-picker.js +1 -1
  38. package/dist/elements/chat-workspace.js +1 -1
  39. package/dist/elements/chat.js +1 -1
  40. package/dist/elements/checkpoint.js +1 -1
  41. package/dist/elements/choice.js +1 -1
  42. package/dist/elements/chunks/{Icon-b5A8hlHs.js → Icon--Y8vsFal.js} +1 -1
  43. package/dist/elements/chunks/action-icons-D9u3OzYW.js +1 -0
  44. package/dist/elements/chunks/arrow-left-CVh1TeN7.js +1 -0
  45. package/dist/elements/chunks/{artifact-BSuPYFHY.js → artifact-BkYcfIlZ.js} +1 -1
  46. package/dist/elements/chunks/attachments-DwfNlubq.js +1 -0
  47. package/dist/elements/chunks/audio-visualizer-D7lB6mvR.js +1 -0
  48. package/dist/elements/chunks/{badge-TIMiJFKc.js → badge-Q0-Cmwzz.js} +1 -1
  49. package/dist/elements/chunks/{button-B9okqG73.js → button-flUbeufF.js} +1 -1
  50. package/dist/elements/chunks/{card-renderer-CSoTL_e1.js → card-renderer-3Dyl-FpN.js} +1 -1
  51. package/dist/elements/chunks/check-B5kBPKeo.js +1 -0
  52. package/dist/elements/chunks/chevron-down-DZ1GS39h.js +1 -0
  53. package/dist/elements/chunks/chevron-right-CvP7OUd2.js +1 -0
  54. package/dist/elements/chunks/{choice-card-CGMc3kek.js → choice-card-D4QUIOWZ.js} +1 -1
  55. package/dist/elements/chunks/circle-CDf9g6MQ.js +1 -0
  56. package/dist/elements/chunks/circle-check-CA47Nqbz.js +1 -0
  57. package/dist/elements/chunks/{circle-x-mhH7pOK2.js → circle-x-CJ_HVbC8.js} +1 -1
  58. package/dist/elements/chunks/code-block-BdLiTitd.js +1 -0
  59. package/dist/elements/chunks/collapsible-BzbXpPzr.js +1 -0
  60. package/dist/elements/chunks/{composer-CZZmtYOT.js → composer-Byve-n8K.js} +2 -2
  61. package/dist/elements/chunks/{confirm-card-BWjf2-xo.js → confirm-card-B0flE3MN.js} +1 -1
  62. package/dist/elements/chunks/context-VLIL3sJ-.js +1 -0
  63. package/dist/elements/chunks/controllable-DTwTIlwp.js +1 -0
  64. package/dist/elements/chunks/conversation-item-BDztezOG.js +1 -0
  65. package/dist/elements/chunks/copy-Nf-kqxXH.js +1 -0
  66. package/dist/elements/chunks/{create-tween-CUrM6D_o.js → create-tween-c4Z8TDYz.js} +2 -2
  67. package/dist/elements/chunks/default-input-C2iLFq0b.js +1 -0
  68. package/dist/elements/chunks/define-wjHR1MkU.js +1 -0
  69. package/dist/elements/chunks/disclosure-CcNbyLy0.js +1 -0
  70. package/dist/elements/chunks/{download-DnCyWFJE.js → download-C4V7tBdb.js} +1 -1
  71. package/dist/elements/chunks/dropdown-BgWTthUx.js +1 -0
  72. package/dist/elements/chunks/{ellipsis-CVxqfsDZ.js → ellipsis-7gU33mL2.js} +1 -1
  73. package/dist/elements/chunks/embed-D3KewsIJ.js +1 -0
  74. package/dist/elements/chunks/{external-link-DG2sRdQL.js → external-link-CSOEsNlx.js} +1 -1
  75. package/dist/elements/chunks/{file-text-D0J9F6M7.js → file-text-D1G8khle.js} +1 -1
  76. package/dist/elements/chunks/{file-tree-yGWNK0Js.js → file-tree-zvB2i7P9.js} +1 -1
  77. package/dist/elements/chunks/{folder-NV1pi_ud.js → folder-WW2LGLid.js} +1 -1
  78. package/dist/elements/chunks/form-CQirdC4W.js +1 -0
  79. package/dist/elements/chunks/hover-card-CJJkfHTK.js +1 -0
  80. package/dist/elements/chunks/icon-Cfgmf4fa.js +1 -0
  81. package/dist/elements/chunks/{info-C8r5Q6cc.js → info-DX3YMOt6.js} +1 -1
  82. package/dist/elements/chunks/input-BtW9U6Sq.js +1 -0
  83. package/dist/elements/chunks/{kbd-OGJDUT-O.js → kbd-LGXwuT_V.js} +1 -1
  84. package/dist/elements/chunks/{link-BdsfLB7b.js → link-2My69BqT.js} +1 -1
  85. package/dist/elements/chunks/{link-preview-Cf5KOP-i.js → link-preview-OQ9IEq0f.js} +1 -1
  86. package/dist/elements/chunks/{loader-B5QzK_8_.js → loader-iOxc_Tu-.js} +1 -1
  87. package/dist/elements/chunks/{markdown-DbozyYqa.js → markdown-DgBFaGAz.js} +1 -1
  88. package/dist/elements/chunks/{message-DtXKPqiR.js → message-VMZohrXx.js} +1 -1
  89. package/dist/elements/chunks/message-circle-CpXMM-_n.js +1 -0
  90. package/dist/elements/chunks/message-square-DxcWtETQ.js +1 -0
  91. package/dist/elements/chunks/message-tZmV2A3p.js +1 -0
  92. package/dist/elements/chunks/{minimize-2-SegJ_oku.js → minimize-2-DPIUKTrT.js} +1 -1
  93. package/dist/elements/chunks/{model-switcher-D7JrFFHp.js → model-switcher-DsT8tFB0.js} +1 -1
  94. package/dist/elements/chunks/{overlay-CV7z2YuX.js → overlay-DuiwdXjm.js} +1 -1
  95. package/dist/elements/chunks/{paperclip-Bg0ITU3b.js → paperclip-CRZVp4z_.js} +1 -1
  96. package/dist/elements/chunks/{progress-bar-D1_rzt6N.js → progress-bar-DNy66e_E.js} +1 -1
  97. package/dist/elements/chunks/{prompt-suggestion-D5Sm8xF1.js → prompt-suggestion-Bn64Nl7L.js} +1 -1
  98. package/dist/elements/chunks/reasoning-Emn9N4-T.js +1 -0
  99. package/dist/elements/chunks/{resizable-Dpzjlvp4.js → resizable-D181b4fM.js} +1 -1
  100. package/dist/elements/chunks/{rotate-cw-DzVDJOA1.js → rotate-cw-CPWe37D9.js} +1 -1
  101. package/dist/elements/chunks/{scroll-area-93A1bbeG.js → scroll-area-ClZkZxIk.js} +1 -1
  102. package/dist/elements/chunks/scroll-button-DPE-t3x_.js +1 -0
  103. package/dist/elements/chunks/{separator-DH2r_ZSZ.js → separator-CeJHQqLW.js} +1 -1
  104. package/dist/elements/chunks/{settings-BFMOd6yB.js → settings-CqaHgJF9.js} +1 -1
  105. package/dist/elements/chunks/{settings-group-B3VZKIcz.js → settings-group-CjqjLCOn.js} +1 -1
  106. package/dist/elements/chunks/share-CyfnNEqp.js +1 -0
  107. package/dist/elements/chunks/{skeleton-N8BuWYmf.js → skeleton-D8akBnKS.js} +1 -1
  108. package/dist/elements/chunks/slots-DaNjX0rc.js +1 -0
  109. package/dist/elements/chunks/{source-w9V0NnBn.js → source-UWCvmVak.js} +1 -1
  110. package/dist/elements/chunks/{star-DHvACxtm.js → star-DoHFAikQ.js} +1 -1
  111. package/dist/elements/chunks/store-Cjq0qwYN.js +1 -0
  112. package/dist/elements/chunks/{tasks-card-lPxoodlA.js → tasks-card-CmZ5xh6v.js} +1 -1
  113. package/dist/elements/chunks/{text-shimmer-BY_rI3s5.js → text-shimmer-DYn09zrz.js} +1 -1
  114. package/dist/elements/chunks/textarea-skiGHURM.js +1 -0
  115. package/dist/elements/chunks/{thumbs-up-DQe0hvkZ.js → thumbs-up-DX2Ijk3H.js} +1 -1
  116. package/dist/elements/chunks/{toast-store-VUZZBhzH.js → toast-store-CFnQpThX.js} +1 -1
  117. package/dist/elements/chunks/{tool-C1YtxwPu.js → tool-DKQ2D1DP.js} +1 -1
  118. package/dist/elements/chunks/{tooltip-BqwnFWhn.js → tooltip-B0G99y8w.js} +1 -1
  119. package/dist/elements/chunks/{triangle-alert-B6or7F5u.js → triangle-alert-F9cyUEyy.js} +1 -1
  120. package/dist/elements/chunks/{use-card-resolution-DJg32jGH.js → use-card-resolution-DoA2f_EU.js} +1 -1
  121. package/dist/elements/chunks/{variant-aurora-QXTZPyST.js → variant-aurora-qt6Zct8M.js} +2 -2
  122. package/dist/elements/chunks/variant-custom-tJp22_BQ.js +1 -0
  123. package/dist/{variant-wave-BwzrHD1s.js → elements/chunks/variant-wave-CaQ5ti39.js} +2 -2
  124. package/dist/elements/chunks/x-B6UnOiLw.js +1 -0
  125. package/dist/elements/coachmark.js +1 -1
  126. package/dist/elements/code-block.js +1 -1
  127. package/dist/elements/command.js +1 -1
  128. package/dist/elements/compare.js +1 -1
  129. package/dist/elements/composer.js +1 -1
  130. package/dist/elements/confirm-card.js +1 -1
  131. package/dist/elements/context-meter.js +1 -1
  132. package/dist/elements/conversation-item.d.ts +1 -0
  133. package/dist/elements/conversation-item.js +1 -0
  134. package/dist/elements/conversation-list.js +1 -1
  135. package/dist/elements/default-input.d.ts +3 -3
  136. package/dist/elements/define.d.ts +27 -0
  137. package/dist/elements/diagnostic-events.d.ts +74 -0
  138. package/dist/elements/dialog.js +1 -1
  139. package/dist/elements/dock.d.ts +1 -0
  140. package/dist/elements/dock.js +149 -0
  141. package/dist/elements/dropdown.d.ts +1 -0
  142. package/dist/elements/dropdown.js +1 -0
  143. package/dist/elements/editable-label.js +1 -1
  144. package/dist/elements/element-diagnostics.d.ts +81 -0
  145. package/dist/elements/embed.js +1 -1
  146. package/dist/elements/empty.js +1 -1
  147. package/dist/elements/feedback-bar.js +1 -1
  148. package/dist/elements/file-tree.js +1 -1
  149. package/dist/elements/file-upload.js +1 -1
  150. package/dist/elements/form.js +1 -1
  151. package/dist/elements/hover-card.js +1 -1
  152. package/dist/elements/icon.js +1 -1
  153. package/dist/elements/image.js +1 -1
  154. package/dist/elements/input.js +1 -1
  155. package/dist/elements/kbd.js +1 -1
  156. package/dist/elements/link-preview.js +1 -1
  157. package/dist/elements/loader.js +1 -1
  158. package/dist/elements/markdown.js +1 -1
  159. package/dist/elements/menu.js +1 -1
  160. package/dist/elements/message-skills.js +1 -1
  161. package/dist/elements/message.js +1 -1
  162. package/dist/elements/model-switcher.js +1 -1
  163. package/dist/elements/nav.js +1 -1
  164. package/dist/elements/notice.js +1 -1
  165. package/dist/elements/pane-group.js +1 -1
  166. package/dist/elements/pane.js +1 -1
  167. package/dist/elements/popover.js +1 -1
  168. package/dist/elements/progress-bar.js +1 -1
  169. package/dist/elements/prompt-dock.js +1 -1
  170. package/dist/elements/prompt-input.js +1 -1
  171. package/dist/elements/prompt-suggestions.js +1 -1
  172. package/dist/elements/reasoning.js +1 -1
  173. package/dist/elements/register.d.ts +2 -0
  174. package/dist/elements/remote.js +1 -1
  175. package/dist/elements/resizable.js +1 -1
  176. package/dist/elements/response-stream.js +3 -3
  177. package/dist/elements/screen.js +1 -1
  178. package/dist/elements/scroll-area.js +1 -1
  179. package/dist/elements/scroll-button.js +1 -1
  180. package/dist/elements/search.js +1 -1
  181. package/dist/elements/segmented.js +1 -1
  182. package/dist/elements/separator.js +1 -1
  183. package/dist/elements/setting-item.js +1 -1
  184. package/dist/elements/settings-group.js +1 -1
  185. package/dist/elements/skeleton.js +1 -1
  186. package/dist/elements/slots.d.ts +35 -3
  187. package/dist/elements/source.js +1 -1
  188. package/dist/elements/status.js +1 -1
  189. package/dist/elements/switch.js +1 -1
  190. package/dist/elements/tabs.js +1 -1
  191. package/dist/elements/tasks.js +1 -1
  192. package/dist/elements/text-shimmer.js +1 -1
  193. package/dist/elements/thinking-bar.js +1 -1
  194. package/dist/elements/thread.js +1 -1
  195. package/dist/elements/toast.js +1 -1
  196. package/dist/elements/tool.js +1 -1
  197. package/dist/elements/tooltip.js +1 -1
  198. package/dist/elements/voice-input.js +1 -1
  199. package/dist/elements/voice-output.js +1 -1
  200. package/dist/elements.d.ts +232 -148
  201. package/dist/index.d.ts +2 -0
  202. package/dist/index.js +5456 -4931
  203. package/dist/index.server.js +4142 -3669
  204. package/dist/kai-provider.es.js +40 -40
  205. package/dist/kai.es.js +1 -1
  206. package/dist/llms/llms-full.txt +184 -69
  207. package/dist/llms/llms.txt +3 -3
  208. package/dist/mcp.es.js +2669 -546
  209. package/dist/primitives/use-resize-observer.d.ts +23 -0
  210. package/dist/primitives/use-speech-recognition.d.ts +8 -0
  211. package/dist/primitives/use-text-stream.d.ts +5 -0
  212. package/dist/react/index.d.ts +115 -266
  213. package/dist/react.js +247 -221
  214. package/dist/register-impl-DpE7icIb.js +293 -0
  215. package/dist/schemas/index.d.ts +1 -1
  216. package/dist/schemas/tool-defs.d.ts +35 -0
  217. package/dist/schemas.js +403 -310
  218. package/dist/{solid-BMuDmFo_.js → solid-BtdqNZI7.js} +8435 -7394
  219. package/dist/{solid-CGYNdLRR.js → solid-CnavwBye.js} +5987 -5035
  220. package/dist/solid.d.ts +5 -0
  221. package/dist/solid.js +210 -203
  222. package/dist/solid.server.js +210 -203
  223. package/dist/state/index.d.ts +4 -0
  224. package/dist/state/persistence.d.ts +51 -0
  225. package/dist/state/stream.d.ts +50 -0
  226. package/dist/state/threads.d.ts +71 -0
  227. package/dist/state.js +269 -169
  228. package/dist/types.d.ts +4 -2
  229. package/dist/ui/dock.d.ts +109 -0
  230. package/dist/ui/hover-card.d.ts +44 -0
  231. package/dist/{variant-aurora-DwSOfraQ.js → variant-aurora-B8II3-I1.js} +27 -27
  232. package/dist/{variant-aurora-wKvAQ5wp.js → variant-aurora-CMNWOSJR.js} +28 -28
  233. package/dist/{variant-aurora-XEkPT0_z.js → variant-aurora-CcuDMWwQ.js} +2 -2
  234. package/dist/{variant-custom-eFIg2XWQ.js → variant-custom-BdvYC7af.js} +40 -40
  235. package/dist/{variant-custom-BhS9wIpU.js → variant-custom-C714mqKl.js} +65 -65
  236. package/dist/variant-custom-L16JLLZy.js +1 -0
  237. package/dist/{elements/chunks/variant-wave-DDCwhLas.js → variant-wave-2S4La2Ka.js} +2 -2
  238. package/dist/{variant-wave-CbVsmgwT.js → variant-wave-D7ruiA1W.js} +25 -25
  239. package/dist/{variant-wave-CUWGx9Bn.js → variant-wave-ecwykeyL.js} +21 -21
  240. package/dist/wire/chunk.d.ts +47 -0
  241. package/dist/wire/diagnostics.d.ts +575 -0
  242. package/dist/wire/encode-probe.d.ts +70 -0
  243. package/dist/wire/encode.d.ts +24 -0
  244. package/dist/wire/index.d.ts +2 -0
  245. package/dist/wire/sse.d.ts +10 -2
  246. package/dist/wire.js +917 -480
  247. package/frameworks/react/index.tsx +132 -93
  248. package/llms-full.txt +184 -69
  249. package/llms.txt +3 -3
  250. package/package.json +26 -6
  251. package/src/agent-tooling/archetypes.ts +41 -9
  252. package/src/agent-tooling/catalog/README.md +676 -0
  253. package/src/agent-tooling/catalog/catalog-types.ts +231 -0
  254. package/src/agent-tooling/catalog/fabrications.ts +96 -0
  255. package/src/agent-tooling/catalog/invariants.ts +284 -0
  256. package/src/agent-tooling/catalog/labs-titles.ts +114 -0
  257. package/src/agent-tooling/catalog/scenarios.ts +87 -0
  258. package/src/agent-tooling/catalog/surfaces.ts +275 -0
  259. package/src/agent-tooling/integrations/anthropic.ts +7 -1
  260. package/src/agent-tooling/integrations/cloudflare.ts +43 -2
  261. package/src/agent-tooling/integrations/langgraph.ts +7 -1
  262. package/src/agent-tooling/integrations/mastra.ts +7 -1
  263. package/src/agent-tooling/integrations/mock.ts +92 -17
  264. package/src/agent-tooling/integrations/ollama.ts +7 -1
  265. package/src/agent-tooling/integrations/openai.ts +7 -1
  266. package/src/agent-tooling/integrations/openrouter.ts +7 -1
  267. package/src/agent-tooling/integrations/pi.ts +56 -1
  268. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +7 -1
  269. package/src/agent-tooling/mcp/css-raw.d.ts +12 -0
  270. package/src/agent-tooling/mcp/manifest.ts +100 -1
  271. package/src/agent-tooling/mcp/server.ts +61 -5
  272. package/src/agent-tooling/mcp/tools/reference.ts +440 -4
  273. package/src/agent-tooling/mcp/tools/scaffold.ts +1834 -307
  274. package/src/agent-tooling/mcp/tools/theme.ts +146 -30
  275. package/src/agent-tooling/mcp/validate-args.ts +141 -0
  276. package/src/agent-tooling/registry.ts +19 -7
  277. package/src/agent-tooling/route-emit.ts +26 -2
  278. package/src/components/attachment-types.ts +47 -1
  279. package/src/components/attachments.tsx +169 -22
  280. package/src/components/audio-visualizer/fit-scale.ts +112 -0
  281. package/src/components/audio-visualizer/index.tsx +80 -3
  282. package/src/components/audio-visualizer/variant-aurora.tsx +10 -4
  283. package/src/components/audio-visualizer/variant-bar.tsx +38 -6
  284. package/src/components/audio-visualizer/variant-custom.tsx +8 -2
  285. package/src/components/audio-visualizer/variant-grid.tsx +16 -11
  286. package/src/components/audio-visualizer/variant-radial.tsx +15 -10
  287. package/src/components/audio-visualizer/variant-wave.tsx +7 -2
  288. package/src/components/chat-container.tsx +4 -1
  289. package/src/components/chat-thread.tsx +36 -7
  290. package/src/components/code-block.tsx +79 -3
  291. package/src/components/composer.tsx +8 -1
  292. package/src/components/conversation-item.tsx +114 -4
  293. package/src/components/conversation-list.tsx +212 -12
  294. package/src/components/embed.tsx +4 -0
  295. package/src/components/form.tsx +41 -14
  296. package/src/components/message.tsx +75 -5
  297. package/src/components/response-stream.tsx +13 -7
  298. package/src/components/screen.tsx +5 -0
  299. package/src/components/scroll-button.tsx +7 -0
  300. package/src/components/toast.tsx +11 -5
  301. package/src/components/voice-input.tsx +35 -3
  302. package/src/components/voice-output.tsx +46 -6
  303. package/src/components/workspace-shell.tsx +285 -0
  304. package/src/diagnostics/hook.ts +352 -0
  305. package/src/diagnostics/index.ts +82 -0
  306. package/src/diagnostics/report-request.ts +296 -0
  307. package/src/elements/attachments.tsx +6 -7
  308. package/src/elements/audio-visualizer.tsx +48 -4
  309. package/src/elements/chat-workspace.tsx +159 -287
  310. package/src/elements/chat.tsx +12 -9
  311. package/src/elements/code-block.tsx +19 -1
  312. package/src/elements/command.tsx +4 -1
  313. package/src/elements/compiled.css +1 -1
  314. package/src/elements/conversation-item.tsx +80 -0
  315. package/src/elements/conversation-list.tsx +60 -11
  316. package/src/elements/default-input.tsx +5 -5
  317. package/src/elements/define.tsx +168 -8
  318. package/src/elements/diagnostic-events.ts +114 -0
  319. package/src/elements/dialog.tsx +47 -0
  320. package/src/elements/disclosure.ts +34 -12
  321. package/src/elements/dock.tsx +204 -0
  322. package/src/elements/dropdown.tsx +123 -0
  323. package/src/elements/element-diagnostics.ts +392 -0
  324. package/src/elements/element-manifest.json +12 -0
  325. package/src/elements/element-meta.json +672 -439
  326. package/src/elements/element-nonscalar.json +170 -0
  327. package/src/elements/element-types.d.ts +232 -148
  328. package/src/elements/menu.tsx +13 -0
  329. package/src/elements/prompt-dock.tsx +2 -1
  330. package/src/elements/prompt-input.tsx +8 -8
  331. package/src/elements/register-impl.ts +46 -0
  332. package/src/elements/register.ts +28 -0
  333. package/src/elements/resizable.tsx +52 -25
  334. package/src/elements/scroll-button.tsx +5 -0
  335. package/src/elements/slots.ts +107 -12
  336. package/src/elements/styles.css +30 -6
  337. package/src/elements/thinking-bar.tsx +16 -7
  338. package/src/elements/toast.tsx +3 -0
  339. package/src/elements/tool.tsx +26 -5
  340. package/src/elements/voice-input.tsx +11 -2
  341. package/src/elements/voice-output.tsx +16 -3
  342. package/src/index.ts +2 -0
  343. package/src/primitives/use-resize-observer.ts +33 -0
  344. package/src/primitives/use-speech-recognition.ts +12 -1
  345. package/src/primitives/use-stick-to-bottom.ts +34 -2
  346. package/src/primitives/use-text-stream.ts +33 -2
  347. package/src/remote/provider-runtime.ts +6 -3
  348. package/src/schemas/index.ts +1 -0
  349. package/src/schemas/tool-defs.ts +297 -6
  350. package/src/solid.ts +5 -0
  351. package/src/state/index.ts +12 -0
  352. package/src/state/persistence.ts +180 -0
  353. package/src/state/stream.ts +71 -4
  354. package/src/state/threads.ts +142 -0
  355. package/src/types.ts +4 -2
  356. package/src/ui/dialog.tsx +6 -0
  357. package/src/ui/dock.tsx +445 -0
  358. package/src/ui/dropdown.tsx +67 -23
  359. package/src/ui/hover-card.tsx +156 -6
  360. package/src/ui/input.tsx +29 -10
  361. package/src/ui/resizable.tsx +11 -0
  362. package/src/wire/chunk.ts +47 -0
  363. package/src/wire/consume.ts +185 -26
  364. package/src/wire/diagnostics.ts +727 -0
  365. package/src/wire/encode-probe.ts +214 -0
  366. package/src/wire/encode.ts +267 -14
  367. package/src/wire/formats/anthropic.ts +8 -1
  368. package/src/wire/formats/openai.ts +5 -0
  369. package/src/wire/index.ts +22 -0
  370. package/src/wire/read.ts +250 -6
  371. package/src/wire/sse.ts +16 -3
  372. package/dist/elements/chunks/action-icons-DW9muWrY.js +0 -1
  373. package/dist/elements/chunks/arrow-left-iJVnQmxA.js +0 -1
  374. package/dist/elements/chunks/attachments-CL6TaqIb.js +0 -1
  375. package/dist/elements/chunks/audio-visualizer-BuUqQmaO.js +0 -1
  376. package/dist/elements/chunks/chat-thread-DxoKaStk.js +0 -1
  377. package/dist/elements/chunks/check-MdLzbxrm.js +0 -1
  378. package/dist/elements/chunks/chevron-down-MK7IzDqe.js +0 -1
  379. package/dist/elements/chunks/chevron-right-C5jXhmtV.js +0 -1
  380. package/dist/elements/chunks/circle-BG7VkPep.js +0 -1
  381. package/dist/elements/chunks/circle-check-GqvO2UBs.js +0 -1
  382. package/dist/elements/chunks/code-block-ClhQO9RW.js +0 -1
  383. package/dist/elements/chunks/collapsible-DBcBkFPh.js +0 -1
  384. package/dist/elements/chunks/context-hKjNqiyQ.js +0 -1
  385. package/dist/elements/chunks/conversation-list-CsGJjSKG.js +0 -1
  386. package/dist/elements/chunks/default-input-B1Mem5wu.js +0 -1
  387. package/dist/elements/chunks/define-B_tXMYRm.js +0 -1
  388. package/dist/elements/chunks/disclosure-DAoYOKew.js +0 -1
  389. package/dist/elements/chunks/dropdown-CmNna5lT.js +0 -1
  390. package/dist/elements/chunks/embed-CjAD2jSi.js +0 -1
  391. package/dist/elements/chunks/form-Cp8Y00Wn.js +0 -1
  392. package/dist/elements/chunks/hover-card-6KtXpLwT.js +0 -1
  393. package/dist/elements/chunks/icon-97R8SR2p.js +0 -1
  394. package/dist/elements/chunks/input-DrseEjhp.js +0 -1
  395. package/dist/elements/chunks/message-CE4k-qaj.js +0 -1
  396. package/dist/elements/chunks/message-square-C9ovmtOo.js +0 -1
  397. package/dist/elements/chunks/reasoning-BrxQfpfI.js +0 -1
  398. package/dist/elements/chunks/scroll-button-CrTo7wm4.js +0 -1
  399. package/dist/elements/chunks/share-DgNbkJKY.js +0 -1
  400. package/dist/elements/chunks/slots-CIw9RlAe.js +0 -1
  401. package/dist/elements/chunks/store-HIPGhNz9.js +0 -1
  402. package/dist/elements/chunks/textarea-DPfuhswM.js +0 -1
  403. package/dist/elements/chunks/variant-custom-D5ZlmziF.js +0 -1
  404. package/dist/elements/chunks/video-Bb86fVYI.js +0 -1
  405. package/dist/elements/chunks/x-BOhjBeOd.js +0 -1
  406. package/dist/primitives/card-validate-generator.testlib.d.ts +0 -20
  407. package/dist/register-impl-DGmGzrEM.js +0 -145
  408. package/dist/variant-custom-MkTNTycd.js +0 -1
  409. package/src/primitives/card-validate-generator.testlib.ts +0 -38
@@ -0,0 +1,142 @@
1
+ // src/state/threads.ts — the thread-switching fold.
2
+ //
3
+ // The identity category from the workspace glue inventory (spec §1/§3b): an
4
+ // id-bound `SetMessages`, per-thread loading state, the abort map, and
5
+ // delete-under-stream safety. Pure folds + callbacks per the state charter —
6
+ // no fetch, no DOM, no storage; the only state a helper here owns is the
7
+ // irreducible kind (which turns are in flight), never the consumer's records.
8
+ import type { ChatMessage } from '../elements/chat-types';
9
+ import type { SetMessages } from './stream';
10
+
11
+ /** The minimal thread record the fold needs. The consumer's own record type
12
+ * (title, timestamps, whatever its storage keeps) extends this — those fields
13
+ * are policy and stay outside the kit. */
14
+ export interface ThreadLike {
15
+ id: string;
16
+ messages: ChatMessage[];
17
+ }
18
+
19
+ /** The functional-updater setter over the whole thread LIST — the same
20
+ * React-setState shape `SetMessages` is, one level up. */
21
+ export type SetThreads<T extends ThreadLike> = (updater: (prev: T[]) => T[]) => void;
22
+
23
+ export interface BindThreadOptions<T extends ThreadLike> {
24
+ /** Stamp the changed record (e.g. `updatedAt`). Runs on a hit only. Kept a
25
+ * callback because WHAT gets stamped — and with whose clock — is the
26
+ * consumer's record shape, not ours. */
27
+ touch?: (thread: T) => T;
28
+ /** A delta arrived for a thread that is no longer in the list (deleted while
29
+ * its reply was streaming). The delta is dropped either way — dropping is
30
+ * the only correct fold — but the drop is reported, not silent. */
31
+ onDrop?: (threadId: string) => void;
32
+ }
33
+
34
+ /**
35
+ * Pure fold: apply `updater` to the messages of thread `id` within a list.
36
+ *
37
+ * reactivity-two-halves (CLAUDE.md): the hit gets a NEW containing array
38
+ * (which notifies) and a NEW record object (which makes the change visible);
39
+ * every other record keeps its reference. A MISS returns `threads` itself —
40
+ * the same reference — so a late delta for a deleted thread re-renders
41
+ * nothing and resurrects nothing.
42
+ */
43
+ export function updateThreadMessages<T extends ThreadLike>(
44
+ threads: T[],
45
+ id: string,
46
+ updater: (prev: ChatMessage[]) => ChatMessage[],
47
+ opts: BindThreadOptions<T> = {},
48
+ ): T[] {
49
+ let hit = false;
50
+ const next = threads.map((t) => {
51
+ if (t.id !== id) return t;
52
+ hit = true;
53
+ const updated = { ...t, messages: updater(t.messages) };
54
+ return opts.touch ? opts.touch(updated) : updated;
55
+ });
56
+ if (!hit) {
57
+ opts.onDrop?.(id);
58
+ return threads;
59
+ }
60
+ return next;
61
+ }
62
+
63
+ /**
64
+ * A `SetMessages` bound to ONE thread. Capture the id when the user hits send
65
+ * and hand the result to `createAssistantStream`: the reply then lands in the
66
+ * thread that was open at send time, never whichever one is open when the
67
+ * tokens arrive — which is what makes switching away mid-reply (and coming
68
+ * back to a finished answer) safe.
69
+ */
70
+ export function bindThreadMessages<T extends ThreadLike>(
71
+ setThreads: SetThreads<T>,
72
+ id: string,
73
+ opts: BindThreadOptions<T> = {},
74
+ ): SetMessages {
75
+ return (updater) => {
76
+ setThreads((prev) => updateThreadMessages(prev, id, updater, opts));
77
+ };
78
+ }
79
+
80
+ /** The in-flight-turn bookkeeping for a multi-thread app: which threads are
81
+ * streaming, and the AbortController for each. See `createThreadSessions`. */
82
+ export interface ThreadSessions {
83
+ /** Register a new in-flight turn. One turn per thread: a prior in-flight
84
+ * turn for the SAME thread is aborted first. Pass the returned controller's
85
+ * `signal` to your fetch — the fetch line itself stays yours. */
86
+ begin(threadId: string): AbortController;
87
+ /** The turn settled (done or failed) — call from your `finally`. `controller`
88
+ * is required so a slow old turn's cleanup cannot clear a newer turn that
89
+ * has since begun on the same thread: a stale controller is a no-op. */
90
+ end(threadId: string, controller: AbortController): void;
91
+ /** Cancel the in-flight turn (the delete-under-stream story): aborts the
92
+ * controller and clears the streaming state. Returns whether anything was
93
+ * actually in flight. Pair it with the id-bound sink above, which drops any
94
+ * deltas that were already past the abort. */
95
+ abort(threadId: string): boolean;
96
+ isStreaming(threadId: string): boolean;
97
+ /** A fresh array snapshot each call — safe to hand straight to a setState. */
98
+ streamingIds(): string[];
99
+ }
100
+
101
+ /**
102
+ * Create the session tracker. `onChange` fires with a fresh `streamingIds()`
103
+ * snapshot after every transition — mirror it into your framework's state to
104
+ * derive `loading` (`sessions.isStreaming(activeId)` is the per-thread flag a
105
+ * single boolean cannot be, because a stream keeps running when you switch
106
+ * away).
107
+ *
108
+ * This is the one stateful object in the module, because an abort map is
109
+ * irreducibly stateful; it still does no I/O and owns no timer.
110
+ */
111
+ export function createThreadSessions(onChange?: (streamingIds: string[]) => void): ThreadSessions {
112
+ const inFlight = new Map<string, AbortController>();
113
+ const snapshot = () => Array.from(inFlight.keys());
114
+ const notify = () => onChange?.(snapshot());
115
+
116
+ return {
117
+ begin(threadId) {
118
+ inFlight.get(threadId)?.abort();
119
+ const controller = new AbortController();
120
+ inFlight.set(threadId, controller);
121
+ notify();
122
+ return controller;
123
+ },
124
+ end(threadId, controller) {
125
+ if (inFlight.get(threadId) !== controller) return;
126
+ inFlight.delete(threadId);
127
+ notify();
128
+ },
129
+ abort(threadId) {
130
+ const controller = inFlight.get(threadId);
131
+ if (!controller) return false;
132
+ controller.abort();
133
+ inFlight.delete(threadId);
134
+ notify();
135
+ return true;
136
+ },
137
+ isStreaming(threadId) {
138
+ return inFlight.has(threadId);
139
+ },
140
+ streamingIds: snapshot,
141
+ };
142
+ }
package/src/types.ts CHANGED
@@ -31,9 +31,11 @@ export interface ConversationSummary {
31
31
  id: string;
32
32
  title: string;
33
33
  groupId?: string;
34
- scope: ConversationScope;
34
+ /** Only meaningful to scope-aware consumers; the list never reads it. */
35
+ scope?: ConversationScope;
35
36
  messageCount: number;
36
- lastMessageAt: string;
37
+ /** Fallback for the auto relative time when updatedAt is absent. */
38
+ lastMessageAt?: string;
37
39
  updatedAt: string;
38
40
  /** Right-aligned trailing text on the row — a count, status, or "days ago".
39
41
  * When omitted, a short relative time is auto-derived from `updatedAt`
package/src/ui/dialog.tsx CHANGED
@@ -190,6 +190,12 @@ export function Dialog(props: DialogProps) {
190
190
  tabindex={-1}
191
191
  data-expanded={presence.state() === 'open' ? '' : undefined}
192
192
  data-closed={presence.state() === 'closed' ? '' : undefined}
193
+ // `outline-none` is DELIBERATE here and must stay. This is a
194
+ // `tabindex={-1}` focus-trap container: it is focused
195
+ // programmatically on open to move keyboard and screen-reader
196
+ // context into the dialog, and is never a tab stop. Drawing a ring
197
+ // would put a blue box around the entire dialog every time it
198
+ // opened. The controls inside it carry their own focus rings.
193
199
  class={cn(
194
200
  'flex max-h-[85vh] w-full max-w-lg flex-col overflow-hidden rounded-xl border border-border bg-background text-foreground shadow-xl outline-none',
195
201
  'animate-in fade-in-0 zoom-in-95 data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95 motion-reduce:animate-none',
@@ -0,0 +1,445 @@
1
+ import {
2
+ createEffect, createSignal, createUniqueId, Show, untrack, type Accessor, type JSX,
3
+ } from 'solid-js';
4
+ import { MessageCircle, X } from 'lucide-solid';
5
+
6
+ /** Which corner the dock sits in. LOGICAL, so `-end` follows the writing direction
7
+ * and an RTL page docks on the left with no extra work (both reference
8
+ * implementations reached for `inset-inline-end` independently). */
9
+ export type DockPosition = 'bottom-end' | 'bottom-start' | 'top-end' | 'top-start';
10
+
11
+ /** Where focus goes when the panel opens.
12
+ * - `content` (default) — `focus()` the first element assigned to the panel, falling
13
+ * back to the panel itself when that element cannot take focus. Content-AGNOSTIC:
14
+ * the dock never learns what it is holding.
15
+ * - `panel` — focus the `tabindex="-1"` panel container (what `kai-dialog` does).
16
+ * - `none` — never move focus. */
17
+ export type DockFocusOnOpen = 'content' | 'panel' | 'none';
18
+
19
+ /** Imperative open controller, handed to a parent (the `kai-dock` facade) via
20
+ * `controllerRef` so it can drive/observe open state with `wireDisclosure`. */
21
+ export interface DockController { open: Accessor<boolean>; setOpen: (v: boolean) => void }
22
+
23
+ export interface DockProps {
24
+ /** The panel body. ANY content — the dock never reads or types it. */
25
+ children?: JSX.Element;
26
+ /** Content inside the built-in button while CLOSED. Defaults to a chat glyph. */
27
+ launcher?: JSX.Element;
28
+ /** Content inside the button while OPEN. Defaults to a ✕; when only `launcher` is
29
+ * given, that content stays rather than morphing into a clashing built-in. */
30
+ launcherOpen?: JSX.Element;
31
+ /** The widget's NAME. Derives the panel's accessible name and both launcher names
32
+ * (`Open ${label}` / `Close ${label}`) — one string instead of three. */
33
+ label?: string;
34
+ /** i18n override for the launcher's name while closed. */
35
+ openLabel?: string;
36
+ /** i18n override for the launcher's name while open. */
37
+ closeLabel?: string;
38
+ /** Which corner. Logical, RTL-correct. Defaults to `bottom-end`. */
39
+ position?: DockPosition;
40
+ /** Render the unread dot. CONSUMER-OWNED: shown only while closed, and never
41
+ * written back by the dock (see the component doc). */
42
+ unread?: boolean;
43
+ /** Disable the launcher. */
44
+ disabled?: boolean;
45
+ /** Where focus lands on open. Defaults to `content`. */
46
+ focusOnOpen?: DockFocusOnOpen;
47
+ /** Controlled open state. When set, the component never changes it itself — drive it
48
+ * from `onOpenChange`. Omit for uncontrolled (internal) state. */
49
+ open?: boolean;
50
+ /** Initial open state when uncontrolled. Does NOT move focus (see the focus contract). */
51
+ defaultOpen?: boolean;
52
+ /** Fires whenever the dock wants to open or close (launcher / Escape / a method). */
53
+ onOpenChange?: (open: boolean) => void;
54
+ /** Receive the open controller once mounted. */
55
+ controllerRef?: (api: DockController) => void;
56
+ /** Receive the focusable panel node so a facade's `focus()` can target it. */
57
+ panelRef?: (el: HTMLElement) => void;
58
+ /** Receive the launcher button so a facade's `focus()` can target it. */
59
+ launcherRef?: (el: HTMLButtonElement) => void;
60
+ /**
61
+ * Resolve the element `focusOnOpen: 'content'` should focus.
62
+ *
63
+ * A PROP, because the answer is "the first element assigned to the panel slot" and
64
+ * only the web-component facade can see slot assignments — a Solid caller passes
65
+ * its own ref instead. Returning nothing falls back to focusing the panel.
66
+ */
67
+ contentTarget?: () => HTMLElement | null | undefined;
68
+ }
69
+
70
+ /** The built-in closed glyph. Exported so the facade can use it as its `launcher`
71
+ * slot's FALLBACK content — one definition, two users, rather than a second copy
72
+ * of the same icon choice living in the element layer. */
73
+ export function DockLauncherGlyph(): JSX.Element {
74
+ return <MessageCircle size={24} />;
75
+ }
76
+
77
+ /** The built-in open glyph. Exported for the same reason as {@link DockLauncherGlyph}. */
78
+ export function DockCloseGlyph(): JSX.Element {
79
+ return <X size={24} />;
80
+ }
81
+
82
+ /**
83
+ * The dock's own stylesheet, scoped to its shadow root.
84
+ *
85
+ * IN A `<style>` RATHER THAN INLINE, and that is a decision. `kai-prompt-dock` puts
86
+ * its tokenized chrome in an inline `style={{}}`, which resolves `var()` fallbacks
87
+ * and lets an outside override win — but an inline declaration also beats every rule
88
+ * in the cascade, and this element ships TWO rules that have to be able to win: the
89
+ * narrow-viewport full-bleed default and the reduced-motion rule. Both are media
90
+ * queries, which cannot be expressed inline at all. So the geometry lives here and
91
+ * only the open/closed state (visibility / opacity / pointer-events) is inline, where
92
+ * it belongs: that one must beat any consumer rule, and it is also the half a jsdom
93
+ * test can actually read.
94
+ *
95
+ * EVERY RULE IS SCOPED UNDER `[data-kai-dock]`, including the `[part=...]` ones. Inside
96
+ * a shadow root that scoping is redundant; in a plain Solid page it is the difference
97
+ * between styling this dock and restyling every `[part="panel"]` on the document.
98
+ *
99
+ * DOCUMENTED CONSTRAINT, which the element cannot fix from inside: put `<kai-dock>`
100
+ * as a body-level child. A `transform` / `filter` / `contain` ancestor makes
101
+ * `position: fixed` resolve against that ancestor instead of the viewport.
102
+ */
103
+ const DOCK_CSS = `
104
+ :host { display: contents; }
105
+
106
+ [data-kai-dock] {
107
+ position: fixed;
108
+ z-index: var(--kai-dock-z, 1000);
109
+ display: flex;
110
+ gap: var(--kai-dock-gap, 0.85rem);
111
+ /* Only a positioning frame: it must not swallow clicks on the page it floats
112
+ over. The launcher and the open panel opt back in. */
113
+ pointer-events: none;
114
+ }
115
+
116
+ /* Logical insets throughout, so an RTL page docks on the other side for free.
117
+ column-reverse for the bottom corners keeps the LAUNCHER FIRST IN DOM ORDER --
118
+ Tab reaches the control before the panel content -- while painting it below. */
119
+ [data-kai-dock][data-position^="bottom"] {
120
+ flex-direction: column-reverse;
121
+ inset-block-end: var(--kai-dock-inset, 1.5rem);
122
+ }
123
+ [data-kai-dock][data-position^="top"] {
124
+ flex-direction: column;
125
+ inset-block-start: var(--kai-dock-inset, 1.5rem);
126
+ }
127
+ [data-kai-dock][data-position$="end"] {
128
+ inset-inline-end: var(--kai-dock-inset, 1.5rem);
129
+ align-items: flex-end;
130
+ }
131
+ [data-kai-dock][data-position$="start"] {
132
+ inset-inline-start: var(--kai-dock-inset, 1.5rem);
133
+ align-items: flex-start;
134
+ }
135
+
136
+ [data-kai-dock] [part="launcher"] {
137
+ pointer-events: auto;
138
+ position: relative;
139
+ /* ABOVE the panel, always. At <=480px the panel goes fixed/inset:0 and it comes
140
+ after the launcher in DOM order, so at z-index:auto it painted OVER the launcher
141
+ and a touch user lost every pointer route to closing. Both reference
142
+ implementations stacked the launcher over the panel for the same reason. */
143
+ z-index: 1;
144
+ flex: none;
145
+ display: inline-flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ gap: 0.5rem;
149
+ /* The size token is the BLOCK size and the inline MINIMUM, not a fixed square. An
150
+ icon-only launcher is still a perfect disc (the icon plus padding is narrower
151
+ than the minimum), but a slotted TEXT label — the reference app's "Support"
152
+ pill — widens the button instead of overflowing it. */
153
+ inline-size: auto;
154
+ min-inline-size: var(--kai-dock-launcher-size, 56px);
155
+ block-size: var(--kai-dock-launcher-size, 56px);
156
+ padding-block: 0;
157
+ padding-inline: 0.9rem;
158
+ white-space: nowrap;
159
+ border: 0;
160
+ border-radius: 9999px;
161
+ background: var(--color-primary);
162
+ color: var(--color-primary-foreground);
163
+ box-shadow: 0 10px 25px -6px rgb(0 0 0 / 0.35);
164
+ cursor: pointer;
165
+ transition: transform 150ms ease, box-shadow 150ms ease;
166
+ }
167
+ [data-kai-dock] [part="launcher"]:hover:not(:disabled) { transform: translateY(-1px); }
168
+ [data-kai-dock] [part="launcher"]:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; }
169
+ [data-kai-dock] [part="launcher"]:disabled { opacity: 0.55; cursor: not-allowed; }
170
+
171
+ [data-kai-dock] [part="badge"] {
172
+ position: absolute;
173
+ inset-block-start: 3px;
174
+ inset-inline-end: 3px;
175
+ inline-size: 0.75rem;
176
+ block-size: 0.75rem;
177
+ border-radius: 9999px;
178
+ background: var(--color-destructive);
179
+ box-shadow: 0 0 0 2px var(--color-primary);
180
+ }
181
+
182
+ [data-kai-dock] [part="panel"] {
183
+ pointer-events: auto;
184
+ display: flex;
185
+ flex-direction: column;
186
+ overflow: hidden;
187
+ inline-size: var(--kai-dock-width, 380px);
188
+ block-size: var(--kai-dock-height, 600px);
189
+ max-inline-size: calc(100vw - 2 * var(--kai-dock-inset, 1.5rem));
190
+ max-block-size: calc(
191
+ 100dvh - 2 * var(--kai-dock-inset, 1.5rem)
192
+ - var(--kai-dock-launcher-size, 56px) - var(--kai-dock-gap, 0.85rem)
193
+ );
194
+ border: 1px solid var(--color-border);
195
+ border-radius: var(--kai-dock-radius, 16px);
196
+ background: var(--color-background);
197
+ color: var(--color-foreground);
198
+ box-shadow: 0 20px 45px -12px rgb(0 0 0 / 0.35);
199
+ /* VISIBILITY IS SWITCHED, NEVER ANIMATED, and the zero duration is the whole point.
200
+ visibility 180ms here meant the COMPUTED value stayed hidden for about two
201
+ frames after opening while the inline style already said visible — and a
202
+ focus() into a hidden subtree is dropped silently, so focusOnOpen never landed
203
+ in a real browser (it worked under reduced motion, which is what made it look like
204
+ a focus bug rather than a paint one).
205
+ This rule is the CLOSING direction: hold visibility for the length of the fade so
206
+ the panel does not vanish mid-transition, then switch. The opening direction sets
207
+ the delay to 0s below, so opening is synchronous and focus has somewhere to go. */
208
+ transition: opacity 180ms ease, transform 180ms ease, visibility 0s linear 180ms;
209
+ }
210
+ [data-kai-dock] [part="panel"][data-expanded] {
211
+ transition: opacity 180ms ease, transform 180ms ease, visibility 0s linear 0s;
212
+ }
213
+ /* The panel is a flex column and the slotted surface fills it -- the one layout
214
+ fact both reference implementations wrote by hand (flex:1; min-height:0). Both
215
+ spellings are here on purpose: ::slotted covers the shadow-DOM facade, > *
216
+ covers a plain Solid caller, and each is inert in the other's world (a <slot>
217
+ is display:contents, so flex properties do nothing to it). */
218
+ [data-kai-dock] [part="panel"] ::slotted(*) { flex: 1 1 auto; min-block-size: 0; }
219
+ [data-kai-dock] [part="panel"] > * { flex: 1 1 auto; min-block-size: 0; }
220
+ [data-kai-dock] [part="panel"]:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; }
221
+ [data-kai-dock][data-position^="bottom"] [part="panel"][data-closed] { transform: translateY(0.5rem); }
222
+ [data-kai-dock][data-position^="top"] [part="panel"][data-closed] { transform: translateY(-0.5rem); }
223
+
224
+ /* Narrow viewports take the panel full-bleed. A SHIPPED DEFAULT, not a prop: both
225
+ references wrote this rule independently, which makes it a HOW. The launcher stays
226
+ in its corner and ON TOP of the full-bleed panel — that is the z-index: 1 in the
227
+ launcher rule above, not anything in this block, and without it a phone user has no
228
+ pointer route to closing at all (Escape alone strands exactly the touch user). */
229
+ @media (max-width: 480px) {
230
+ [data-kai-dock] [part="panel"] {
231
+ position: fixed;
232
+ inset: 0;
233
+ inline-size: auto;
234
+ block-size: auto;
235
+ max-inline-size: none;
236
+ max-block-size: none;
237
+ border: 0;
238
+ border-radius: 0;
239
+ }
240
+ }
241
+
242
+ @media (prefers-reduced-motion: reduce) {
243
+ /* [data-expanded] is listed explicitly because the open-direction rule above is
244
+ MORE SPECIFIC than a bare [part="panel"], so without it an opening panel kept
245
+ animating opacity and transform for exactly the users who asked it not to. */
246
+ [data-kai-dock] [part="launcher"],
247
+ [data-kai-dock] [part="panel"],
248
+ [data-kai-dock] [part="panel"][data-expanded] { transition: none; }
249
+ [data-kai-dock] [part="panel"][data-closed] { transform: none; }
250
+ }
251
+ `;
252
+
253
+ /**
254
+ * The deepest active element, drilling through nested shadow roots.
255
+ *
256
+ * A local copy of the helper `ui/dialog.tsx` already carries. Deliberately not
257
+ * refactored into a shared module by this change: dialog is not this task's file, and
258
+ * a four-line tree walk duplicated once is cheaper than a cross-file move nobody
259
+ * asked for. If a third caller appears, hoist it then.
260
+ */
261
+ function deepActiveElement(): HTMLElement | null {
262
+ let el = document.activeElement as HTMLElement | null;
263
+ while (el?.shadowRoot?.activeElement) el = el.shadowRoot.activeElement as HTMLElement;
264
+ return el;
265
+ }
266
+
267
+ /**
268
+ * Is `node` inside `root` in the FLATTENED tree?
269
+ *
270
+ * `root.contains(node)` is the wrong question here and would answer `false` for every
271
+ * real case: the panel's content is light-DOM nodes ASSIGNED to a slot, so it lives in
272
+ * the host's tree, not the panel's. This walks up through `assignedSlot` at each slot
273
+ * and through `host` at each shadow boundary, which is the tree focus actually moves in.
274
+ */
275
+ function isWithin(root: Node, node: Node | null): boolean {
276
+ let cur: Node | null = node;
277
+ while (cur) {
278
+ if (cur === root) return true;
279
+ const slot = cur instanceof Element ? cur.assignedSlot : null;
280
+ if (slot) { cur = slot; continue; }
281
+ if (cur.parentNode) { cur = cur.parentNode; continue; }
282
+ cur = cur instanceof ShadowRoot ? cur.host : null;
283
+ }
284
+ return false;
285
+ }
286
+
287
+ /**
288
+ * Dock — a corner-docked launcher button with a panel that opens above it.
289
+ *
290
+ * The affordance behind a support widget: a circular button pinned to a corner of the
291
+ * viewport, and a floating panel holding whatever you slot into it. The panel is
292
+ * content-agnostic on purpose — a chat, a form, your own component — and the dock
293
+ * never reads, types or reaches into it.
294
+ *
295
+ * WHAT IT OWNS (all of it non-configurable, because these are facts about the medium
296
+ * rather than preferences):
297
+ *
298
+ * - **Hide semantics.** Closed means `visibility: hidden` + `opacity: 0` + `inert`,
299
+ * and the panel is NEVER unmounted. `display: none` would leave the panel with no
300
+ * layout box while closed, so anything inside that measures itself — a thread
301
+ * scroller, a `ResizeObserver` — measures zero and re-measures on every open. The
302
+ * cost of keeping the box is that a stream keeps rendering into a closed panel,
303
+ * which is precisely what makes an unread dot honest.
304
+ * - **Focus.** On open, focus moves per `focusOnOpen`; on close it returns to the
305
+ * launcher, always. `inert` is cleared and visibility restored BEFORE `focus()` runs
306
+ * — a `focus()` into an inert or hidden subtree is silently dropped, no error — via
307
+ * a `queueMicrotask`, the same shape `ui/dialog.tsx` uses. Mount moves no focus.
308
+ * - **Escape.** Closes only while the dock CONTAINS focus, and never stops the event.
309
+ * A background widget that ate every Escape on the page would break the host page's
310
+ * own menus; swallowing a key from a page you are a guest on is a decide-quietly move.
311
+ * - **No focus trap**, deliberately, unlike `kai-dialog`. The page staying usable is
312
+ * the whole point of "docked", which is also why the panel is `aria-modal="false"`.
313
+ *
314
+ * WHAT IT REFUSES: it never aborts a request, never persists its own open state, never
315
+ * decides what "unread" means, and never clears `unread` on open — writing over a
316
+ * consumer's prop is the trap `kai-chat.loading` fell into. Those are the app's calls;
317
+ * `open` + `onOpenChange` is the seam.
318
+ *
319
+ * Geometry is CSS custom properties rather than props: `--kai-dock-width` ·
320
+ * `--kai-dock-height` · `--kai-dock-inset` · `--kai-dock-gap` · `--kai-dock-radius` ·
321
+ * `--kai-dock-z` · `--kai-dock-launcher-size`.
322
+ *
323
+ * @see `elements/dock.tsx` for the `<kai-dock>` web-component facade.
324
+ */
325
+ export function Dock(props: DockProps) {
326
+ const panelId = `kai-dock-panel-${createUniqueId()}`;
327
+ const [internalOpen, setInternalOpen] = createSignal(props.defaultOpen ?? false);
328
+
329
+ const isControlled = () => props.open !== undefined;
330
+ const isOpen = () => (isControlled() ? !!props.open : internalOpen());
331
+ const setOpen = (v: boolean) => {
332
+ if (!isControlled()) setInternalOpen(v);
333
+ props.onOpenChange?.(v);
334
+ };
335
+
336
+ props.controllerRef?.({ open: isOpen, setOpen });
337
+
338
+ let panel: HTMLElement | undefined;
339
+ let launcher: HTMLButtonElement | undefined;
340
+
341
+ const label = () => props.label ?? 'Chat';
342
+ const launcherName = () => (isOpen()
343
+ ? props.closeLabel ?? `Close ${label()}`
344
+ : props.openLabel ?? `Open ${label()}`);
345
+
346
+ /** The focus half of an open, deferred so the state flip above has already landed. */
347
+ const focusOnOpen = () => {
348
+ const mode = props.focusOnOpen ?? 'content';
349
+ if (mode === 'none') return;
350
+ if (mode === 'content') {
351
+ const target = props.contentTarget?.();
352
+ target?.focus();
353
+ // Content-agnostic fallback: a plain <div> is not focusable, so `focus()` was a
354
+ // silent no-op and the panel takes it instead. Asking where focus ACTUALLY
355
+ // landed is the only way to tell, since the dock is not allowed to inspect what
356
+ // it is holding.
357
+ if (target && isWithin(panel!, deepActiveElement())) return;
358
+ }
359
+ panel?.focus();
360
+ };
361
+
362
+ // The focus choreography, and the ONE ordering that is a hard invariant.
363
+ //
364
+ // SEEDED WITH THE INITIAL OPEN VALUE, not `false`. `kai-dialog` seeds `false` on
365
+ // purpose so a `defaultOpen` modal DOES take focus at mount; a docked widget is the
366
+ // opposite — it is a guest on someone's page and must not steal focus on load, from
367
+ // `default-open` OR from `open`. Same mechanism, opposite default, and the
368
+ // difference is exactly the modal/non-modal split.
369
+ createEffect((wasOpen: boolean) => {
370
+ const open = isOpen();
371
+ if (open !== wasOpen) {
372
+ // The attributes and inline styles below are written by render effects, which
373
+ // Solid runs before this one; the microtask is belt-and-braces for a state
374
+ // change driven from outside a Solid batch, and gives a freshly-slotted element
375
+ // a turn to upgrade before we call focus() on it.
376
+ if (open) queueMicrotask(focusOnOpen);
377
+ else queueMicrotask(() => launcher?.focus());
378
+ }
379
+ return open;
380
+ }, untrack(() => props.open ?? props.defaultOpen ?? false));
381
+
382
+ const onKeyDown = (e: KeyboardEvent) => {
383
+ if (e.key !== 'Escape' || !isOpen()) return;
384
+ // D-C. This listener is ON the dock rather than on `document`, so the only
385
+ // keydowns it can ever see are ones whose target is inside it — which IS the
386
+ // condition "the dock contains focus", with no ambiguity and no second check. It
387
+ // also composes: an overlay inside the panel that handles its own Escape and
388
+ // stops the event correctly prevents the dock from closing underneath it.
389
+ // No stopPropagation, no preventDefault: the host page keeps its key.
390
+ setOpen(false);
391
+ };
392
+
393
+ return (
394
+ <>
395
+ <style>{DOCK_CSS}</style>
396
+ <div data-kai-dock data-position={props.position ?? 'bottom-end'} onKeyDown={onKeyDown}>
397
+ <button
398
+ ref={(el) => { launcher = el; props.launcherRef?.(el); }}
399
+ part="launcher"
400
+ type="button"
401
+ aria-expanded={isOpen() ? 'true' : 'false'}
402
+ aria-controls={panelId}
403
+ // The NAME is what changes for assistive tech; the glyph morph is decorative.
404
+ aria-label={launcherName()}
405
+ disabled={props.disabled || undefined}
406
+ onClick={() => setOpen(!isOpen())}
407
+ >
408
+ <Show when={props.unread && !isOpen()}>
409
+ <span part="badge" aria-hidden="true" />
410
+ </Show>
411
+ <span aria-hidden="true" style={{ display: 'inline-flex' }}>
412
+ <Show when={isOpen()} fallback={props.launcher ?? <DockLauncherGlyph />}>
413
+ {props.launcherOpen ?? props.launcher ?? <DockCloseGlyph />}
414
+ </Show>
415
+ </span>
416
+ </button>
417
+ <div
418
+ ref={(el) => { panel = el; props.panelRef?.(el); }}
419
+ part="panel"
420
+ id={panelId}
421
+ role="dialog"
422
+ // Non-modal, and saying so is load-bearing given there is no focus trap:
423
+ // claiming modality without trapping is worse than claiming neither.
424
+ aria-modal="false"
425
+ aria-label={label()}
426
+ tabindex={-1}
427
+ data-expanded={isOpen() ? '' : undefined}
428
+ data-closed={isOpen() ? undefined : ''}
429
+ // `inert` is the half that actually stops Tab: a visibility:hidden panel is
430
+ // already skipped by most AT, but a focusable control inside it is not.
431
+ bool:inert={!isOpen()}
432
+ // INLINE, unlike the geometry: the closed state must beat any consumer rule,
433
+ // and it is the half a test can read without a layout engine.
434
+ style={{
435
+ visibility: isOpen() ? 'visible' : 'hidden',
436
+ opacity: isOpen() ? '1' : '0',
437
+ 'pointer-events': isOpen() ? 'auto' : 'none',
438
+ }}
439
+ >
440
+ {props.children}
441
+ </div>
442
+ </div>
443
+ </>
444
+ );
445
+ }