@kitn.ai/ui 0.26.0 → 0.28.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 (488) hide show
  1. package/README.md +25 -6
  2. package/bin/mcp.js +16 -3
  3. package/bin/route.js +23 -0
  4. package/bin/route.test.js +58 -0
  5. package/dist/{elements/chunks/Icon--Y8vsFal.js → Icon-DObhsRtm.js} +1 -1
  6. package/dist/action-icons-CcgNN2oT.js +1 -0
  7. package/dist/arrow-down-CjRU3JgV.js +1 -0
  8. package/dist/arrow-left-Bnpb1UEW.js +1 -0
  9. package/dist/artifact-Bwt7OgcR.js +1 -0
  10. package/dist/assets/dev-5XzXYR21.js +91 -0
  11. package/dist/attachments-Qr4kg-2b.js +1 -0
  12. package/dist/{elements/chunks/audio-visualizer-D7lB6mvR.js → audio-visualizer-DToYdfSc.js} +1 -1
  13. package/dist/badge-CdEu_jn2.js +1 -0
  14. package/dist/bash-InADTalH.js +1 -6
  15. package/dist/bell-DQAAOpDz.js +1 -0
  16. package/dist/{elements/chunks/button-flUbeufF.js → button-DkuOnGLy.js} +1 -1
  17. package/dist/card-renderer-Jx70m6_l.js +1 -0
  18. package/dist/card-routing-0YidER_3.js +1 -0
  19. package/dist/check-njkVdZu2.js +1 -0
  20. package/dist/checkbox-D7iRB3nL.js +1 -0
  21. package/dist/checkbox-group-B2_rpoPM.js +1 -0
  22. package/dist/chevron-down-B72s6PXR.js +1 -0
  23. package/dist/chevron-right-BatuHzUm.js +1 -0
  24. package/dist/choice-card-m0BYqqOK.js +1 -0
  25. package/dist/circle-check-B62pODB3.js +1 -0
  26. package/dist/circle-x-BPG0iPst.js +1 -0
  27. package/dist/circle-zx0ByzY9.js +1 -0
  28. package/dist/{elements/chunks/cn-CU7UAWzk.js → cn-DN5AWfiS.js} +1 -1
  29. package/dist/code-block-ClMLKb7C.js +1 -0
  30. package/dist/{elements/chunks/collapsible-BzbXpPzr.js → collapsible-B4DlWicL.js} +1 -1
  31. package/dist/components/attachment-types.d.ts +7 -0
  32. package/dist/components/audio-visualizer/fit-scale.d.ts +1 -1
  33. package/dist/components/card-renderer.d.ts +13 -0
  34. package/dist/components/card.d.ts +4 -0
  35. package/dist/components/chat-thread.d.ts +135 -2
  36. package/dist/components/conversation-item.d.ts +35 -7
  37. package/dist/components/conversation-list.d.ts +17 -5
  38. package/dist/components/conversation-panel.d.ts +29 -0
  39. package/dist/components/form-widgets.d.ts +67 -2
  40. package/dist/components/form.d.ts +48 -1
  41. package/dist/components/home-panel.d.ts +34 -0
  42. package/dist/components/message.d.ts +25 -3
  43. package/dist/components/reasoning.d.ts +6 -0
  44. package/dist/components/scroll-button.d.ts +11 -0
  45. package/dist/components/thread.d.ts +5 -0
  46. package/dist/components/tool-types.d.ts +9 -0
  47. package/dist/components/widget-tab-bar.d.ts +16 -0
  48. package/dist/composer-sQ0HcvyG.js +106 -0
  49. package/dist/confirm-card-rQWDQsnC.js +1 -0
  50. package/dist/construct-cli.es.js +1337 -0
  51. package/dist/{elements/chunks/context-VLIL3sJ-.js → context-0kg4ZX1J.js} +1 -1
  52. package/dist/controllable-CnGpMF6r.js +1 -0
  53. package/dist/conversation-item-BxwQLaFJ.js +1 -0
  54. package/dist/conversation-list-BCfGN1JU.js +1 -0
  55. package/dist/copy-n9u9rqpi.js +1 -0
  56. package/dist/core-AYMC6_lb.js +12 -5874
  57. package/dist/{create-tween-BFT6c-Y8.js → create-tween-C-q-JCM-.js} +1 -1
  58. package/dist/{create-tween-CJNlzoaO.js → create-tween-CRrJXxBd.js} +1 -1
  59. package/dist/{create-tween-CQED4T0g.js → create-tween-DZ3RRDHD.js} +1 -1
  60. package/dist/{elements/chunks/create-tween-c4Z8TDYz.js → create-tween-uPkArjIG.js} +2 -2
  61. package/dist/css-M7EaDHN_.js +1 -6
  62. package/dist/custom-elements.json +1433 -202
  63. package/dist/default-input-CaMrJkgH.js +1 -0
  64. package/dist/define-By-uL588.js +1 -0
  65. package/dist/define.d.ts +4 -0
  66. package/dist/define.js +510 -0
  67. package/dist/define.server.d.ts +5 -0
  68. package/dist/define.server.js +506 -0
  69. package/dist/{elements/chunks/disclosure-CcNbyLy0.js → disclosure-WUImBbDv.js} +1 -1
  70. package/dist/{elements/chunks/dropdown-BgWTthUx.js → dropdown-wZVeJOMd.js} +1 -1
  71. package/dist/elements/agent-card.js +1 -1
  72. package/dist/elements/artifact.js +1 -1
  73. package/dist/elements/attachments.js +1 -1
  74. package/dist/elements/audio-visualizer.js +1 -1
  75. package/dist/elements/autoloader.js +1 -1
  76. package/dist/elements/avatar.js +1 -1
  77. package/dist/elements/badge.js +1 -1
  78. package/dist/elements/button.js +1 -1
  79. package/dist/elements/card.js +1 -1
  80. package/dist/elements/cards.js +1 -1
  81. package/dist/elements/chain-of-thought.js +1 -1
  82. package/dist/elements/chat-scope-picker.js +1 -1
  83. package/dist/elements/chat-workspace.js +1 -1
  84. package/dist/elements/chat.js +1 -1
  85. package/dist/elements/checkbox-group.d.ts +1 -0
  86. package/dist/elements/checkbox-group.js +1 -0
  87. package/dist/elements/checkbox.d.ts +1 -0
  88. package/dist/elements/checkbox.js +1 -0
  89. package/dist/elements/checkpoint.js +1 -1
  90. package/dist/elements/choice.js +1 -1
  91. package/dist/elements/coachmark.js +1 -1
  92. package/dist/elements/code-block.js +1 -1
  93. package/dist/elements/command.js +1 -1
  94. package/dist/elements/compare.js +1 -1
  95. package/dist/elements/composer.js +1 -1
  96. package/dist/elements/confirm-card.js +1 -1
  97. package/dist/elements/context-meter.js +1 -1
  98. package/dist/elements/conversation-item.js +1 -1
  99. package/dist/elements/conversation-list.js +1 -1
  100. package/dist/elements/define-entry.d.ts +12 -0
  101. package/dist/elements/dialog.js +1 -1
  102. package/dist/elements/dock.js +102 -8
  103. package/dist/elements/dropdown.js +1 -1
  104. package/dist/elements/editable-label.js +1 -1
  105. package/dist/elements/element-data-types.d.ts +37 -0
  106. package/dist/elements/embed.js +1 -1
  107. package/dist/elements/empty.js +1 -1
  108. package/dist/elements/feedback-bar.js +1 -1
  109. package/dist/elements/file-tree.js +1 -1
  110. package/dist/elements/file-upload.js +1 -1
  111. package/dist/elements/form.js +1 -1
  112. package/dist/elements/hover-card.js +1 -1
  113. package/dist/elements/icon.js +1 -1
  114. package/dist/elements/image.js +1 -1
  115. package/dist/elements/input.js +1 -1
  116. package/dist/elements/kbd.js +1 -1
  117. package/dist/elements/link-preview.js +1 -1
  118. package/dist/elements/loader.js +1 -1
  119. package/dist/elements/markdown.js +1 -1
  120. package/dist/elements/menu.js +1 -1
  121. package/dist/elements/message-skills.js +1 -1
  122. package/dist/elements/message.js +1 -1
  123. package/dist/elements/model-switcher.js +1 -1
  124. package/dist/elements/nav.js +1 -1
  125. package/dist/elements/notice.js +1 -1
  126. package/dist/elements/pane-grid.d.ts +1 -0
  127. package/dist/elements/pane-grid.js +1 -0
  128. package/dist/elements/pane-group.js +1 -1
  129. package/dist/elements/pane.js +1 -1
  130. package/dist/elements/popover.js +1 -1
  131. package/dist/elements/progress-bar.js +1 -1
  132. package/dist/elements/prompt-dock.js +1 -1
  133. package/dist/elements/prompt-input.js +1 -1
  134. package/dist/elements/prompt-suggestions.js +1 -1
  135. package/dist/elements/radio-group.d.ts +1 -0
  136. package/dist/elements/radio-group.js +1 -0
  137. package/dist/elements/reasoning.js +1 -1
  138. package/dist/elements/remote.js +1 -1
  139. package/dist/elements/resizable.js +1 -1
  140. package/dist/elements/response-stream.js +2 -2
  141. package/dist/elements/screen.js +1 -1
  142. package/dist/elements/scroll-area.js +1 -1
  143. package/dist/elements/scroll-button.js +1 -1
  144. package/dist/elements/search.js +1 -1
  145. package/dist/elements/segmented.js +1 -1
  146. package/dist/elements/select.d.ts +1 -0
  147. package/dist/elements/select.js +1 -0
  148. package/dist/elements/separator.js +1 -1
  149. package/dist/elements/setting-item.js +1 -1
  150. package/dist/elements/settings-group.js +1 -1
  151. package/dist/elements/skeleton.js +1 -1
  152. package/dist/elements/slider.d.ts +1 -0
  153. package/dist/elements/slider.js +1 -0
  154. package/dist/elements/slots.d.ts +1 -1
  155. package/dist/elements/source.js +1 -1
  156. package/dist/elements/status.js +1 -1
  157. package/dist/elements/switch.js +1 -1
  158. package/dist/elements/tabs.js +1 -1
  159. package/dist/elements/tasks.js +1 -1
  160. package/dist/elements/text-shimmer.js +1 -1
  161. package/dist/elements/thinking-bar.js +1 -1
  162. package/dist/elements/thread.js +1 -1
  163. package/dist/elements/toast.js +1 -1
  164. package/dist/elements/tool.js +1 -1
  165. package/dist/elements/tooltip.js +1 -1
  166. package/dist/elements/voice-input.js +1 -1
  167. package/dist/elements/voice-output.js +1 -1
  168. package/dist/elements.d.ts +380 -26
  169. package/dist/{elements/chunks/ellipsis-7gU33mL2.js → ellipsis-CbOpJnU3.js} +1 -1
  170. package/dist/embed-CrfjkbUG.js +1 -0
  171. package/dist/engine-javascript-vq0WuIJl.js +14 -2516
  172. package/dist/{elements/chunks/external-link-CSOEsNlx.js → external-link-ByXAwBBQ.js} +1 -1
  173. package/dist/file-text-DMib2qwt.js +1 -0
  174. package/dist/{elements/chunks/file-tree-zvB2i7P9.js → file-tree-CJqM2aP7.js} +1 -1
  175. package/dist/folder-_BAb929C.js +1 -0
  176. package/dist/form-DSZgZQZk.js +1 -0
  177. package/dist/github-dark-dimmed-DUshB20C.js +1 -4
  178. package/dist/github-light-JYsPkUQd.js +1 -4
  179. package/dist/hover-card-BQbul9eq.js +1 -0
  180. package/dist/html-CPZ3oZQ7.js +1 -10
  181. package/dist/icon-CdcfRhYI.js +1 -0
  182. package/dist/{elements/chunks/index-BCRhi84a.js → index-xu_pdcPi.js} +1 -1
  183. package/dist/index.d.ts +4 -2
  184. package/dist/index.js +7474 -5857
  185. package/dist/index.server.js +5965 -4762
  186. package/dist/info-DsFjGuI_.js +1 -0
  187. package/dist/input-DKbmkMos.js +1 -0
  188. package/dist/javascript-C25yR2R2.js +1 -6
  189. package/dist/json-DxJze_jm.js +1 -6
  190. package/dist/kai.es.js +1 -1
  191. package/dist/kbd-PvdGhX4s.js +1 -0
  192. package/dist/{elements/chunks/link-preview-OQ9IEq0f.js → link-preview-DbM1866N.js} +1 -1
  193. package/dist/link-zF8PzWfF.js +1 -0
  194. package/dist/loader-BM4CEz4G.js +1 -0
  195. package/dist/{elements/chunks/markdown-DgBFaGAz.js → markdown-LMvmAyBk.js} +7 -7
  196. package/dist/mcp.es.js +1361 -81
  197. package/dist/message-BSsgvHvf.js +1 -0
  198. package/dist/message-BYZECyek.js +1 -0
  199. package/dist/message-circle-DruwObfX.js +1 -0
  200. package/dist/message-square-BG6q6mlm.js +1 -0
  201. package/dist/{elements/chunks/minimize-2-DPIUKTrT.js → minimize-2-BQRoBV1V.js} +1 -1
  202. package/dist/{elements/chunks/model-switcher-DsT8tFB0.js → model-switcher-CMpCOYcB.js} +1 -1
  203. package/dist/{elements/chunks/overlay-DuiwdXjm.js → overlay-Cb-MkyDa.js} +1 -1
  204. package/dist/panel-right-i8warFxT.js +1 -0
  205. package/dist/paperclip-ByY3UVO6.js +1 -0
  206. package/dist/play-BvKKq3fX.js +1 -0
  207. package/dist/primitives/card-data-types.d.ts +49 -0
  208. package/dist/primitives/card-routing.d.ts +1 -33
  209. package/dist/primitives/card-validate.d.ts +2 -2
  210. package/dist/primitives/chat-config.d.ts +29 -2
  211. package/dist/primitives/conversation-store.d.ts +54 -0
  212. package/dist/primitives/field-mask.d.ts +96 -0
  213. package/dist/primitives/field-semantics.d.ts +36 -0
  214. package/dist/primitives/input-mask.d.ts +46 -0
  215. package/dist/primitives/url-scheme-policy.d.ts +34 -0
  216. package/dist/primitives/use-resize-observer.d.ts +2 -2
  217. package/dist/progress-bar-jVAHvMUf.js +1 -0
  218. package/dist/prompt-suggestion-CD-qtNgC.js +1 -0
  219. package/dist/radio-BsoBMhkF.js +1 -0
  220. package/dist/react/index.d.ts +586 -19
  221. package/dist/react.js +223 -187
  222. package/dist/reasoning-4kBPtlTx.js +1 -0
  223. package/dist/register-impl-B1hLkrI8.js +424 -0
  224. package/dist/{elements/chunks/resizable-D181b4fM.js → resizable-uPmY9VY-.js} +1 -1
  225. package/dist/rotate-ccw-B2xn93py.js +1 -0
  226. package/dist/rotate-cw-CMVLoCyH.js +1 -0
  227. package/dist/schemas/form.schema.json +21 -1
  228. package/dist/schemas/tool-defs.d.ts +11 -1
  229. package/dist/schemas.js +81 -81
  230. package/dist/scroll-area-BqLUo8ZZ.js +1 -0
  231. package/dist/scroll-button-DslXiJyL.js +1 -0
  232. package/dist/select-2cLRtPGT.js +1 -0
  233. package/dist/{elements/chunks/separator-CeJHQqLW.js → separator-BWQtNJcM.js} +1 -1
  234. package/dist/{elements/chunks/settings-CqaHgJF9.js → settings-BBR63jLg.js} +1 -1
  235. package/dist/{elements/chunks/settings-group-CjqjLCOn.js → settings-group-B2ciy-Iw.js} +1 -1
  236. package/dist/{elements/chunks/skeleton-D8akBnKS.js → skeleton-CkHI-HgC.js} +1 -1
  237. package/dist/slider-DDR9U-i0.js +1 -0
  238. package/dist/{solid-BtdqNZI7.js → solid-BzCFdL25.js} +9771 -7594
  239. package/dist/{solid-CnavwBye.js → solid-DruVOXnf.js} +7893 -6183
  240. package/dist/solid.d.ts +16 -2
  241. package/dist/solid.js +256 -243
  242. package/dist/solid.server.js +256 -243
  243. package/dist/source-GEnFF528.js +1 -0
  244. package/dist/star-CilHq8GP.js +1 -0
  245. package/dist/state/index.d.ts +1 -1
  246. package/dist/state/mock.d.ts +32 -2
  247. package/dist/state.js +133 -108
  248. package/dist/store-Ciyk6eZG.js +1 -0
  249. package/dist/svelte--5p79yCD.js +1 -15
  250. package/dist/switch-KkeQIbuH.js +1 -0
  251. package/dist/tasks-card-BrN4bggJ.js +1 -0
  252. package/dist/text-shimmer-Dcj1MHuq.js +1 -0
  253. package/dist/textarea-Dpvbngbg.js +1 -0
  254. package/dist/theme.tokens.css +110 -13
  255. package/dist/{elements/chunks/thumbs-up-DX2Ijk3H.js → thumbs-up-CsT9Wzb9.js} +1 -1
  256. package/dist/toast-store-tgK6MuMv.js +1 -0
  257. package/dist/{elements/chunks/tool-DKQ2D1DP.js → tool-DzUjIBT8.js} +1 -1
  258. package/dist/{elements/chunks/tooltip-B0G99y8w.js → tooltip-Bkkg_4t0.js} +1 -1
  259. package/dist/trash-2-jBKxjGGQ.js +1 -0
  260. package/dist/{elements/chunks/triangle-alert-F9cyUEyy.js → triangle-alert-DlQ68ppf.js} +1 -1
  261. package/dist/tsx-B8rCNbgL.js +1 -6
  262. package/dist/types.d.ts +48 -3
  263. package/dist/typescript-RycA9KXf.js +1 -6
  264. package/dist/ui/checkbox-group.d.ts +83 -0
  265. package/dist/ui/checkbox.d.ts +40 -0
  266. package/dist/ui/dock.d.ts +51 -0
  267. package/dist/ui/icon.d.ts +3 -0
  268. package/dist/ui/input.d.ts +48 -2
  269. package/dist/ui/radio.d.ts +85 -0
  270. package/dist/ui/select.d.ts +71 -0
  271. package/dist/ui/slider.d.ts +65 -0
  272. package/dist/ui/switch.d.ts +9 -2
  273. package/dist/upload-TcAHXv9W.js +1 -0
  274. package/dist/url-scheme-policy-DR5DgvkM.js +1 -0
  275. package/dist/use-card-resolution-DaZqAdWf.js +1 -0
  276. package/dist/{elements/chunks/variant-aurora-qt6Zct8M.js → variant-aurora-BhLuYxJR.js} +1 -1
  277. package/dist/{variant-aurora-B8II3-I1.js → variant-aurora-CNmfHhcx.js} +2 -2
  278. package/dist/{variant-aurora-CMNWOSJR.js → variant-aurora-D6GsdTPA.js} +2 -2
  279. package/dist/{variant-aurora-CcuDMWwQ.js → variant-aurora-DbW_OWL2.js} +1 -1
  280. package/dist/{elements/chunks/variant-custom-tJp22_BQ.js → variant-custom-BRvaNHSi.js} +1 -1
  281. package/dist/{variant-custom-L16JLLZy.js → variant-custom-BvYHR62b.js} +1 -1
  282. package/dist/{variant-custom-BdvYC7af.js → variant-custom-DMTrEO5w.js} +2 -2
  283. package/dist/{variant-custom-C714mqKl.js → variant-custom-DV2x254s.js} +2 -2
  284. package/dist/{variant-wave-ecwykeyL.js → variant-wave-BNMGrDfj.js} +2 -2
  285. package/dist/{variant-wave-2S4La2Ka.js → variant-wave-BpDd8t3Z.js} +1 -1
  286. package/dist/{variant-wave-D7ruiA1W.js → variant-wave-CyBueXj9.js} +2 -2
  287. package/dist/{elements/chunks/variant-wave-CaQ5ti39.js → variant-wave-sp-qwsdI.js} +1 -1
  288. package/dist/vue-BmIZj4XD.js +1 -33
  289. package/dist/x-Z5UntkIJ.js +1 -0
  290. package/frameworks/react/index.tsx +210 -22
  291. package/llms-full.txt +1184 -22
  292. package/llms.txt +6 -3
  293. package/package.json +17 -4
  294. package/src/agent-tooling/README.md +192 -0
  295. package/src/agent-tooling/catalog/invariants.ts +3 -3
  296. package/src/agent-tooling/catalog/surfaces.ts +72 -0
  297. package/src/agent-tooling/construct/cli-entry.ts +8 -0
  298. package/src/agent-tooling/construct/cli.ts +147 -0
  299. package/src/agent-tooling/construct/codegen.ts +1635 -0
  300. package/src/agent-tooling/construct/construct.v1.schema.json +319 -0
  301. package/src/agent-tooling/construct/dev.ts +148 -0
  302. package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +7 -0
  303. package/src/agent-tooling/construct/fixtures/ops-console.construct.json +44 -0
  304. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +32 -0
  305. package/src/agent-tooling/construct/schema.ts +452 -0
  306. package/src/agent-tooling/mcp/server.ts +2 -1
  307. package/src/agent-tooling/mcp/tools/construct.ts +130 -0
  308. package/src/agent-tooling/mcp/tools/debug.ts +126 -4
  309. package/src/agent-tooling/mcp/tools/reference.ts +104 -3
  310. package/src/agent-tooling/recipes/composed-thread.ts +714 -0
  311. package/src/agent-tooling/recipes/index.ts +22 -0
  312. package/src/agent-tooling/recipes/types.ts +32 -0
  313. package/src/agent-tooling/registry.ts +5 -0
  314. package/src/components/attachment-types.ts +7 -0
  315. package/src/components/audio-visualizer/fit-scale.ts +1 -1
  316. package/src/components/card-renderer.tsx +39 -8
  317. package/src/components/card.tsx +10 -2
  318. package/src/components/chat-scope-picker.tsx +2 -2
  319. package/src/components/chat-thread.tsx +602 -43
  320. package/src/components/choice-card.tsx +102 -37
  321. package/src/components/coachmark.tsx +1 -1
  322. package/src/components/code-block.tsx +2 -2
  323. package/src/components/composer.tsx +89 -15
  324. package/src/components/confirm-card.tsx +20 -4
  325. package/src/components/conversation-item.tsx +56 -9
  326. package/src/components/conversation-list.tsx +25 -10
  327. package/src/components/conversation-panel.tsx +118 -0
  328. package/src/components/empty.tsx +14 -3
  329. package/src/components/file-tree.tsx +5 -1
  330. package/src/components/form-widgets.tsx +257 -195
  331. package/src/components/form.tsx +297 -9
  332. package/src/components/home-panel.tsx +159 -0
  333. package/src/components/loader.tsx +12 -5
  334. package/src/components/message-skills.tsx +1 -1
  335. package/src/components/message.tsx +71 -11
  336. package/src/components/prompt-input.tsx +7 -1
  337. package/src/components/prompt-suggestion.tsx +4 -1
  338. package/src/components/reasoning.tsx +30 -3
  339. package/src/components/response-compare.tsx +2 -2
  340. package/src/components/scroll-button.tsx +56 -4
  341. package/src/components/source.tsx +6 -1
  342. package/src/components/tasks-card.tsx +58 -16
  343. package/src/components/thread.tsx +10 -1
  344. package/src/components/toast.tsx +1 -1
  345. package/src/components/tool-types.ts +9 -0
  346. package/src/components/widget-tab-bar.tsx +73 -0
  347. package/src/elements/attachments.tsx +3 -1
  348. package/src/elements/chat-workspace.tsx +1 -1
  349. package/src/elements/chat.tsx +115 -5
  350. package/src/elements/checkbox-group.tsx +164 -0
  351. package/src/elements/checkbox.tsx +134 -0
  352. package/src/elements/compiled.css +1 -1
  353. package/src/elements/composer.tsx +12 -1
  354. package/src/elements/conversation-item.tsx +62 -22
  355. package/src/elements/conversation-list.tsx +2 -2
  356. package/src/elements/define-entry.ts +12 -0
  357. package/src/elements/dock.tsx +11 -0
  358. package/src/elements/element-data-types.ts +40 -0
  359. package/src/elements/element-manifest.json +24 -0
  360. package/src/elements/element-meta.json +767 -34
  361. package/src/elements/element-nonscalar.json +17 -0
  362. package/src/elements/element-types.d.ts +380 -26
  363. package/src/elements/feedback-bar.tsx +5 -0
  364. package/src/elements/icon-names.json +29 -0
  365. package/src/elements/input.tsx +312 -23
  366. package/src/elements/message.tsx +2 -0
  367. package/src/elements/pane-grid.tsx +111 -0
  368. package/src/elements/prompt-input.tsx +8 -2
  369. package/src/elements/radio-group.tsx +129 -0
  370. package/src/elements/register-impl.ts +6 -0
  371. package/src/elements/scroll-button.tsx +54 -6
  372. package/src/elements/select.tsx +165 -0
  373. package/src/elements/slider.tsx +171 -0
  374. package/src/elements/slots.ts +1 -1
  375. package/src/elements/styles.css +89 -3
  376. package/src/elements/thread.tsx +2 -0
  377. package/src/elements/toast.tsx +14 -1
  378. package/src/index.ts +5 -2
  379. package/src/primitives/card-data-types.ts +49 -0
  380. package/src/primitives/card-host.tsx +4 -2
  381. package/src/primitives/card-routing.ts +7 -56
  382. package/src/primitives/card-schemas/form.schema.json +21 -1
  383. package/src/primitives/card-validate.ts +10 -2
  384. package/src/primitives/chat-config.tsx +33 -6
  385. package/src/primitives/conversation-store.ts +248 -0
  386. package/src/primitives/field-mask.ts +256 -0
  387. package/src/primitives/field-semantics.ts +115 -0
  388. package/src/primitives/input-mask.ts +853 -0
  389. package/src/primitives/toast-store.ts +66 -4
  390. package/src/primitives/url-scheme-policy.ts +70 -0
  391. package/src/primitives/use-resize-observer.ts +2 -2
  392. package/src/schemas/tool-defs.ts +12 -2
  393. package/src/solid.ts +20 -2
  394. package/src/state/index.ts +1 -1
  395. package/src/state/mock.ts +92 -6
  396. package/src/types.ts +45 -3
  397. package/src/ui/agent-card.tsx +2 -2
  398. package/src/ui/avatar.tsx +1 -1
  399. package/src/ui/checkbox-group.tsx +153 -0
  400. package/src/ui/checkbox.tsx +63 -0
  401. package/src/ui/dock.tsx +192 -7
  402. package/src/ui/icon.tsx +53 -0
  403. package/src/ui/input.tsx +249 -9
  404. package/src/ui/kbd.tsx +1 -1
  405. package/src/ui/nav.tsx +1 -1
  406. package/src/ui/pane-group.tsx +2 -2
  407. package/src/ui/radio.tsx +150 -0
  408. package/src/ui/select.tsx +168 -0
  409. package/src/ui/slider.tsx +178 -0
  410. package/src/ui/switch.tsx +49 -18
  411. package/src/utils/cn.ts +11 -5
  412. package/theme.css +112 -13
  413. package/dist/elements/chunks/action-icons-D9u3OzYW.js +0 -1
  414. package/dist/elements/chunks/arrow-left-CVh1TeN7.js +0 -1
  415. package/dist/elements/chunks/artifact-BkYcfIlZ.js +0 -1
  416. package/dist/elements/chunks/attachments-DwfNlubq.js +0 -1
  417. package/dist/elements/chunks/badge-Q0-Cmwzz.js +0 -1
  418. package/dist/elements/chunks/bash-InADTalH.js +0 -1
  419. package/dist/elements/chunks/card-renderer-3Dyl-FpN.js +0 -1
  420. package/dist/elements/chunks/card-routing-BK09BdmL.js +0 -1
  421. package/dist/elements/chunks/check-B5kBPKeo.js +0 -1
  422. package/dist/elements/chunks/chevron-down-DZ1GS39h.js +0 -1
  423. package/dist/elements/chunks/chevron-right-CvP7OUd2.js +0 -1
  424. package/dist/elements/chunks/choice-card-D4QUIOWZ.js +0 -1
  425. package/dist/elements/chunks/circle-CDf9g6MQ.js +0 -1
  426. package/dist/elements/chunks/circle-check-CA47Nqbz.js +0 -1
  427. package/dist/elements/chunks/circle-x-CJ_HVbC8.js +0 -1
  428. package/dist/elements/chunks/code-block-BdLiTitd.js +0 -1
  429. package/dist/elements/chunks/composer-Byve-n8K.js +0 -69
  430. package/dist/elements/chunks/confirm-card-B0flE3MN.js +0 -1
  431. package/dist/elements/chunks/controllable-DTwTIlwp.js +0 -1
  432. package/dist/elements/chunks/conversation-item-BDztezOG.js +0 -1
  433. package/dist/elements/chunks/copy-Nf-kqxXH.js +0 -1
  434. package/dist/elements/chunks/core-AYMC6_lb.js +0 -12
  435. package/dist/elements/chunks/css-M7EaDHN_.js +0 -1
  436. package/dist/elements/chunks/default-input-C2iLFq0b.js +0 -1
  437. package/dist/elements/chunks/define-wjHR1MkU.js +0 -1
  438. package/dist/elements/chunks/download-C4V7tBdb.js +0 -1
  439. package/dist/elements/chunks/embed-D3KewsIJ.js +0 -1
  440. package/dist/elements/chunks/engine-javascript-vq0WuIJl.js +0 -141
  441. package/dist/elements/chunks/file-text-D1G8khle.js +0 -1
  442. package/dist/elements/chunks/folder-WW2LGLid.js +0 -1
  443. package/dist/elements/chunks/form-CQirdC4W.js +0 -1
  444. package/dist/elements/chunks/github-dark-dimmed-DUshB20C.js +0 -1
  445. package/dist/elements/chunks/github-light-JYsPkUQd.js +0 -1
  446. package/dist/elements/chunks/hover-card-CJJkfHTK.js +0 -1
  447. package/dist/elements/chunks/html-CPZ3oZQ7.js +0 -1
  448. package/dist/elements/chunks/icon-Cfgmf4fa.js +0 -1
  449. package/dist/elements/chunks/info-DX3YMOt6.js +0 -1
  450. package/dist/elements/chunks/input-BtW9U6Sq.js +0 -1
  451. package/dist/elements/chunks/javascript-C25yR2R2.js +0 -1
  452. package/dist/elements/chunks/json-DxJze_jm.js +0 -1
  453. package/dist/elements/chunks/kbd-LGXwuT_V.js +0 -1
  454. package/dist/elements/chunks/link-2My69BqT.js +0 -1
  455. package/dist/elements/chunks/loader-iOxc_Tu-.js +0 -1
  456. package/dist/elements/chunks/message-VMZohrXx.js +0 -1
  457. package/dist/elements/chunks/message-circle-CpXMM-_n.js +0 -1
  458. package/dist/elements/chunks/message-square-DxcWtETQ.js +0 -1
  459. package/dist/elements/chunks/message-tZmV2A3p.js +0 -1
  460. package/dist/elements/chunks/paperclip-CRZVp4z_.js +0 -1
  461. package/dist/elements/chunks/progress-bar-DNy66e_E.js +0 -1
  462. package/dist/elements/chunks/prompt-suggestion-Bn64Nl7L.js +0 -1
  463. package/dist/elements/chunks/reasoning-Emn9N4-T.js +0 -1
  464. package/dist/elements/chunks/rotate-cw-CPWe37D9.js +0 -1
  465. package/dist/elements/chunks/scroll-area-ClZkZxIk.js +0 -1
  466. package/dist/elements/chunks/scroll-button-DPE-t3x_.js +0 -1
  467. package/dist/elements/chunks/share-CyfnNEqp.js +0 -1
  468. package/dist/elements/chunks/source-UWCvmVak.js +0 -1
  469. package/dist/elements/chunks/star-DoHFAikQ.js +0 -1
  470. package/dist/elements/chunks/store-Cjq0qwYN.js +0 -1
  471. package/dist/elements/chunks/svelte--5p79yCD.js +0 -1
  472. package/dist/elements/chunks/tasks-card-CmZ5xh6v.js +0 -1
  473. package/dist/elements/chunks/text-shimmer-DYn09zrz.js +0 -1
  474. package/dist/elements/chunks/textarea-skiGHURM.js +0 -1
  475. package/dist/elements/chunks/toast-store-CFnQpThX.js +0 -1
  476. package/dist/elements/chunks/tsx-B8rCNbgL.js +0 -1
  477. package/dist/elements/chunks/typescript-RycA9KXf.js +0 -1
  478. package/dist/elements/chunks/use-card-resolution-DoA2f_EU.js +0 -1
  479. package/dist/elements/chunks/vue-BmIZj4XD.js +0 -1
  480. package/dist/elements/chunks/x-B6UnOiLw.js +0 -1
  481. package/dist/llms/llms-full.txt +0 -2949
  482. package/dist/llms/llms.txt +0 -156
  483. package/dist/register-impl-DpE7icIb.js +0 -293
  484. package/dist/ui/stat.d.ts +0 -16
  485. package/src/ui/stat.tsx +0 -41
  486. /package/dist/{elements/chunks/card-tags-D8lZ-C_U.js → card-tags-D8lZ-C_U.js} +0 -0
  487. /package/dist/{elements/chunks/link-preview-DNILK391.js → link-preview-DNILK391.js} +0 -0
  488. /package/dist/{elements/chunks/slots-DaNjX0rc.js → slots-DaNjX0rc.js} +0 -0
@@ -22,6 +22,8 @@ import {
22
22
  } from '../primitives/card-validate';
23
23
  import type { CardEnvelope, CardEvent, CardHost, CardResolution } from '../primitives/card-contract';
24
24
  import { emitCardEvent } from '../primitives/card-routing';
25
+ import { compileMask, formatForDisplay, formatRaw, normalizeToRaw } from '../primitives/field-mask';
26
+ import { FIELD_SEMANTIC_TYPES, fieldSemantics, type FieldSemanticType } from '../primitives/field-semantics';
25
27
  import { useCardHost } from '../primitives/card-host';
26
28
  import { useCardResolution } from './use-card-resolution';
27
29
  import { Check } from 'lucide-solid';
@@ -167,6 +169,233 @@ export function widgetFor(field: FormField, inlineMax: number): WidgetKind {
167
169
  }
168
170
  }
169
171
 
172
+ // ─────────────────────────────────────────────────────────────────────────────
173
+ // Field formats (spec §7.3) — the model-facing half of masked fields.
174
+ // ─────────────────────────────────────────────────────────────────────────────
175
+
176
+ /**
177
+ * What one field's `x-kai-*` format hints resolve to.
178
+ *
179
+ * The three props on the left are `Input`'s masking surface verbatim;
180
+ * `hint` is the TEXT statement of the expected format, which `FieldRow` renders and
181
+ * links through its EXISTING `aria-describedby` chain (no third channel).
182
+ * Every field is `undefined` when the field carries no hints, which is what "byte for
183
+ * byte the behavior of today" means here.
184
+ */
185
+ export interface FieldMaskHint {
186
+ /** The compiled-clean mask pattern handed to `Input`'s `format`. */
187
+ format?: string;
188
+ /** An explicit, aligned display guide. Absent = the pattern derives its own. */
189
+ guide?: string;
190
+ /** The tier-1 semantic type: `inputmode`, `autocomplete`, and the canonical form. */
191
+ semantic?: FieldSemanticType;
192
+ /** The expected format, stated in text for the description chain. */
193
+ hint?: string;
194
+ /**
195
+ * Everything that was ignored and why, as DATA rather than as a side effect.
196
+ *
197
+ * Resolution runs inside a memo on a render path, so it must never throw and must
198
+ * not warn twice for the same reason on a re-render. Returning the messages lets
199
+ * `FieldRow` own the once-per-row discipline and lets the pure tests assert the
200
+ * wording without a console spy.
201
+ */
202
+ warnings: string[];
203
+ }
204
+
205
+ /** The resolution of a field with no format hints: shared, frozen, and the default
206
+ * every non-text widget carries so the prop bag has one shape. */
207
+ export const EMPTY_MASK_HINT: FieldMaskHint = Object.freeze({ warnings: [] as string[] });
208
+
209
+ /**
210
+ * Messages already printed for a given field DEFINITION.
211
+ *
212
+ * Keyed on the field object, not on the row, and that is the fix rather than the
213
+ * convenience. A per-row `Set` looks right and does not work: `FieldRow`'s body runs
214
+ * TWICE for one visible row (measured — the parent rebuilds the `<For>` once at mount),
215
+ * so each build got its own empty Set and every bad hint printed twice. The reviewer
216
+ * caught that because the tests asserted `toHaveBeenCalled()` and never a count; they
217
+ * assert `toHaveBeenCalledTimes` now.
218
+ *
219
+ * A `WeakMap` because the key is model output: it must not pin a discarded card's field
220
+ * definitions in memory, and it must not dedupe across cards either. The repo's
221
+ * reactivity contract makes the identity exactly right — an edited field arrives as a
222
+ * NEW object (CLAUDE.md, the `kai-` contract), so a changed hint warns again, while the
223
+ * same definition rendered twice warns once.
224
+ */
225
+ const WARNED_FIELDS = new WeakMap<FormField, Set<string>>();
226
+
227
+ function warnOncePerField(field: FormField, message: string): void {
228
+ let seen = WARNED_FIELDS.get(field);
229
+ if (seen === undefined) {
230
+ seen = new Set<string>();
231
+ WARNED_FIELDS.set(field, seen);
232
+ }
233
+ if (seen.has(message)) return;
234
+ seen.add(message);
235
+ // eslint-disable-next-line no-console
236
+ console.warn(message);
237
+ }
238
+
239
+ /** Clip a model-supplied string before it reaches a console warning. Not a security
240
+ * boundary — the console is not a sink — but a 500-character pattern printing itself
241
+ * on every render is noise that hides the sentence that matters (M4 precedent). */
242
+ function clipForWarning(text: string): string {
243
+ return text.length <= 32 ? text : `${text.slice(0, 32)}… (${text.length} chars)`;
244
+ }
245
+
246
+ const SEMANTIC_TOKENS: readonly string[] = FIELD_SEMANTIC_TYPES;
247
+
248
+ /**
249
+ * Resolve a field's `x-kai-format` / `x-kai-mask` / `x-kai-mask-guide` into the
250
+ * masking props `Input` takes, or into nothing at all.
251
+ *
252
+ * EVERY INPUT HERE IS MODEL OUTPUT and therefore untrusted. The hints are
253
+ * display-only — they produce text and caret positions, never HTML, a URL or an
254
+ * attribute on a navigable element — so this is about denial of service and
255
+ * confusion, not injection: an unknown token, a pattern the engine refuses and a
256
+ * misaligned guide each degrade to the largest thing that still works, and say so.
257
+ * A THROW here would take out the whole card, including the fields that were fine.
258
+ */
259
+ export function resolveFieldMask(field: FormField, fieldKey = ''): FieldMaskHint {
260
+ const declared: unknown = field['x-kai-format'];
261
+ const mask: unknown = field['x-kai-mask'];
262
+ const guideIn: unknown = field['x-kai-mask-guide'];
263
+ const where = fieldKey ? ` on field "${clipForWarning(fieldKey)}"` : '';
264
+
265
+ if (declared === undefined) {
266
+ // A pattern with nothing to attach it to. Silence here would leave a model
267
+ // convinced it had asked for a mask and a developer looking at a plain field.
268
+ if (typeof mask === 'string' || typeof guideIn === 'string') {
269
+ return {
270
+ warnings: [
271
+ `kai-form: x-kai-mask${where} has no x-kai-format and is ignored. ` +
272
+ `Set "x-kai-format": "custom" to mask with it.`,
273
+ ],
274
+ };
275
+ }
276
+ return { warnings: [] };
277
+ }
278
+
279
+ if (typeof declared !== 'string' || !SEMANTIC_TOKENS.includes(declared)) {
280
+ return {
281
+ warnings: [
282
+ `kai-form: x-kai-format "${clipForWarning(String(declared))}"${where} is not one of ` +
283
+ `${SEMANTIC_TOKENS.join(', ')}. Rendering an unmasked text field.`,
284
+ ],
285
+ };
286
+ }
287
+
288
+ const semantic = declared as FieldSemanticType;
289
+ const warnings: string[] = [];
290
+ const custom = semantic === 'custom';
291
+
292
+ if (!custom && typeof mask === 'string') {
293
+ warnings.push(
294
+ `kai-form: x-kai-mask${where} is read only with x-kai-format "custom"; ` +
295
+ `"${semantic}" brings its own format. The mask is ignored.`,
296
+ );
297
+ }
298
+
299
+ const pattern = custom ? mask : fieldSemantics(semantic).defaultFormat;
300
+ if (typeof pattern !== 'string' || pattern === '') {
301
+ warnings.push(
302
+ `kai-form: x-kai-format "custom"${where} needs an x-kai-mask pattern. ` +
303
+ `Rendering an unmasked text field.`,
304
+ );
305
+ return { warnings };
306
+ }
307
+
308
+ // Compile the PATTERN first, alone. Compiling it with a bad guide would report the
309
+ // guide's failure and the pattern's as one event, and the two have different
310
+ // fallbacks: a refused pattern means no mask at all, a refused guide means this
311
+ // mask with its own derived guide.
312
+ let compiled;
313
+ try {
314
+ compiled = compileMask(pattern);
315
+ } catch (err) {
316
+ warnings.push(
317
+ `kai-form: x-kai-mask "${clipForWarning(pattern)}"${where} did not compile ` +
318
+ `(${err instanceof Error ? err.message : String(err)}). Rendering an unmasked text field.`,
319
+ );
320
+ return { warnings };
321
+ }
322
+
323
+ // A pattern with no `#`/`@`/`*` COMPILES — unknown characters are literals by
324
+ // position, which is the rule that makes `V-***` work and is not negotiable. But it
325
+ // compiles to capacity 0: a field that accepts nothing, shows nothing and submits
326
+ // nothing. Left alone that is the quietest failure in the whole feature, and the one
327
+ // a model is likeliest to reach — `mm/dd/yyyy` and `CHG-1234` are the strings this
328
+ // schema's own descriptions put in front of it, one free-form key away from where
329
+ // they belong. So it is refused here, out loud, on the same path as a pattern the
330
+ // engine rejects outright. `compileMask` cannot make this call for both callers: a
331
+ // zero-capacity pattern is a legitimate compile, it is only a nonsense FIELD.
332
+ if (compiled.capacity === 0) {
333
+ warnings.push(
334
+ `kai-form: x-kai-mask "${clipForWarning(pattern)}"${where} has no fill positions, so nothing ` +
335
+ `could ever be typed into it. Use # for a digit, @ for a letter or digit, * for an ` +
336
+ `obscurable one (e.g. "##/##/####", with "mm/dd/yyyy" as x-kai-mask-guide). ` +
337
+ `Rendering an unmasked text field.`,
338
+ );
339
+ return { warnings };
340
+ }
341
+
342
+ let guide = typeof guideIn === 'string' ? guideIn : undefined;
343
+ if (guide !== undefined) {
344
+ try {
345
+ compileMask(pattern, guide);
346
+ } catch (err) {
347
+ warnings.push(
348
+ `kai-form: x-kai-mask-guide "${clipForWarning(guide)}"${where} does not align with the ` +
349
+ `pattern (${err instanceof Error ? err.message : String(err)}). Using the pattern's own guide.`,
350
+ );
351
+ guide = undefined;
352
+ }
353
+ }
354
+
355
+ return {
356
+ format: pattern,
357
+ guide,
358
+ semantic,
359
+ // The guide when there is one (`mm/dd/yyyy` reads as a format to a person); the
360
+ // pattern otherwise, because a DERIVED guide is spaces at every fill position and
361
+ // says nothing out loud. A visual guide is not a description either way (spec §6).
362
+ hint: `Format: ${guide ?? pattern}`,
363
+ warnings,
364
+ };
365
+ }
366
+
367
+ /**
368
+ * The text a masked control SHOWS for a stored (canonical) value.
369
+ *
370
+ * The store holds one value per field and it is the canonical one: digits for
371
+ * `tel`/`ssn`/`credit-card`. That value is also what flows back into the control as its
372
+ * `value` prop — and writing `5550101234` over a field the masker just wrote
373
+ * `555-010-1234` into un-formats it one keystroke behind the user, with the caret
374
+ * jumping to the end. `<kai-input>` hit exactly this and split display from canonical;
375
+ * this is the same split, one layer up, for the Solid `Form`.
376
+ *
377
+ * Re-formatting through the SAME pure engine means the string handed back is the string
378
+ * already in the field, which the HTML value setter treats as a no-op (caret included).
379
+ *
380
+ * THE GUIDE BRANCH IS NOT A CHOICE MADE HERE — it mirrors `display()` in
381
+ * `primitives/input-mask.ts`: with an explicit guide the field is always the pattern's
382
+ * full length, without one it shows up to the last typed character. Disagreeing with the
383
+ * masker about that would put the fight straight back.
384
+ */
385
+ function maskedDisplay(hint: FieldMaskHint, value: unknown): unknown {
386
+ if (hint.format === undefined || typeof value !== 'string' || value === '') return value;
387
+ try {
388
+ const pattern = compileMask(hint.format, hint.guide);
389
+ const raw = normalizeToRaw(pattern, value);
390
+ return hint.guide === undefined ? formatRaw(pattern, raw) : formatForDisplay(pattern, raw);
391
+ } catch {
392
+ // Unreachable in practice — `resolveFieldMask` only reports a `format` that already
393
+ // compiled — but this runs on a render path, so it degrades to the stored text
394
+ // rather than taking the card down if that ever stops being true.
395
+ return value;
396
+ }
397
+ }
398
+
170
399
  /** Humanize a camelCase / snake_case property key into a label. */
171
400
  export function humanize(key: string): string {
172
401
  const spaced = key
@@ -626,6 +855,13 @@ export function Form(props: FormProps): JSX.Element {
626
855
  >
627
856
  <div class="flex w-full flex-wrap items-center justify-between gap-2">
628
857
  <Show when={dismissible()}>
858
+ {/* The contract `dismiss` verb — a footer ACTION (it emits
859
+ `{kind:'dismiss'}` and collapses the card to a re-openable
860
+ stub), not the chrome close in `Card`'s own `dismissible`
861
+ prop, which the contract cards deliberately never set.
862
+ confirm/choice/tasks were unlabelled ghost ✕ icons and now
863
+ match this one, so all four dismiss controls are the same
864
+ labelled ghost button. */}
629
865
  <Button
630
866
  type="button"
631
867
  variant="ghost"
@@ -738,27 +974,59 @@ interface FieldRowProps {
738
974
 
739
975
  function FieldRow(props: FieldRowProps): JSX.Element {
740
976
  const id = `f-${props.fieldKey}-${Math.random().toString(36).slice(2, 8)}`;
977
+ const labelId = `${id}-label`;
741
978
  const errorId = `${id}-err`;
742
979
  const descId = `${id}-desc`;
980
+ const formatId = `${id}-fmt`;
743
981
  const label = () => props.field.title ?? humanize(props.fieldKey);
744
982
  const widget = createMemo(() => widgetFor(props.field, props.inlineMax));
745
983
  const placeholder = () => props.field['x-kai-placeholder'];
984
+
985
+ // Field formats (spec §7.3). A memo, so a keystroke does not re-resolve: it reads
986
+ // `props.field` and nothing else, which is the subscription discipline the rest of
987
+ // this row is built on (K-D12b).
988
+ const maskHint = createMemo(() => {
989
+ const resolved = resolveFieldMask(props.field, props.fieldKey);
990
+ for (const message of resolved.warnings) warnOncePerField(props.field, message);
991
+ return resolved;
992
+ });
993
+
746
994
  const describedBy = () =>
747
- [props.field.description ? descId : '', props.error() ? errorId : '']
995
+ [
996
+ props.field.description ? descId : '',
997
+ maskHint().hint ? formatId : '',
998
+ props.error() ? errorId : '',
999
+ ]
748
1000
  .filter(Boolean)
749
1001
  .join(' ') || undefined;
750
1002
 
751
- const common = fieldCommon(props, id, placeholder, label, describedBy);
1003
+ // `fieldset` and `repeater` render a REAL <fieldset><legend> holding this same text,
1004
+ // so the row's own label would print it twice. They are the only two kinds that
1005
+ // supply their own VISIBLE grouping label.
1006
+ //
1007
+ // The other four kinds this list used to hold — radio, checkbox-group, multiselect,
1008
+ // taglist — supplied an `aria-label` and nothing else, which is a name only a screen
1009
+ // reader can reach: "Severity", "Environments" and "Tags" were announced and INVISIBLE
1010
+ // on screen. The row now always renders the text for them and hands `labelId` to the
1011
+ // widget, which points its group at it with `aria-labelledby`. One string, visible,
1012
+ // correctly associated.
1013
+ const ownLegend = () => ['fieldset', 'repeater'].includes(widget());
1014
+
1015
+ // Which kinds have no single labelable control for `<label for>` to name. Their label
1016
+ // is still an element with an id, which is what `aria-labelledby` needs; `taglist` is
1017
+ // deliberately NOT here, because its draft <input> carries the row id and a real
1018
+ // `for` association is strictly better than an ARIA one.
1019
+ const groupOnly = () => ['radio', 'checkbox-group', 'multiselect'].includes(widget());
1020
+
1021
+ const common = fieldCommon(props, id, placeholder, label, describedBy, maskHint, () =>
1022
+ ownLegend() ? undefined : labelId,
1023
+ );
752
1024
 
753
- // A nested fieldset / repeater / checkbox-group provide their own grouping
754
- // label, so the row's <label> is only rendered for simple single controls.
755
- const isGrouped = () =>
756
- ['fieldset', 'repeater', 'checkbox-group', 'multiselect', 'radio', 'taglist'].includes(widget());
757
1025
 
758
1026
  return (
759
1027
  <div class="flex flex-col gap-2 rounded-xl bg-surface p-3.5" data-field={props.fieldKey}>
760
- <Show when={!isGrouped()}>
761
- <label for={id} class="text-sm font-medium text-foreground">
1028
+ <Show when={!ownLegend()}>
1029
+ <label id={labelId} for={groupOnly() ? undefined : id} class="text-sm font-medium text-foreground">
762
1030
  {label()}
763
1031
  <Show when={props.required}>
764
1032
  <span class="text-destructive dark:text-red-400" aria-hidden="true">{' *'}</span>
@@ -772,6 +1040,15 @@ function FieldRow(props: FieldRowProps): JSX.Element {
772
1040
  </p>
773
1041
  </Show>
774
1042
 
1043
+ {/* The expected format, as TEXT and in the same describedby chain as the
1044
+ description and the error (spec §6, SC 3.3.2). The visual guide inside the
1045
+ control is not a description and does not replace this. */}
1046
+ <Show when={maskHint().hint}>
1047
+ <p id={formatId} class="text-xs text-muted-foreground">
1048
+ {maskHint().hint}
1049
+ </p>
1050
+ </Show>
1051
+
775
1052
  <WidgetSwitch widget={widget()} common={common} fieldKey={props.fieldKey} />
776
1053
 
777
1054
  <Show when={props.error()}>
@@ -806,10 +1083,17 @@ export function fieldCommon(
806
1083
  placeholder: () => string | undefined,
807
1084
  label: () => string,
808
1085
  describedBy: () => string | undefined = () => undefined,
1086
+ mask: () => FieldMaskHint = () => EMPTY_MASK_HINT,
1087
+ labelledBy: () => string | undefined = () => undefined,
809
1088
  ): ReturnType<FieldRowCommon> {
810
1089
  return {
811
1090
  id,
812
- get value() { return props.value(); },
1091
+ // The DISPLAY text for a masked field, the stored value verbatim for every other
1092
+ // one (`maskedDisplay` is the identity when nothing resolved). What the form
1093
+ // SUBMITS is untouched by this: that comes off the store, which the control fills
1094
+ // with the canonical value through `onInput`.
1095
+ get value() { return maskedDisplay(mask(), props.value()); },
1096
+ get mask() { return mask(); },
813
1097
  get field() { return props.field; },
814
1098
  get disabled() { return props.disabled || props.field.readOnly === true; },
815
1099
  get placeholder() { return placeholder(); },
@@ -817,6 +1101,7 @@ export function fieldCommon(
817
1101
  get invalid() { return Boolean(props.error()); },
818
1102
  get describedBy() { return describedBy(); },
819
1103
  get label() { return label(); },
1104
+ get labelledBy() { return labelledBy(); },
820
1105
  onInput: (v) => props.onInput(v),
821
1106
  onBlur: () => props.onBlur(),
822
1107
  };
@@ -830,6 +1115,7 @@ interface WidgetSwitchProps {
830
1115
  type FieldRowCommon = () => {
831
1116
  id: string;
832
1117
  value: unknown;
1118
+ mask: FieldMaskHint;
833
1119
  field: FormField;
834
1120
  disabled: boolean;
835
1121
  placeholder?: string;
@@ -837,6 +1123,8 @@ type FieldRowCommon = () => {
837
1123
  invalid: boolean;
838
1124
  describedBy?: string;
839
1125
  label: string;
1126
+ /** The id of the row's visible <label>, for widgets that name a GROUP. */
1127
+ labelledBy?: string;
840
1128
  onInput: (v: unknown) => void;
841
1129
  onBlur: () => void;
842
1130
  };
@@ -0,0 +1,159 @@
1
+ import { For, Show } from 'solid-js';
2
+ import { cn } from '../utils/cn';
3
+ import { Button } from '../ui/button';
4
+ import { renderIcon } from '../ui/icon';
5
+ import { isSafeUrl } from '../primitives/url-scheme-policy';
6
+ import { relativeTimeShort, isConversationUnread } from './conversation-item';
7
+ import type { ConversationSummary, HomeLinkEntry } from '../types';
8
+
9
+ export interface HomePanelProps {
10
+ greeting?: { title?: string; subtitle?: string };
11
+ /** The most-recent conversation, host-derived. `undefined` hides the card
12
+ * entirely — this component never fetches or picks one itself. */
13
+ recent?: ConversationSummary;
14
+ /** Defaults to `true`. */
15
+ showNewConversation?: boolean;
16
+ /** Defaults to `'Send us a message'`. */
17
+ newChatLabel?: string;
18
+ links?: HomeLinkEntry[];
19
+ onSelectRecent?: (id: string) => void;
20
+ onNewChat: () => void;
21
+ /** Fired only for href-less link entries — an entry with a safe `href`
22
+ * navigates as a real anchor instead. */
23
+ onLink?: (entry: HomeLinkEntry) => void;
24
+ class?: string;
25
+ }
26
+
27
+ /**
28
+ * The widget home screen (Intercom-pattern, H-1): greeting, the most-recent
29
+ * conversation, a "start a new conversation" CTA, and a list of host-defined
30
+ * links. Pure props in, events out — no fetching, no routing; a later task
31
+ * wires this behind `ChatThread`'s Home/Messages tab bar.
32
+ *
33
+ * Unsafe-href rule: a `links` entry with an `href` that fails `isSafeUrl`
34
+ * (e.g. `javascript:`) is NOT silently promoted into a button/event-emitter —
35
+ * that would let a host-authored-but-attacker-influenced config still fire a
36
+ * handler off a scheme SAFE_SCHEMES rejects. It renders as a plain,
37
+ * non-interactive row: label visible, no anchor, no button, no click handler.
38
+ */
39
+ export function HomePanel(props: HomePanelProps) {
40
+ const title = () => props.greeting?.title ?? 'Hi there 👋';
41
+
42
+ return (
43
+ <div
44
+ data-kai-home-panel
45
+ class={cn('flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-5', props.class)}
46
+ >
47
+ <div class="flex flex-col gap-1">
48
+ <h2 class="text-xl font-semibold text-foreground">{title()}</h2>
49
+ <Show when={props.greeting?.subtitle}>
50
+ <p class="text-sm text-muted-foreground">{props.greeting!.subtitle}</p>
51
+ </Show>
52
+ </div>
53
+
54
+ <Show when={props.recent}>
55
+ {(recent) => {
56
+ const time = () => relativeTimeShort(recent().updatedAt ?? recent().lastMessageAt);
57
+ const unread = () => isConversationUnread(recent());
58
+ return (
59
+ <button
60
+ type="button"
61
+ data-kai-home-recent
62
+ onClick={() => props.onSelectRecent?.(recent().id)}
63
+ class="flex flex-col gap-1 rounded-xl border border-border p-4 text-left transition-colors hover:bg-accent"
64
+ >
65
+ <span class="flex items-center gap-2 text-sm font-semibold text-foreground">
66
+ <span class="min-w-0 flex-1 truncate">{recent().title}</span>
67
+ <Show when={unread()}>
68
+ <span aria-hidden="true" class="size-1.5 shrink-0 rounded-full bg-unread" />
69
+ </Show>
70
+ <Show when={time()}>
71
+ <span class="shrink-0 text-xs font-normal text-muted-foreground">{time()}</span>
72
+ </Show>
73
+ </span>
74
+ <Show when={recent().trailing}>
75
+ <span class="line-clamp-1 text-sm text-muted-foreground">{recent().trailing}</span>
76
+ </Show>
77
+ </button>
78
+ );
79
+ }}
80
+ </Show>
81
+
82
+ <Show when={props.showNewConversation !== false}>
83
+ <Button
84
+ type="button"
85
+ data-kai-home-new
86
+ full
87
+ align="start"
88
+ onClick={() => props.onNewChat()}
89
+ class="justify-between"
90
+ >
91
+ <span>{props.newChatLabel ?? 'Send us a message'}</span>
92
+ {renderIcon('arrow-right', { class: 'size-4 shrink-0' })}
93
+ </Button>
94
+ </Show>
95
+
96
+ <Show when={props.links && props.links.length > 0}>
97
+ <div class="flex flex-col overflow-hidden rounded-xl border border-border">
98
+ <For each={props.links}>
99
+ {(entry) => {
100
+ const safeHref = () => (entry.href && isSafeUrl(entry.href) ? entry.href : undefined);
101
+ const inner = (
102
+ <>
103
+ <span class="flex min-w-0 items-center gap-2">
104
+ <Show when={entry.icon}>{renderIcon(entry.icon, { class: 'size-4 shrink-0 text-muted-foreground' })}</Show>
105
+ <span class="flex min-w-0 flex-col text-left">
106
+ <span class="truncate text-sm font-medium text-foreground">{entry.label}</span>
107
+ <Show when={entry.description}>
108
+ <span class="truncate text-xs text-muted-foreground">{entry.description}</span>
109
+ </Show>
110
+ </span>
111
+ </span>
112
+ {renderIcon('chevron-right', { class: 'size-4 shrink-0 text-muted-foreground' })}
113
+ </>
114
+ );
115
+ const rowClass = 'flex items-center justify-between gap-2 border-b border-border p-3 last:border-b-0';
116
+ return (
117
+ <Show
118
+ when={!entry.href}
119
+ fallback={
120
+ <Show
121
+ when={safeHref()}
122
+ fallback={
123
+ // Unsafe href: a non-interactive row. No anchor, no
124
+ // button, no handler — the label stays visible, it
125
+ // just never becomes clickable.
126
+ <div data-kai-home-link class={rowClass}>
127
+ {inner}
128
+ </div>
129
+ }
130
+ >
131
+ <a
132
+ data-kai-home-link
133
+ href={safeHref()}
134
+ target="_blank"
135
+ rel="noreferrer noopener"
136
+ class={cn(rowClass, 'transition-colors hover:bg-accent')}
137
+ >
138
+ {inner}
139
+ </a>
140
+ </Show>
141
+ }
142
+ >
143
+ <button
144
+ type="button"
145
+ data-kai-home-link
146
+ onClick={() => props.onLink?.(entry)}
147
+ class={cn(rowClass, 'w-full text-left transition-colors hover:bg-accent')}
148
+ >
149
+ {inner}
150
+ </button>
151
+ </Show>
152
+ );
153
+ }}
154
+ </For>
155
+ </div>
156
+ </Show>
157
+ </div>
158
+ );
159
+ }
@@ -250,7 +250,11 @@ export function TerminalLoader(props: TerminalLoaderProps) {
250
250
  const containerSizes = { sm: 'h-4', md: 'h-5', lg: 'h-6' };
251
251
  return (
252
252
  <div class={cn('flex items-center space-x-1', containerSizes[size()], props.class)}>
253
- <span class={cn('text-primary font-mono', textSizes[size()])}>{'>'}</span>
253
+ {/* Decorative status chrome, not a control — was the BRAND token
254
+ (text-primary). The sibling TextShimmerLoader below already renders
255
+ its "Thinking" text purely in --color-foreground/--color-muted-foreground,
256
+ with no brand color at all; matches that precedent. */}
257
+ <span class={cn('text-foreground font-mono', textSizes[size()])}>{'>'}</span>
254
258
  <div class={cn('bg-primary animate-[blink_1s_step-end_infinite]', cursorSizes[size()])} />
255
259
  <span class="sr-only">Loading</span>
256
260
  </div>
@@ -302,13 +306,16 @@ export function TextDotsLoader(props: TextDotsLoaderProps) {
302
306
  const textSizes = { sm: 'text-xs', md: 'text-sm', lg: 'text-base' };
303
307
  return (
304
308
  <div class={cn('inline-flex items-center', props.class)}>
305
- <span class={cn('text-primary font-medium', textSizes[size()])}>
309
+ {/* Content/chrome, not a control — was `text-primary` on the label AND
310
+ the dots (both must match: one phrase, "Thinking..."). Matches
311
+ TextShimmerLoader's neutral precedent above. */}
312
+ <span class={cn('text-foreground font-medium', textSizes[size()])}>
306
313
  {props.text ?? 'Thinking'}
307
314
  </span>
308
315
  <span class="inline-flex">
309
- <span class="text-primary animate-[loading-dots_1.4s_infinite_0.2s]">.</span>
310
- <span class="text-primary animate-[loading-dots_1.4s_infinite_0.4s]">.</span>
311
- <span class="text-primary animate-[loading-dots_1.4s_infinite_0.6s]">.</span>
316
+ <span class="text-foreground animate-[loading-dots_1.4s_infinite_0.2s]">.</span>
317
+ <span class="text-foreground animate-[loading-dots_1.4s_infinite_0.4s]">.</span>
318
+ <span class="text-foreground animate-[loading-dots_1.4s_infinite_0.6s]">.</span>
312
319
  </span>
313
320
  </div>
314
321
  );
@@ -27,7 +27,7 @@ function MessageSkills(props: MessageSkillsProps) {
27
27
  <div class={cn("flex items-center gap-1 flex-wrap", props.class)}>
28
28
  <For each={props.skills}>
29
29
  {(skill) => (
30
- <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-violet-400/10 text-violet-600 dark:text-violet-400">
30
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded text-micro font-medium bg-violet-400/10 text-violet-600 dark:text-violet-400">
31
31
  {skill.name}
32
32
  </span>
33
33
  )}