@kitn.ai/ui 0.28.0 → 0.30.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 (615) hide show
  1. package/README.md +11 -7
  2. package/dist/{Icon-DObhsRtm.js → Icon-BzBET3E-.js} +1 -1
  3. package/dist/{action-icons-CcgNN2oT.js → action-icons-DaW2pU6m.js} +1 -1
  4. package/dist/agent-tooling/construct/public.d.ts +15 -0
  5. package/dist/agent-tooling/construct/schema.d.ts +111 -0
  6. package/dist/arrow-down-D783iMJj.js +1 -0
  7. package/dist/arrow-left-0VLM5kyg.js +1 -0
  8. package/dist/artifact-DGjzHYUT.js +1 -0
  9. package/dist/{attachments-Qr4kg-2b.js → attachments-YvXHlg88.js} +1 -1
  10. package/dist/{audio-visualizer-DToYdfSc.js → audio-visualizer-9lz5IHsL.js} +1 -1
  11. package/dist/{badge-CdEu_jn2.js → badge-Dh_vp8A5.js} +1 -1
  12. package/dist/{bell-DQAAOpDz.js → bell-CT7_WUwS.js} +1 -1
  13. package/dist/{button-DkuOnGLy.js → button-C_3te6fm.js} +1 -1
  14. package/dist/{card-renderer-Jx70m6_l.js → card-renderer-DZVaGH2e.js} +1 -1
  15. package/dist/check-CiEOVnEN.js +1 -0
  16. package/dist/{checkbox-D7iRB3nL.js → checkbox-Bu2T2e0U.js} +1 -1
  17. package/dist/{checkbox-group-B2_rpoPM.js → checkbox-group-BiryEXPQ.js} +1 -1
  18. package/dist/chevron-down-CSobuZRG.js +1 -0
  19. package/dist/chevron-right-DR9ZAP-2.js +1 -0
  20. package/dist/{choice-card-m0BYqqOK.js → choice-card-CM6IjkmL.js} +1 -1
  21. package/dist/circle-BC66EYF7.js +1 -0
  22. package/dist/circle-check-BqmahW0O.js +1 -0
  23. package/dist/{circle-x-BPG0iPst.js → circle-x-Bk-dA5Wf.js} +1 -1
  24. package/dist/{code-block-ClMLKb7C.js → code-block-B26t-Q7q.js} +1 -1
  25. package/dist/{collapsible-B4DlWicL.js → collapsible-Bhc636Cf.js} +1 -1
  26. package/dist/components/builder-composer-triggers.d.ts +62 -0
  27. package/dist/components/builder-layout.d.ts +54 -0
  28. package/dist/components/builder-message-actions.d.ts +72 -0
  29. package/dist/components/builder-panel.d.ts +189 -0
  30. package/dist/components/builder-preview.d.ts +11 -0
  31. package/dist/components/builder-shell-controls.d.ts +69 -0
  32. package/dist/components/builder-skeleton.d.ts +58 -0
  33. package/dist/components/builder-start.d.ts +75 -0
  34. package/dist/components/builder-workspace-variants.d.ts +23 -0
  35. package/dist/components/captions.d.ts +66 -0
  36. package/dist/{composer-sQ0HcvyG.js → composer-DMI85B6j.js} +1 -1
  37. package/dist/{confirm-card-rQWDQsnC.js → confirm-card-B0YIAwXp.js} +1 -1
  38. package/dist/construct-cli.es.js +3 -3
  39. package/dist/construct.d.ts +5 -0
  40. package/dist/construct.js +327 -0
  41. package/dist/{context-0kg4ZX1J.js → context-DWgfhqWp.js} +1 -1
  42. package/dist/controllable-BagRpyHv.js +1 -0
  43. package/dist/{conversation-item-BxwQLaFJ.js → conversation-item-BsOsVGmT.js} +1 -1
  44. package/dist/{conversation-list-BCfGN1JU.js → conversation-list-aX3Vtc7L.js} +1 -1
  45. package/dist/{copy-n9u9rqpi.js → copy-BmEwsNhJ.js} +1 -1
  46. package/dist/core-C8fzo39E.js +1 -12
  47. package/dist/{create-tween-uPkArjIG.js → create-tween-CNOAU3Fx.js} +1 -1
  48. package/dist/{create-tween-C-q-JCM-.js → create-tween-WfqiENls.js} +1 -1
  49. package/dist/{create-tween-CRrJXxBd.js → create-tween-wlFtW9Kh.js} +1 -1
  50. package/dist/{create-tween-DZ3RRDHD.js → create-tween-zt79ba32.js} +1 -1
  51. package/dist/{default-input-CaMrJkgH.js → default-input-DMV5Eaze.js} +1 -1
  52. package/dist/define-CI0y3eE4.js +1 -0
  53. package/dist/define.js +40 -40
  54. package/dist/define.server.js +45 -45
  55. package/dist/{disclosure-WUImBbDv.js → disclosure-NnACBxsC.js} +1 -1
  56. package/dist/{dropdown-wZVeJOMd.js → dropdown-DT7G0RGR.js} +1 -1
  57. package/dist/elements/agent-card.js +1 -1
  58. package/dist/elements/artifact.js +1 -1
  59. package/dist/elements/attachments.js +1 -1
  60. package/dist/elements/audio-visualizer.js +1 -1
  61. package/dist/elements/avatar.js +1 -1
  62. package/dist/elements/badge.js +1 -1
  63. package/dist/elements/button.js +1 -1
  64. package/dist/elements/card.js +1 -1
  65. package/dist/elements/cards.js +1 -1
  66. package/dist/elements/chain-of-thought.js +1 -1
  67. package/dist/elements/chat-scope-picker.js +1 -1
  68. package/dist/elements/chat-workspace.js +1 -1
  69. package/dist/elements/chat.js +1 -1
  70. package/dist/elements/checkbox-group.js +1 -1
  71. package/dist/elements/checkbox.js +1 -1
  72. package/dist/elements/checkpoint.js +1 -1
  73. package/dist/elements/choice.js +1 -1
  74. package/dist/elements/coachmark.js +1 -1
  75. package/dist/elements/code-block.js +1 -1
  76. package/dist/elements/command.js +1 -1
  77. package/dist/elements/compare.js +1 -1
  78. package/dist/elements/composer.js +1 -1
  79. package/dist/elements/confirm-card.js +1 -1
  80. package/dist/elements/context-meter.js +1 -1
  81. package/dist/elements/conversation-item.js +1 -1
  82. package/dist/elements/conversation-list.js +1 -1
  83. package/dist/elements/dialog.js +1 -1
  84. package/dist/elements/dock.js +1 -1
  85. package/dist/elements/dropdown.js +1 -1
  86. package/dist/elements/editable-label.js +1 -1
  87. package/dist/elements/embed.js +1 -1
  88. package/dist/elements/empty.js +1 -1
  89. package/dist/elements/feedback-bar.js +1 -1
  90. package/dist/elements/file-tree.js +1 -1
  91. package/dist/elements/file-upload.js +1 -1
  92. package/dist/elements/form.js +1 -1
  93. package/dist/elements/hover-card.js +1 -1
  94. package/dist/elements/icon.js +1 -1
  95. package/dist/elements/image.js +1 -1
  96. package/dist/elements/input.js +1 -1
  97. package/dist/elements/kbd.js +1 -1
  98. package/dist/elements/link-preview.js +1 -1
  99. package/dist/elements/loader.js +1 -1
  100. package/dist/elements/markdown.js +1 -1
  101. package/dist/elements/menu.js +1 -1
  102. package/dist/elements/message-skills.js +1 -1
  103. package/dist/elements/message.js +1 -1
  104. package/dist/elements/model-switcher.js +1 -1
  105. package/dist/elements/nav.js +1 -1
  106. package/dist/elements/notice.js +1 -1
  107. package/dist/elements/pane-grid.js +1 -1
  108. package/dist/elements/pane-group.js +1 -1
  109. package/dist/elements/pane.js +1 -1
  110. package/dist/elements/popover.js +1 -1
  111. package/dist/elements/progress-bar.js +1 -1
  112. package/dist/elements/prompt-dock.js +1 -1
  113. package/dist/elements/prompt-input.js +1 -1
  114. package/dist/elements/prompt-suggestions.js +1 -1
  115. package/dist/elements/radio-group.js +1 -1
  116. package/dist/elements/reasoning.js +1 -1
  117. package/dist/elements/remote.js +1 -1
  118. package/dist/elements/resizable.js +1 -1
  119. package/dist/elements/response-stream.js +1 -1
  120. package/dist/elements/screen.js +1 -1
  121. package/dist/elements/scroll-area.js +1 -1
  122. package/dist/elements/scroll-button.js +1 -1
  123. package/dist/elements/search.js +1 -1
  124. package/dist/elements/segmented.js +1 -1
  125. package/dist/elements/select.js +1 -1
  126. package/dist/elements/separator.js +1 -1
  127. package/dist/elements/setting-item.js +1 -1
  128. package/dist/elements/settings-group.js +1 -1
  129. package/dist/elements/skeleton.js +1 -1
  130. package/dist/elements/slider.js +1 -1
  131. package/dist/elements/source.js +1 -1
  132. package/dist/elements/status.js +1 -1
  133. package/dist/elements/switch.js +1 -1
  134. package/dist/elements/tabs.js +1 -1
  135. package/dist/elements/tasks.js +1 -1
  136. package/dist/elements/text-shimmer.js +1 -1
  137. package/dist/elements/thinking-bar.js +1 -1
  138. package/dist/elements/thread.js +1 -1
  139. package/dist/elements/toast.js +1 -1
  140. package/dist/elements/tool.js +1 -1
  141. package/dist/elements/tooltip.js +1 -1
  142. package/dist/elements/voice-input.js +1 -1
  143. package/dist/elements/voice-output.js +1 -1
  144. package/dist/{ellipsis-CbOpJnU3.js → ellipsis-6pJ7SxYT.js} +1 -1
  145. package/dist/{embed-CrfjkbUG.js → embed-BMje-nxj.js} +1 -1
  146. package/dist/engine-javascript-C1x7zo1_.js +1 -141
  147. package/dist/{external-link-ByXAwBBQ.js → external-link-Beh9DKeR.js} +1 -1
  148. package/dist/{file-text-DMib2qwt.js → file-text-D1GPfZ8Q.js} +1 -1
  149. package/dist/{file-tree-CJqM2aP7.js → file-tree-Dwj2lqg-.js} +1 -1
  150. package/dist/{folder-_BAb929C.js → folder-b6D8x1DY.js} +1 -1
  151. package/dist/{form-DSZgZQZk.js → form-DWgdxDXu.js} +1 -1
  152. package/dist/{hover-card-BQbul9eq.js → hover-card-CWRkqm40.js} +1 -1
  153. package/dist/{icon-CdcfRhYI.js → icon-CL0jdhsf.js} +1 -1
  154. package/dist/index.js +1741 -1725
  155. package/dist/index.server.js +1582 -1566
  156. package/dist/{info-DsFjGuI_.js → info-0cOUherQ.js} +1 -1
  157. package/dist/{input-DKbmkMos.js → input-BH6Ru91Q.js} +1 -1
  158. package/dist/kai.es.js +1 -1
  159. package/dist/{kbd-PvdGhX4s.js → kbd-DRAnVtNX.js} +1 -1
  160. package/dist/{link-zF8PzWfF.js → link-Dlr0vssB.js} +1 -1
  161. package/dist/{link-preview-DbM1866N.js → link-preview-DODgRyV9.js} +1 -1
  162. package/dist/loader-KvJB6NrT.js +1 -0
  163. package/dist/{markdown-LMvmAyBk.js → markdown-DX9tuJVj.js} +1 -1
  164. package/dist/mcp.es.js +9 -5
  165. package/dist/{message-BYZECyek.js → message-BqQ7Ot5q.js} +1 -1
  166. package/dist/{message-BSsgvHvf.js → message-DkaGfBd8.js} +1 -1
  167. package/dist/message-circle-BPpp5QKv.js +1 -0
  168. package/dist/message-square-BLbsd9BJ.js +1 -0
  169. package/dist/{minimize-2-BQRoBV1V.js → minimize-2-CyOHJwcN.js} +1 -1
  170. package/dist/{model-switcher-CMpCOYcB.js → model-switcher-0hrKDiK2.js} +1 -1
  171. package/dist/{overlay-Cb-MkyDa.js → overlay-DBMFXie6.js} +1 -1
  172. package/dist/{panel-right-i8warFxT.js → panel-right-t8SWJL6e.js} +1 -1
  173. package/dist/{paperclip-ByY3UVO6.js → paperclip-CdzuOjbU.js} +1 -1
  174. package/dist/play-BXy1u7rW.js +1 -0
  175. package/dist/primitives/use-auto-resize.d.ts +11 -0
  176. package/dist/{progress-bar-jVAHvMUf.js → progress-bar-CiBtxMWT.js} +1 -1
  177. package/dist/{prompt-suggestion-CD-qtNgC.js → prompt-suggestion-I_DHP21i.js} +1 -1
  178. package/dist/{radio-BsoBMhkF.js → radio-C_p3n39l.js} +1 -1
  179. package/dist/{reasoning-4kBPtlTx.js → reasoning-BDwy_tIe.js} +1 -1
  180. package/dist/register-impl-DvEnhIxx.js +424 -0
  181. package/dist/{resizable-uPmY9VY-.js → resizable-SYC-fStM.js} +1 -1
  182. package/dist/{rotate-ccw-B2xn93py.js → rotate-ccw-B1nwmwuY.js} +1 -1
  183. package/dist/{rotate-cw-CMVLoCyH.js → rotate-cw-CLAnBcm1.js} +1 -1
  184. package/dist/{scroll-area-BqLUo8ZZ.js → scroll-area-zRyXoLhI.js} +1 -1
  185. package/dist/{scroll-button-DslXiJyL.js → scroll-button-jzrbYCG9.js} +1 -1
  186. package/dist/{select-2cLRtPGT.js → select-DM-0nEe0.js} +1 -1
  187. package/dist/{separator-BWQtNJcM.js → separator-BToOOoOt.js} +1 -1
  188. package/dist/{settings-BBR63jLg.js → settings-PaGo_S4U.js} +1 -1
  189. package/dist/{settings-group-B2ciy-Iw.js → settings-group-BFXGIfXq.js} +1 -1
  190. package/dist/{skeleton-CkHI-HgC.js → skeleton-DYpmFnnm.js} +1 -1
  191. package/dist/{slider-DDR9U-i0.js → slider-ebutAmkQ.js} +1 -1
  192. package/dist/{solid-DruVOXnf.js → solid-15v_3WVL.js} +3563 -3412
  193. package/dist/{solid-BzCFdL25.js → solid-5P-otgdi.js} +4502 -4296
  194. package/dist/solid.d.ts +6 -0
  195. package/dist/solid.js +259 -254
  196. package/dist/solid.server.js +259 -254
  197. package/dist/{source-GEnFF528.js → source-c1gpRl9h.js} +1 -1
  198. package/dist/{star-CilHq8GP.js → star-pcV_3LqF.js} +1 -1
  199. package/dist/{store-Ciyk6eZG.js → store-CSgbFFCg.js} +1 -1
  200. package/dist/{switch-KkeQIbuH.js → switch-Bh2OdLV2.js} +1 -1
  201. package/dist/{tasks-card-BrN4bggJ.js → tasks-card-CbqQAn2S.js} +1 -1
  202. package/dist/text-shimmer-BSzgEnDH.js +1 -0
  203. package/dist/textarea-DrYNt_0z.js +1 -0
  204. package/dist/theme.tokens.css +22 -21
  205. package/dist/{thumbs-up-CsT9Wzb9.js → thumbs-up-BL3-NenK.js} +1 -1
  206. package/dist/{toast-store-tgK6MuMv.js → toast-store-DkOlYczt.js} +1 -1
  207. package/dist/{tool-DzUjIBT8.js → tool-CeIWUVUe.js} +1 -1
  208. package/dist/{tooltip-Bkkg_4t0.js → tooltip-GA-nmg06.js} +1 -1
  209. package/dist/{trash-2-jBKxjGGQ.js → trash-2-CT66QWlm.js} +1 -1
  210. package/dist/{triangle-alert-DlQ68ppf.js → triangle-alert-CeHLWxhD.js} +1 -1
  211. package/dist/ui/color-field.d.ts +48 -0
  212. package/dist/ui/textarea.d.ts +4 -0
  213. package/dist/ui/toggle-chip.d.ts +40 -0
  214. package/dist/{upload-TcAHXv9W.js → upload-9Csb0F0J.js} +1 -1
  215. package/dist/{use-card-resolution-DaZqAdWf.js → use-card-resolution-Zg0a08Z-.js} +1 -1
  216. package/dist/{variant-aurora-DbW_OWL2.js → variant-aurora-4VBQHIKV.js} +1 -1
  217. package/dist/{variant-aurora-CNmfHhcx.js → variant-aurora-DBeCAQNk.js} +2 -2
  218. package/dist/{variant-aurora-D6GsdTPA.js → variant-aurora-FzHVXHby.js} +2 -2
  219. package/dist/{variant-aurora-BhLuYxJR.js → variant-aurora-LE3fqxgw.js} +1 -1
  220. package/dist/{variant-custom-BvYHR62b.js → variant-custom-AFRGcOBS.js} +1 -1
  221. package/dist/{variant-custom-DV2x254s.js → variant-custom-BQkNuPpE.js} +2 -2
  222. package/dist/{variant-custom-BRvaNHSi.js → variant-custom-DI5R0rln.js} +1 -1
  223. package/dist/{variant-custom-DMTrEO5w.js → variant-custom-WWnrrRHS.js} +2 -2
  224. package/dist/{variant-wave-CyBueXj9.js → variant-wave-DUCbH5oI.js} +2 -2
  225. package/dist/{variant-wave-BNMGrDfj.js → variant-wave-DuQV7DhQ.js} +2 -2
  226. package/dist/{variant-wave-BpDd8t3Z.js → variant-wave-nSI2sfII.js} +1 -1
  227. package/dist/{variant-wave-sp-qwsdI.js → variant-wave-wTP5v_jU.js} +1 -1
  228. package/dist/x-eiO9V6J6.js +1 -0
  229. package/{src/elements/styles.css → kit-base.css} +27 -44
  230. package/package.json +32 -17
  231. package/solid.css +33 -0
  232. package/theme.css +39 -23
  233. package/bin/route.test.js +0 -58
  234. package/dist/arrow-down-CjRU3JgV.js +0 -1
  235. package/dist/arrow-left-Bnpb1UEW.js +0 -1
  236. package/dist/artifact-Bwt7OgcR.js +0 -1
  237. package/dist/check-njkVdZu2.js +0 -1
  238. package/dist/chevron-down-B72s6PXR.js +0 -1
  239. package/dist/chevron-right-BatuHzUm.js +0 -1
  240. package/dist/circle-check-B62pODB3.js +0 -1
  241. package/dist/circle-zx0ByzY9.js +0 -1
  242. package/dist/controllable-CnGpMF6r.js +0 -1
  243. package/dist/define-By-uL588.js +0 -1
  244. package/dist/loader-BM4CEz4G.js +0 -1
  245. package/dist/message-circle-DruwObfX.js +0 -1
  246. package/dist/message-square-BG6q6mlm.js +0 -1
  247. package/dist/play-BvKKq3fX.js +0 -1
  248. package/dist/register-impl-B1hLkrI8.js +0 -424
  249. package/dist/text-shimmer-Dcj1MHuq.js +0 -1
  250. package/dist/textarea-Dpvbngbg.js +0 -1
  251. package/dist/x-Z5UntkIJ.js +0 -1
  252. package/src/agent-tooling/README.md +0 -192
  253. package/src/agent-tooling/archetypes.ts +0 -205
  254. package/src/agent-tooling/catalog/README.md +0 -676
  255. package/src/agent-tooling/catalog/catalog-types.ts +0 -231
  256. package/src/agent-tooling/catalog/fabrications.ts +0 -96
  257. package/src/agent-tooling/catalog/invariants.ts +0 -284
  258. package/src/agent-tooling/catalog/labs-titles.ts +0 -114
  259. package/src/agent-tooling/catalog/scenarios.ts +0 -87
  260. package/src/agent-tooling/catalog/surfaces.ts +0 -347
  261. package/src/agent-tooling/construct/cli-entry.ts +0 -8
  262. package/src/agent-tooling/construct/cli.ts +0 -147
  263. package/src/agent-tooling/construct/codegen.ts +0 -1635
  264. package/src/agent-tooling/construct/construct.v1.schema.json +0 -319
  265. package/src/agent-tooling/construct/dev.ts +0 -148
  266. package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +0 -7
  267. package/src/agent-tooling/construct/fixtures/ops-console.construct.json +0 -44
  268. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +0 -32
  269. package/src/agent-tooling/construct/schema.ts +0 -452
  270. package/src/agent-tooling/integrations/anthropic.ts +0 -442
  271. package/src/agent-tooling/integrations/cloudflare.ts +0 -218
  272. package/src/agent-tooling/integrations/langgraph.ts +0 -133
  273. package/src/agent-tooling/integrations/mastra.ts +0 -182
  274. package/src/agent-tooling/integrations/mock.ts +0 -148
  275. package/src/agent-tooling/integrations/ollama.ts +0 -97
  276. package/src/agent-tooling/integrations/openai.ts +0 -95
  277. package/src/agent-tooling/integrations/openrouter.ts +0 -82
  278. package/src/agent-tooling/integrations/pi.ts +0 -176
  279. package/src/agent-tooling/integrations/pydantic-ai.ts +0 -108
  280. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +0 -398
  281. package/src/agent-tooling/mcp/css-raw.d.ts +0 -12
  282. package/src/agent-tooling/mcp/manifest.ts +0 -422
  283. package/src/agent-tooling/mcp/server.ts +0 -116
  284. package/src/agent-tooling/mcp/stdio.ts +0 -18
  285. package/src/agent-tooling/mcp/tools/construct.ts +0 -130
  286. package/src/agent-tooling/mcp/tools/debug.ts +0 -621
  287. package/src/agent-tooling/mcp/tools/reference.ts +0 -980
  288. package/src/agent-tooling/mcp/tools/scaffold.ts +0 -6708
  289. package/src/agent-tooling/mcp/tools/theme.ts +0 -456
  290. package/src/agent-tooling/mcp/tools/types.ts +0 -14
  291. package/src/agent-tooling/mcp/validate-args.ts +0 -141
  292. package/src/agent-tooling/recipes/composed-thread.ts +0 -714
  293. package/src/agent-tooling/recipes/index.ts +0 -22
  294. package/src/agent-tooling/recipes/types.ts +0 -32
  295. package/src/agent-tooling/registry.ts +0 -132
  296. package/src/agent-tooling/route-emit.ts +0 -305
  297. package/src/agent-tooling/types.ts +0 -390
  298. package/src/components/artifact-card.tsx +0 -120
  299. package/src/components/artifact.tsx +0 -914
  300. package/src/components/attachment-types.ts +0 -66
  301. package/src/components/attachments.tsx +0 -536
  302. package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +0 -257
  303. package/src/components/audio-visualizer/aurora.glsl.ts +0 -330
  304. package/src/components/audio-visualizer/fit-scale.ts +0 -112
  305. package/src/components/audio-visualizer/index.tsx +0 -528
  306. package/src/components/audio-visualizer/labs/lab-choreography.ts +0 -187
  307. package/src/components/audio-visualizer/labs/lab-shaders.ts +0 -363
  308. package/src/components/audio-visualizer/labs/lab-visualizer.tsx +0 -102
  309. package/src/components/audio-visualizer/shader-canvas.tsx +0 -876
  310. package/src/components/audio-visualizer/sizes.ts +0 -54
  311. package/src/components/audio-visualizer/variant-aurora.tsx +0 -305
  312. package/src/components/audio-visualizer/variant-bar.tsx +0 -175
  313. package/src/components/audio-visualizer/variant-custom.tsx +0 -344
  314. package/src/components/audio-visualizer/variant-grid.tsx +0 -175
  315. package/src/components/audio-visualizer/variant-radial.tsx +0 -163
  316. package/src/components/audio-visualizer/variant-wave.tsx +0 -130
  317. package/src/components/audio-visualizer/wave.glsl.ts +0 -130
  318. package/src/components/card-fallback.tsx +0 -60
  319. package/src/components/card-renderer.tsx +0 -220
  320. package/src/components/card.tsx +0 -212
  321. package/src/components/chain-of-thought.tsx +0 -348
  322. package/src/components/chat-container.tsx +0 -89
  323. package/src/components/chat-scope-picker.tsx +0 -57
  324. package/src/components/chat-thread.tsx +0 -1014
  325. package/src/components/checkpoint.tsx +0 -88
  326. package/src/components/choice-card.tsx +0 -743
  327. package/src/components/coachmark.tsx +0 -249
  328. package/src/components/code-block.tsx +0 -181
  329. package/src/components/composer-dom.ts +0 -159
  330. package/src/components/composer-highlight.ts +0 -242
  331. package/src/components/composer-history.ts +0 -82
  332. package/src/components/composer.tsx +0 -1075
  333. package/src/components/confirm-card.tsx +0 -402
  334. package/src/components/context.tsx +0 -377
  335. package/src/components/conversation-item.tsx +0 -241
  336. package/src/components/conversation-list.tsx +0 -429
  337. package/src/components/conversation-panel.tsx +0 -118
  338. package/src/components/dismissed-stub.tsx +0 -80
  339. package/src/components/embed.tsx +0 -200
  340. package/src/components/empty.tsx +0 -177
  341. package/src/components/feedback-bar.tsx +0 -166
  342. package/src/components/file-tree.tsx +0 -530
  343. package/src/components/file-upload.tsx +0 -167
  344. package/src/components/form-widgets.tsx +0 -525
  345. package/src/components/form.tsx +0 -1296
  346. package/src/components/home-panel.tsx +0 -159
  347. package/src/components/image.tsx +0 -65
  348. package/src/components/link-preview.tsx +0 -195
  349. package/src/components/loader.tsx +0 -367
  350. package/src/components/markdown.tsx +0 -162
  351. package/src/components/message-skills.tsx +0 -40
  352. package/src/components/message.tsx +0 -765
  353. package/src/components/model-switcher.tsx +0 -132
  354. package/src/components/prompt-input.tsx +0 -188
  355. package/src/components/prompt-suggestion.tsx +0 -150
  356. package/src/components/reasoning.tsx +0 -293
  357. package/src/components/response-compare-types.ts +0 -152
  358. package/src/components/response-compare.tsx +0 -455
  359. package/src/components/response-stream.tsx +0 -109
  360. package/src/components/screen.tsx +0 -181
  361. package/src/components/scroll-button.tsx +0 -93
  362. package/src/components/source.tsx +0 -192
  363. package/src/components/tasks-card.tsx +0 -801
  364. package/src/components/text-shimmer.tsx +0 -37
  365. package/src/components/thinking-bar.tsx +0 -50
  366. package/src/components/thread.tsx +0 -228
  367. package/src/components/toast.tsx +0 -503
  368. package/src/components/tool-classify.ts +0 -26
  369. package/src/components/tool-types.ts +0 -49
  370. package/src/components/tool.tsx +0 -190
  371. package/src/components/use-card-resolution.ts +0 -44
  372. package/src/components/voice-input.tsx +0 -222
  373. package/src/components/voice-output.tsx +0 -242
  374. package/src/components/widget-tab-bar.tsx +0 -73
  375. package/src/components/workspace-shell.tsx +0 -285
  376. package/src/diagnostics/hook.ts +0 -352
  377. package/src/diagnostics/index.ts +0 -82
  378. package/src/diagnostics/report-request.ts +0 -296
  379. package/src/elements/agent-card.tsx +0 -67
  380. package/src/elements/artifact.tsx +0 -194
  381. package/src/elements/attachments.tsx +0 -140
  382. package/src/elements/audio-visualizer.tsx +0 -227
  383. package/src/elements/autoloader.ts +0 -89
  384. package/src/elements/avatar.tsx +0 -33
  385. package/src/elements/badge.tsx +0 -32
  386. package/src/elements/button.tsx +0 -149
  387. package/src/elements/card.tsx +0 -133
  388. package/src/elements/cards.tsx +0 -266
  389. package/src/elements/chain-of-thought.tsx +0 -141
  390. package/src/elements/chat-scope-picker.tsx +0 -65
  391. package/src/elements/chat-types.ts +0 -102
  392. package/src/elements/chat-workspace.tsx +0 -188
  393. package/src/elements/chat.tsx +0 -304
  394. package/src/elements/checkbox-group.tsx +0 -164
  395. package/src/elements/checkbox.tsx +0 -134
  396. package/src/elements/checkpoint.tsx +0 -47
  397. package/src/elements/choice.tsx +0 -100
  398. package/src/elements/coachmark.tsx +0 -90
  399. package/src/elements/code-block.tsx +0 -60
  400. package/src/elements/command.tsx +0 -197
  401. package/src/elements/compare.tsx +0 -111
  402. package/src/elements/compiled.css +0 -2
  403. package/src/elements/composer.tsx +0 -162
  404. package/src/elements/confirm-card.tsx +0 -76
  405. package/src/elements/context-meter.tsx +0 -107
  406. package/src/elements/conversation-item.tsx +0 -120
  407. package/src/elements/conversation-list.tsx +0 -203
  408. package/src/elements/css.ts +0 -5
  409. package/src/elements/default-input.tsx +0 -392
  410. package/src/elements/define-entry.ts +0 -12
  411. package/src/elements/define.tsx +0 -563
  412. package/src/elements/diagnostic-events.ts +0 -114
  413. package/src/elements/dialog.tsx +0 -145
  414. package/src/elements/disclosure.ts +0 -95
  415. package/src/elements/dock.tsx +0 -215
  416. package/src/elements/dropdown.tsx +0 -123
  417. package/src/elements/editable-label.tsx +0 -140
  418. package/src/elements/element-data-types.ts +0 -152
  419. package/src/elements/element-diagnostics.ts +0 -392
  420. package/src/elements/element-manifest.json +0 -354
  421. package/src/elements/element-nonscalar.json +0 -187
  422. package/src/elements/element-types.d.ts +0 -4353
  423. package/src/elements/embed.tsx +0 -35
  424. package/src/elements/empty.tsx +0 -29
  425. package/src/elements/feedback-bar.tsx +0 -66
  426. package/src/elements/file-tree.tsx +0 -62
  427. package/src/elements/file-upload.tsx +0 -44
  428. package/src/elements/form.tsx +0 -94
  429. package/src/elements/hover-card.tsx +0 -80
  430. package/src/elements/icon.tsx +0 -41
  431. package/src/elements/image.tsx +0 -32
  432. package/src/elements/input.tsx +0 -492
  433. package/src/elements/kbd.tsx +0 -45
  434. package/src/elements/link-preview.tsx +0 -34
  435. package/src/elements/loader.tsx +0 -25
  436. package/src/elements/markdown.tsx +0 -38
  437. package/src/elements/menu.tsx +0 -241
  438. package/src/elements/message-skills.tsx +0 -83
  439. package/src/elements/message.tsx +0 -361
  440. package/src/elements/model-switcher.tsx +0 -121
  441. package/src/elements/nav.tsx +0 -87
  442. package/src/elements/notice.tsx +0 -53
  443. package/src/elements/pane-grid.tsx +0 -111
  444. package/src/elements/pane-group.tsx +0 -119
  445. package/src/elements/pane.tsx +0 -129
  446. package/src/elements/popover.tsx +0 -81
  447. package/src/elements/progress-bar.tsx +0 -39
  448. package/src/elements/prompt-dock.tsx +0 -82
  449. package/src/elements/prompt-input.tsx +0 -240
  450. package/src/elements/prompt-suggestions.tsx +0 -113
  451. package/src/elements/radio-group.tsx +0 -129
  452. package/src/elements/reasoning.tsx +0 -71
  453. package/src/elements/register-impl.ts +0 -148
  454. package/src/elements/register.ts +0 -76
  455. package/src/elements/remote.tsx +0 -225
  456. package/src/elements/resizable.globals.d.ts +0 -36
  457. package/src/elements/resizable.tsx +0 -704
  458. package/src/elements/response-stream.tsx +0 -40
  459. package/src/elements/screen.tsx +0 -90
  460. package/src/elements/scroll-area.tsx +0 -33
  461. package/src/elements/scroll-button.tsx +0 -179
  462. package/src/elements/search.tsx +0 -185
  463. package/src/elements/segmented.tsx +0 -129
  464. package/src/elements/select.tsx +0 -165
  465. package/src/elements/separator.tsx +0 -36
  466. package/src/elements/setting-item.tsx +0 -50
  467. package/src/elements/settings-group.tsx +0 -37
  468. package/src/elements/skeleton.tsx +0 -45
  469. package/src/elements/slider.tsx +0 -171
  470. package/src/elements/slot-text.ts +0 -73
  471. package/src/elements/slots.ts +0 -787
  472. package/src/elements/source.tsx +0 -126
  473. package/src/elements/status.tsx +0 -42
  474. package/src/elements/switch.tsx +0 -118
  475. package/src/elements/tabs.tsx +0 -95
  476. package/src/elements/tasks.tsx +0 -103
  477. package/src/elements/text-shimmer.tsx +0 -28
  478. package/src/elements/thinking-bar.tsx +0 -43
  479. package/src/elements/thread.tsx +0 -139
  480. package/src/elements/toast.tsx +0 -106
  481. package/src/elements/tool.tsx +0 -78
  482. package/src/elements/tooltip.tsx +0 -85
  483. package/src/elements/validate-messages.ts +0 -76
  484. package/src/elements/voice-input.tsx +0 -100
  485. package/src/elements/voice-output.tsx +0 -82
  486. package/src/index.ts +0 -430
  487. package/src/primitives/audio-bands.ts +0 -174
  488. package/src/primitives/card-component-types.ts +0 -63
  489. package/src/primitives/card-contract.ts +0 -80
  490. package/src/primitives/card-data-types.ts +0 -361
  491. package/src/primitives/card-host.tsx +0 -37
  492. package/src/primitives/card-recovery.ts +0 -160
  493. package/src/primitives/card-registry.tsx +0 -78
  494. package/src/primitives/card-resolution.ts +0 -36
  495. package/src/primitives/card-routing.ts +0 -85
  496. package/src/primitives/card-schemas/artifact.schema.json +0 -92
  497. package/src/primitives/card-schemas/card-envelope.schema.json +0 -14
  498. package/src/primitives/card-schemas/card-event.schema.json +0 -12
  499. package/src/primitives/card-schemas/choice.schema.json +0 -75
  500. package/src/primitives/card-schemas/confirm.schema.json +0 -65
  501. package/src/primitives/card-schemas/embed.schema.json +0 -63
  502. package/src/primitives/card-schemas/form.result.schema.json +0 -7
  503. package/src/primitives/card-schemas/form.schema.json +0 -53
  504. package/src/primitives/card-schemas/link.schema.json +0 -54
  505. package/src/primitives/card-schemas/tasks.result.schema.json +0 -16
  506. package/src/primitives/card-schemas/tasks.schema.json +0 -83
  507. package/src/primitives/card-tags.ts +0 -61
  508. package/src/primitives/card-validate-cards.ts +0 -255
  509. package/src/primitives/card-validate-schemas.ts +0 -58
  510. package/src/primitives/card-validate.ts +0 -131
  511. package/src/primitives/chat-config.tsx +0 -103
  512. package/src/primitives/composer-model.ts +0 -49
  513. package/src/primitives/composer-triggers.ts +0 -35
  514. package/src/primitives/controllable.ts +0 -21
  515. package/src/primitives/conversation-store.ts +0 -248
  516. package/src/primitives/create-kai-chat.ts +0 -66
  517. package/src/primitives/create-tween.ts +0 -266
  518. package/src/primitives/embed-providers.ts +0 -254
  519. package/src/primitives/field-mask.ts +0 -256
  520. package/src/primitives/field-semantics.ts +0 -115
  521. package/src/primitives/highlighter.ts +0 -157
  522. package/src/primitives/input-mask.ts +0 -853
  523. package/src/primitives/link-preview.ts +0 -87
  524. package/src/primitives/message-feedback.ts +0 -124
  525. package/src/primitives/pdf-preview.ts +0 -121
  526. package/src/primitives/toast-store.ts +0 -338
  527. package/src/primitives/url-scheme-policy.ts +0 -70
  528. package/src/primitives/use-audio-analysis.ts +0 -340
  529. package/src/primitives/use-auto-resize.ts +0 -31
  530. package/src/primitives/use-resize-observer.ts +0 -45
  531. package/src/primitives/use-sequencer.ts +0 -72
  532. package/src/primitives/use-speech-recognition.ts +0 -146
  533. package/src/primitives/use-stick-to-bottom.ts +0 -75
  534. package/src/primitives/use-text-stream.ts +0 -143
  535. package/src/primitives/use-voice-recorder.ts +0 -62
  536. package/src/primitives/visualizer-sequences.ts +0 -350
  537. package/src/remote/host-embed.ts +0 -345
  538. package/src/remote/index.ts +0 -2
  539. package/src/remote/origin.ts +0 -30
  540. package/src/remote/provider-runtime.ts +0 -262
  541. package/src/remote/provider.ts +0 -2
  542. package/src/remote/validate.ts +0 -22
  543. package/src/remote/version.ts +0 -12
  544. package/src/remote/wire.ts +0 -48
  545. package/src/schemas/from-tool-call.ts +0 -151
  546. package/src/schemas/index.ts +0 -341
  547. package/src/schemas/provider-subsets.ts +0 -538
  548. package/src/schemas/registry.ts +0 -355
  549. package/src/schemas/tool-defs.ts +0 -793
  550. package/src/solid.ts +0 -173
  551. package/src/state/index.ts +0 -44
  552. package/src/state/messages.ts +0 -53
  553. package/src/state/mock.ts +0 -297
  554. package/src/state/parts.ts +0 -295
  555. package/src/state/persistence.ts +0 -180
  556. package/src/state/stream.ts +0 -246
  557. package/src/state/suggestions.ts +0 -9
  558. package/src/state/threads.ts +0 -142
  559. package/src/test-utils/fake-clock.ts +0 -88
  560. package/src/types.ts +0 -95
  561. package/src/ui/action-icons.ts +0 -53
  562. package/src/ui/agent-card.tsx +0 -218
  563. package/src/ui/avatar.tsx +0 -23
  564. package/src/ui/badge.tsx +0 -21
  565. package/src/ui/button.tsx +0 -49
  566. package/src/ui/card.tsx +0 -245
  567. package/src/ui/checkbox-group.tsx +0 -153
  568. package/src/ui/checkbox.tsx +0 -63
  569. package/src/ui/collapsible.tsx +0 -163
  570. package/src/ui/command.tsx +0 -103
  571. package/src/ui/dialog.tsx +0 -223
  572. package/src/ui/dock.tsx +0 -630
  573. package/src/ui/dropdown.tsx +0 -636
  574. package/src/ui/editable-label.tsx +0 -112
  575. package/src/ui/hover-card.tsx +0 -345
  576. package/src/ui/icon.tsx +0 -167
  577. package/src/ui/input.tsx +0 -391
  578. package/src/ui/kbd.tsx +0 -118
  579. package/src/ui/nav.tsx +0 -282
  580. package/src/ui/notice.tsx +0 -84
  581. package/src/ui/overlay.tsx +0 -277
  582. package/src/ui/pane-grid.tsx +0 -117
  583. package/src/ui/pane-group.tsx +0 -291
  584. package/src/ui/pane.tsx +0 -193
  585. package/src/ui/popover.tsx +0 -106
  586. package/src/ui/progress-bar.tsx +0 -83
  587. package/src/ui/prompt-dock.tsx +0 -145
  588. package/src/ui/radio.tsx +0 -150
  589. package/src/ui/resizable.tsx +0 -693
  590. package/src/ui/scroll-area.tsx +0 -26
  591. package/src/ui/segmented.tsx +0 -109
  592. package/src/ui/select.tsx +0 -168
  593. package/src/ui/separator.tsx +0 -10
  594. package/src/ui/settings-group.tsx +0 -67
  595. package/src/ui/skeleton.tsx +0 -73
  596. package/src/ui/slider.tsx +0 -178
  597. package/src/ui/status.tsx +0 -48
  598. package/src/ui/switch.tsx +0 -119
  599. package/src/ui/tabs.tsx +0 -168
  600. package/src/ui/textarea.tsx +0 -21
  601. package/src/ui/tooltip.tsx +0 -118
  602. package/src/utils/cn.ts +0 -30
  603. package/src/wire/chunk.ts +0 -248
  604. package/src/wire/consume.ts +0 -599
  605. package/src/wire/diagnostics.ts +0 -727
  606. package/src/wire/encode-probe.ts +0 -214
  607. package/src/wire/encode.ts +0 -947
  608. package/src/wire/files.ts +0 -342
  609. package/src/wire/formats/anthropic.ts +0 -274
  610. package/src/wire/formats/openai.ts +0 -228
  611. package/src/wire/index.ts +0 -95
  612. package/src/wire/media-types.ts +0 -344
  613. package/src/wire/read.ts +0 -396
  614. package/src/wire/sink-helpers.ts +0 -50
  615. package/src/wire/sse.ts +0 -153
@@ -1,1296 +0,0 @@
1
- import {
2
- type JSX,
3
- For,
4
- Show,
5
- Index,
6
- splitProps,
7
- mergeProps,
8
- createMemo,
9
- createEffect,
10
- on,
11
- onMount,
12
- ErrorBoundary,
13
- } from 'solid-js';
14
- import { createStore, produce, unwrap } from 'solid-js/store';
15
- import { cn } from '../utils/cn';
16
- import { Button } from '../ui/button';
17
- import { Card } from './card';
18
- import { DismissedStub } from './dismissed-stub';
19
- import {
20
- validateAgainstSchema,
21
- type JsonSchema,
22
- } from '../primitives/card-validate';
23
- import type { CardEnvelope, CardEvent, CardHost, CardResolution } from '../primitives/card-contract';
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';
27
- import { useCardHost } from '../primitives/card-host';
28
- import { useCardResolution } from './use-card-resolution';
29
- import { Check } from 'lucide-solid';
30
- import {
31
- TextWidget,
32
- TextareaWidget,
33
- NumberWidget,
34
- SliderWidget,
35
- RatingWidget,
36
- SwitchWidget,
37
- CheckboxWidget,
38
- RadioGroupWidget,
39
- SelectWidget,
40
- CheckboxGroupWidget,
41
- MultiSelectWidget,
42
- TagListWidget,
43
- } from './form-widgets';
44
-
45
- // ─────────────────────────────────────────────────────────────────────────────
46
- // Types (the JSON-Schema subset kai-form renders) — see form.schema.json.
47
- // ─────────────────────────────────────────────────────────────────────────────
48
-
49
- // AUTHORED IN ../primitives/card-data-types.ts. See the note in confirm-card.tsx,
50
- // or that file's header, for why they left a `.tsx` and why they are `type`
51
- // aliases. `FormField` in particular CANNOT be an interface: it is
52
- // self-referential, so the element-types generator can only inline it down to a
53
- // `Record<string, unknown>` placeholder, and an interface is not assignable to
54
- // that. Re-exported here unchanged.
55
-
56
- import type { FormDefinition, FormField } from '../primitives/card-data-types';
57
-
58
- export type {
59
- FormCardEnvelope,
60
- FormDefinition,
61
- FormField,
62
- } from '../primitives/card-data-types';
63
-
64
- /** The internal widget identifiers `widgetFor` resolves to. */
65
- export type WidgetKind =
66
- | 'text'
67
- | 'textarea'
68
- | 'password'
69
- | 'email'
70
- | 'url'
71
- | 'date'
72
- | 'datetime'
73
- | 'time'
74
- | 'number'
75
- | 'slider'
76
- | 'rating'
77
- | 'switch'
78
- | 'checkbox'
79
- | 'radio'
80
- | 'select'
81
- | 'checkbox-group'
82
- | 'multiselect'
83
- | 'repeater'
84
- | 'taglist'
85
- | 'fieldset'
86
- | 'unsupported';
87
-
88
- export const DEFAULT_INLINE_MAX = 4;
89
-
90
- const VALID_HINTS = new Set([
91
- 'textarea',
92
- 'slider',
93
- 'rating',
94
- 'radio',
95
- 'select',
96
- 'checkbox',
97
- 'password',
98
- 'switch',
99
- ]);
100
-
101
- // ─────────────────────────────────────────────────────────────────────────────
102
- // Pure mapping / validation / coercion helpers (unit-tested in isolation).
103
- // ─────────────────────────────────────────────────────────────────────────────
104
-
105
- /** Resolve the widget for a field. An explicit valid `x-kai-widget` always wins;
106
- * otherwise the type/format/enum/constraint combination selects the widget. */
107
- export function widgetFor(field: FormField, inlineMax: number): WidgetKind {
108
- const hint = field['x-kai-widget'];
109
- if (hint && VALID_HINTS.has(hint)) {
110
- switch (hint) {
111
- case 'textarea':
112
- return 'textarea';
113
- case 'slider':
114
- return 'slider';
115
- case 'rating':
116
- return 'rating';
117
- case 'radio':
118
- return 'radio';
119
- case 'select':
120
- return 'select';
121
- case 'checkbox':
122
- return 'checkbox';
123
- case 'password':
124
- return 'password';
125
- case 'switch':
126
- return 'switch';
127
- }
128
- }
129
-
130
- switch (field.type) {
131
- case 'string': {
132
- if (Array.isArray(field.enum)) {
133
- return field.enum.length <= inlineMax ? 'radio' : 'select';
134
- }
135
- switch (field.format) {
136
- case 'email':
137
- return 'email';
138
- case 'uri':
139
- case 'url':
140
- return 'url';
141
- case 'date':
142
- return 'date';
143
- case 'date-time':
144
- return 'datetime';
145
- case 'time':
146
- return 'time';
147
- }
148
- if (field.maxLength !== undefined && field.maxLength > 120) return 'textarea';
149
- return 'text';
150
- }
151
- case 'number':
152
- case 'integer':
153
- return 'number';
154
- case 'boolean':
155
- return 'switch';
156
- case 'array': {
157
- const items = field.items;
158
- if (items && 'enum' in items && Array.isArray(items.enum)) {
159
- return items.enum.length <= inlineMax ? 'checkbox-group' : 'multiselect';
160
- }
161
- if (items && 'type' in items && (items as FormField).type === 'object') return 'repeater';
162
- if (items && 'type' in items && (items as FormField).type === 'string') return 'taglist';
163
- return 'taglist';
164
- }
165
- case 'object':
166
- return 'fieldset';
167
- default:
168
- return 'unsupported';
169
- }
170
- }
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
-
399
- /** Humanize a camelCase / snake_case property key into a label. */
400
- export function humanize(key: string): string {
401
- const spaced = key
402
- .replace(/[_-]+/g, ' ')
403
- .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
404
- .trim();
405
- return spaced.replace(/\b\w/g, (c) => c.toUpperCase());
406
- }
407
-
408
- /** Field order: `x-kai-order` (filtered to known keys, missing appended) if
409
- * present, else `required` first then schema declaration order. */
410
- export function orderedKeys(def: FormDefinition): string[] {
411
- const all = Object.keys(def.properties ?? {});
412
- const order = def['x-kai-order'];
413
- if (Array.isArray(order)) {
414
- const known = order.filter((k) => all.includes(k));
415
- const rest = all.filter((k) => !known.includes(k));
416
- return [...known, ...rest];
417
- }
418
- const required = (def.required ?? []).filter((k) => all.includes(k));
419
- const rest = all.filter((k) => !required.includes(k));
420
- return [...required, ...rest];
421
- }
422
-
423
- /** Coerce a raw control value to the field's JSON type. Empty number string →
424
- * undefined; number/integer → Number; boolean → real boolean. */
425
- export function coerceValue(field: FormField, raw: unknown): unknown {
426
- if (field.type === 'number' || field.type === 'integer') {
427
- if (raw === '' || raw === null || raw === undefined) return undefined;
428
- const n = typeof raw === 'number' ? raw : Number(raw);
429
- return Number.isNaN(n) ? raw : n;
430
- }
431
- if (field.type === 'boolean') return Boolean(raw);
432
- return raw;
433
- }
434
-
435
- const EMAIL_RE = '^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$';
436
-
437
- /** Translate a FormField into the lean-validator JsonSchema (incl. format→pattern). */
438
- function toJsonSchema(field: FormField): JsonSchema {
439
- const s: JsonSchema = { type: field.type };
440
- if (field.enum) s.enum = field.enum;
441
- if (field.minimum !== undefined) s.minimum = field.minimum;
442
- if (field.maximum !== undefined) s.maximum = field.maximum;
443
- if (field.minLength !== undefined) s.minLength = field.minLength;
444
- if (field.maxLength !== undefined) s.maxLength = field.maxLength;
445
- if (field.minItems !== undefined) s.minItems = field.minItems;
446
- if (field.maxItems !== undefined) s.maxItems = field.maxItems;
447
- if (field.pattern !== undefined) s.pattern = field.pattern;
448
- else if (field.format === 'email') s.pattern = EMAIL_RE;
449
- return s;
450
- }
451
-
452
- export interface FormValidation {
453
- valid: boolean;
454
- fieldErrors: Record<string, string>;
455
- }
456
-
457
- function isEmpty(v: unknown): boolean {
458
- return v === undefined || v === null || v === '' || (Array.isArray(v) && v.length === 0);
459
- }
460
-
461
- /** Full client-side validation of `values` against the form definition. Returns a
462
- * per-field error map (the contract validator subset, applied field-by-field so
463
- * each field can show its own inline message). */
464
- export function validateForm(def: FormDefinition, values: Record<string, unknown>): FormValidation {
465
- const fieldErrors: Record<string, string> = {};
466
- const required = new Set(def.required ?? []);
467
-
468
- for (const [key, field] of Object.entries(def.properties ?? {})) {
469
- const v = values[key];
470
- if (required.has(key) && isEmpty(v)) {
471
- fieldErrors[key] = `${field.title ?? humanize(key)} is required.`;
472
- continue;
473
- }
474
- if (isEmpty(v)) continue; // optional + empty → skip per-field checks
475
- const result = validateAgainstSchema(toJsonSchema(field), v);
476
- if (!result.valid) {
477
- fieldErrors[key] = friendlyError(field, key, result.errors[0]);
478
- }
479
- }
480
-
481
- return { valid: Object.keys(fieldErrors).length === 0, fieldErrors };
482
- }
483
-
484
- function friendlyError(field: FormField, key: string, raw?: string): string {
485
- const label = field.title ?? humanize(key);
486
- if (!raw) return `${label} is invalid.`;
487
- if (raw.includes('minimum')) return `${label} must be at least ${field.minimum}.`;
488
- if (raw.includes('maximum')) return `${label} must be at most ${field.maximum}.`;
489
- if (raw.includes('minLength')) return `${label} must be at least ${field.minLength} characters.`;
490
- if (raw.includes('maxLength')) return `${label} must be at most ${field.maxLength} characters.`;
491
- if (raw.includes('pattern')) {
492
- return field.format === 'email'
493
- ? `${label} must be a valid email address.`
494
- : `${label} is not in the expected format.`;
495
- }
496
- if (raw.includes('one of')) return `${label} must be one of the allowed options.`;
497
- if (raw.includes('expected integer')) return `${label} must be a whole number.`;
498
- if (raw.includes('expected')) return `${label} is invalid.`;
499
- if (raw.includes('minItems')) return `${label}: choose at least ${field.minItems}.`;
500
- if (raw.includes('maxItems')) return `${label}: choose at most ${field.maxItems}.`;
501
- return `${label} is invalid.`;
502
- }
503
-
504
- // ─────────────────────────────────────────────────────────────────────────────
505
- // Read-only summary helpers (unit-tested in form-summary.test.ts).
506
- // ─────────────────────────────────────────────────────────────────────────────
507
-
508
- export interface FormSummaryRow { key: string; label: string; value: string; }
509
-
510
- /** Format one field's value for the read-only summary. */
511
- export function formatFieldValue(field: FormField | undefined, raw: unknown): string {
512
- if (field?.['x-kai-widget'] === 'password') {
513
- return raw == null || raw === '' ? '—' : '••••';
514
- }
515
- if (typeof raw === 'boolean') return raw ? 'Yes' : 'No';
516
- if (raw == null || raw === '') return '—';
517
- if (Array.isArray(raw)) return raw.length ? raw.map((v) => String(v)).join(', ') : '—';
518
- return String(raw);
519
- }
520
-
521
- /** Build the label→value rows for a submitted form, honoring x-kai-order. */
522
- export function summarizeForm(def: FormDefinition, data: Record<string, unknown>): FormSummaryRow[] {
523
- const props = def.properties ?? {};
524
- const ordered =
525
- Array.isArray(def['x-kai-order']) && def['x-kai-order']!.length > 0
526
- ? def['x-kai-order']!.filter((k) => k in props)
527
- : Object.keys(props);
528
- return ordered.map((key) => {
529
- const field = props[key];
530
- return { key, label: field?.title ?? key, value: formatFieldValue(field, data[key]) };
531
- });
532
- }
533
-
534
- /** Build the result object: coerced values with empty optional fields omitted.
535
- * `false` and `0` are kept (they are real values, not "empty"). */
536
- export function buildResult(
537
- def: FormDefinition,
538
- values: Record<string, unknown>,
539
- ): Record<string, unknown> {
540
- const out: Record<string, unknown> = {};
541
- for (const key of Object.keys(def.properties ?? {})) {
542
- const field = def.properties[key];
543
- const coerced = coerceValue(field, values[key]);
544
- if (coerced === undefined || coerced === '' ) continue;
545
- if (Array.isArray(coerced) && coerced.length === 0) continue;
546
- out[key] = coerced;
547
- }
548
- return out;
549
- }
550
-
551
- // ─────────────────────────────────────────────────────────────────────────────
552
- // The <Form> component.
553
- // ─────────────────────────────────────────────────────────────────────────────
554
-
555
- /** Imperative handle exposed via `controllerRef` — surfaces the form's latent
556
- * capabilities (focus the first/first-invalid control, validate, programmatic
557
- * send, reset to defaults, dismiss/reopen) so the `<kai-form>` facade can forward
558
- * them as instance methods. */
559
- export interface FormController {
560
- /** Focus the first control, or the first INVALID control after a failed validation. */
561
- focus(options?: FocusOptions): void;
562
- /** Run full validation + submit: focus the first invalid field on failure, else emit `submit`. */
563
- send(): void;
564
- /** Run client-side validation now and return per-field errors WITHOUT submitting. */
565
- validate(): { valid: boolean; errors?: Record<string, string> };
566
- /** Re-seed from each field's `default` and clear errors. */
567
- reset(): void;
568
- /** Trigger the dismiss path (emit `dismiss` + collapse to the re-openable stub). */
569
- dismiss(): void;
570
- /** Re-open a dismissed card from its stub (emit `reopen`). */
571
- reopen(): void;
572
- }
573
-
574
- export interface FormProps {
575
- /** The form definition (CardEnvelope.data). */
576
- data?: FormDefinition;
577
- /** The card id used to correlate every emitted CardEvent. */
578
- cardId?: string;
579
- /** The envelope title rendered in the card chrome. */
580
- heading?: string;
581
- /** Optional explicit CardHost (otherwise read from a CardProvider, otherwise the
582
- * bubbling `kai-card` CustomEvent off `hostElement`). */
583
- host?: CardHost;
584
- /** The custom-element host node, for the bubbling `kai-card` fallback emit. */
585
- hostElement?: HTMLElement;
586
- class?: string;
587
- /** When set, render the chromed read-only view instead of the form inputs. */
588
- resolution?: CardResolution;
589
- /** Controlled field values — when set, this wins over internal/uncontrolled state. */
590
- values?: Record<string, unknown>;
591
- /** Initial values overlaying the schema defaults (uncontrolled seed). */
592
- defaultValues?: Record<string, unknown>;
593
- /** Disable all fields + submit. */
594
- disabled?: boolean;
595
- /** Fires on input with the current coerced values + validity (distinct from submit). */
596
- onValuesChange?: (payload: { values: Record<string, unknown>; valid: boolean }) => void;
597
- /** Receive the imperative controller once mounted. */
598
- controllerRef?: (controller: FormController) => void;
599
- }
600
-
601
- const DEFAULT_FORM: FormDefinition = { type: 'object', properties: {} };
602
-
603
- /**
604
- * `Form` — renders a JSON-Schema form definition into themed, accessible widgets
605
- * inside `Card` chrome, validates input against that schema, and emits the
606
- * collected, coerced, validated object up the Card contract as `submit`.
607
- * Reads context/emits via a `CardProvider` when present, else the bubbling
608
- * `kai-card` CustomEvent.
609
- */
610
- export function Form(props: FormProps): JSX.Element {
611
- const merged = mergeProps({ cardId: 'kai-form' }, props);
612
- const [local] = splitProps(merged, [
613
- 'data', 'cardId', 'heading', 'host', 'hostElement', 'class', 'resolution',
614
- 'values', 'defaultValues', 'disabled', 'onValuesChange', 'controllerRef',
615
- ]);
616
-
617
- const ctxHost = useCardHost();
618
-
619
- const emit = (event: CardEvent): void => {
620
- const h = local.host ?? ctxHost;
621
- if (h) h.emit(event);
622
- else if (local.hostElement) emitCardEvent(local.hostElement, event);
623
- };
624
-
625
- // Validate the incoming definition against form.schema.json's shape (the lean
626
- // subset). A malformed definition → inline error + an `error` event.
627
- const envelopeValid = createMemo(() => {
628
- const d = local.data;
629
- if (!d) return { ok: false, message: 'No form definition provided.' };
630
- if (d.type !== 'object' || typeof d.properties !== 'object' || d.properties === null) {
631
- return { ok: false, message: "This form couldn't be displayed." };
632
- }
633
- return { ok: true as const, message: '' };
634
- });
635
-
636
- const def = createMemo<FormDefinition>(() => (envelopeValid().ok ? local.data ?? DEFAULT_FORM : DEFAULT_FORM));
637
- const inlineMax = () => def()['x-kai-inlineMax'] ?? DEFAULT_INLINE_MAX;
638
- const keys = createMemo(() => orderedKeys(def()));
639
-
640
- const res = useCardResolution({ prop: () => local.resolution, data: () => local.data });
641
-
642
- // The reactive values store, seeded from each field's `default`.
643
- const [values, setValues] = createStore<Record<string, unknown>>({});
644
- const [errors, setErrors] = createStore<Record<string, string>>({});
645
-
646
- // Build the seed value map: schema `default`s, overlaid by `defaultValues`, then
647
- // (when controlled) the live `values` prop. The controlled prop always wins.
648
- const seedMap = (d: FormDefinition): Record<string, unknown> => {
649
- const next: Record<string, unknown> = {};
650
- for (const [key, field] of Object.entries(d.properties ?? {})) {
651
- if (field.default !== undefined) next[key] = field.default;
652
- else if (field.type === 'array') next[key] = [];
653
- }
654
- if (local.defaultValues) Object.assign(next, local.defaultValues);
655
- if (local.values) Object.assign(next, local.values);
656
- return next;
657
- };
658
-
659
- const seed = (d: FormDefinition): void => {
660
- const next = seedMap(d);
661
- setValues(produce((s) => {
662
- for (const k of Object.keys(s)) delete s[k];
663
- Object.assign(s, next);
664
- }));
665
- setErrors(produce((s) => {
666
- for (const k of Object.keys(s)) delete s[k];
667
- }));
668
- };
669
-
670
- // Reset to defaults only (ignore the controlled `values`/`defaultValues` overlay):
671
- // re-seed from each field's schema `default` and clear errors. Used by reset().
672
- const seedDefaults = (): void => {
673
- const d = def();
674
- const next: Record<string, unknown> = {};
675
- for (const [key, field] of Object.entries(d.properties ?? {})) {
676
- if (field.default !== undefined) next[key] = field.default;
677
- else if (field.type === 'array') next[key] = [];
678
- }
679
- setValues(produce((s) => {
680
- for (const k of Object.keys(s)) delete s[k];
681
- Object.assign(s, next);
682
- }));
683
- setErrors(produce((s) => {
684
- for (const k of Object.keys(s)) delete s[k];
685
- }));
686
- };
687
-
688
- // Reseed whenever a NEW valid definition arrives.
689
- createEffect(on(() => local.data, () => { if (envelopeValid().ok) seed(def()); }));
690
-
691
- // Controlled values: when the consumer drives `values`, mirror it into the store
692
- // (the controlled prop wins over local edits). Deferred so mount's seed runs first.
693
- createEffect(on(() => local.values, (v) => {
694
- if (!v) return;
695
- setValues(produce((s) => {
696
- for (const k of Object.keys(s)) delete s[k];
697
- Object.assign(s, v);
698
- }));
699
- }, { defer: true }));
700
-
701
- // ready + error lifecycle emits.
702
- createEffect(on(envelopeValid, (state) => {
703
- if (state.ok) emit({ kind: 'ready', cardId: local.cardId });
704
- else emit({ kind: 'error', cardId: local.cardId, message: state.message });
705
- }));
706
-
707
- // Surface the resolved state for host styling. Only a `submit` resolution is the
708
- // "submitted" state; a deferred `dismissed` (or `expired`) is not.
709
- createEffect(() => {
710
- const el = local.hostElement;
711
- if (!el) return;
712
- if (res.resolution()?.kind === 'submit') el.setAttribute('data-kai-resolved', 'submitted');
713
- else el.removeAttribute('data-kai-resolved');
714
- });
715
-
716
- const setField = (key: string, raw: unknown): void => {
717
- setValues(key, raw);
718
- if (errors[key]) setErrors(key, undefined as unknown as string);
719
- // Fire the live change signal: current coerced values + validity (distinct from
720
- // the terminal submit). Validation here is read-only — it doesn't surface errors.
721
- const snapshot = unwrap(values) as Record<string, unknown>;
722
- const out = buildResult(def(), snapshot);
723
- const { valid } = validateForm(def(), snapshot);
724
- local.onValuesChange?.({ values: out, valid });
725
- };
726
-
727
- const validateField = (key: string): void => {
728
- const field = def().properties[key];
729
- if (!field) return;
730
- const single = validateForm(
731
- { type: 'object', required: def().required, properties: { [key]: field } },
732
- { [key]: values[key] },
733
- );
734
- setErrors(key, single.fieldErrors[key]);
735
- };
736
-
737
- // Resolve the root to query controls inside (the live <form> when called from the
738
- // submit event, else the host element's shadow root for a programmatic call).
739
- const queryRoot = (formEl?: HTMLElement | null): ParentNode =>
740
- formEl?.closest('form') ?? formEl ?? local.hostElement?.shadowRoot ?? document;
741
-
742
- // Focus the control for a field key (the `[data-control]` inside its `[data-field]`).
743
- const focusControl = (key: string, root: ParentNode, options?: FocusOptions): void => {
744
- root.querySelector<HTMLElement>(`[data-field="${cssEscape(key)}"] [data-control]`)?.focus(options);
745
- };
746
-
747
- // Run full validation; on failure surface the per-field errors + focus the first
748
- // invalid control and return false; on success emit `submit` + resolve. Shared by
749
- // the form's onSubmit handler and the controller's send().
750
- const runSubmit = (formEl?: HTMLElement | null): boolean => {
751
- if (res.isResolved()) return false;
752
- const snapshot = unwrap(values);
753
- const result = validateForm(def(), snapshot as Record<string, unknown>);
754
- setErrors(produce((s) => {
755
- for (const k of Object.keys(s)) delete s[k];
756
- Object.assign(s, result.fieldErrors);
757
- }));
758
- if (!result.valid) {
759
- const firstBad = keys().find((k) => result.fieldErrors[k]);
760
- if (firstBad) {
761
- const root = queryRoot(formEl);
762
- queueMicrotask(() => focusControl(firstBad, root));
763
- }
764
- return false;
765
- }
766
- const out = buildResult(def(), snapshot as Record<string, unknown>);
767
- emit({ kind: 'submit', cardId: local.cardId, data: out });
768
- res.setLocal({ kind: 'submit', data: out });
769
- return true;
770
- };
771
-
772
- const onSubmit = (e: Event): void => {
773
- e.preventDefault();
774
- // Capture the <form> synchronously — `e.currentTarget` is nulled out once the
775
- // event has finished dispatching (so it can't be read in a later microtask).
776
- runSubmit(e.currentTarget as HTMLElement | null);
777
- };
778
-
779
- // Dismiss: emit `dismiss` AND optimistically flip to a `dismissed` resolution so
780
- // the form collapses to its re-openable stub immediately.
781
- const onDismiss = (): void => {
782
- if (res.isResolved()) return;
783
- emit({ kind: 'dismiss', cardId: local.cardId });
784
- res.setLocal({ kind: 'dismissed' });
785
- };
786
- const onReopen = (): void => emit({ kind: 'reopen', cardId: local.cardId });
787
-
788
- const disabled = (): boolean => local.disabled === true;
789
-
790
- // Imperative controller (Pattern C): hand the facade a handle over the form's
791
- // latent capabilities. focus targets the first control (or the first INVALID one
792
- // after a failed validation); validate runs the existing validateForm and surfaces
793
- // the per-field errors WITHOUT submitting; send runs the same path as the Submit
794
- // button; reset re-seeds from schema defaults; dismiss/reopen drive the stub.
795
- onMount(() => {
796
- local.controllerRef?.({
797
- focus: (options) => {
798
- const root = queryRoot();
799
- const firstBad = keys().find((k) => errors[k]);
800
- const target = firstBad ?? keys()[0];
801
- if (target) focusControl(target, root, options);
802
- },
803
- send: () => { runSubmit(); },
804
- validate: () => {
805
- const snapshot = unwrap(values) as Record<string, unknown>;
806
- const result = validateForm(def(), snapshot);
807
- setErrors(produce((s) => {
808
- for (const k of Object.keys(s)) delete s[k];
809
- Object.assign(s, result.fieldErrors);
810
- }));
811
- return result.valid
812
- ? { valid: true }
813
- : { valid: false, errors: result.fieldErrors };
814
- },
815
- reset: () => seedDefaults(),
816
- dismiss: () => onDismiss(),
817
- reopen: () => onReopen(),
818
- });
819
- });
820
-
821
- const actions = createMemo(() => def()['x-kai-actions'] ?? []);
822
- const submitLabel = () => def()['x-kai-submitLabel'] ?? 'Submit';
823
- const dismissible = () => def()['x-kai-dismissible'] === true;
824
-
825
- const summaryRows = createMemo(() => {
826
- const r = res.resolution();
827
- if (!r || r.kind !== 'submit') return [];
828
- return summarizeForm(def(), (r.data ?? {}) as Record<string, unknown>);
829
- });
830
-
831
- return (
832
- <Show
833
- when={envelopeValid().ok}
834
- fallback={<Card heading={local.heading} errorMessage={envelopeValid().message} />}
835
- >
836
- <ErrorBoundary
837
- fallback={() => {
838
- emit({ kind: 'error', cardId: local.cardId, message: 'The form failed to render.' });
839
- return <Card heading={local.heading} errorMessage="The form failed to render." />;
840
- }}
841
- >
842
- <Show
843
- when={!res.isDeferred()}
844
- fallback={
845
- <DismissedStub type="form" title={local.heading ?? def().title} onReopen={onReopen} />
846
- }
847
- >
848
- <Card
849
- heading={local.heading ?? def().title}
850
- description={def().description}
851
- actions={
852
- <Show
853
- when={!res.isResolved()}
854
- fallback={undefined}
855
- >
856
- <div class="flex w-full flex-wrap items-center justify-between gap-2">
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. */}
865
- <Button
866
- type="button"
867
- variant="ghost"
868
- onClick={onDismiss}
869
- >
870
- Dismiss
871
- </Button>
872
- </Show>
873
- <div class="ml-auto flex flex-wrap items-center gap-2">
874
- <For each={actions()}>
875
- {(action) => (
876
- <Button
877
- type="button"
878
- variant={action.variant ?? 'ghost'}
879
- onClick={() => emit({ kind: 'action', cardId: local.cardId, action: action.id })}
880
- >
881
- {action.label}
882
- </Button>
883
- )}
884
- </For>
885
- <Button type="submit" form={formId()} disabled={disabled()}>
886
- {submitLabel()}
887
- </Button>
888
- </div>
889
- </div>
890
- </Show>
891
- }
892
- >
893
- <Show
894
- when={!res.isResolved()}
895
- fallback={<ResolvedForm rows={summaryRows()} optimistic={res.isOptimistic()} />}
896
- >
897
- <form
898
- id={formId()}
899
- class={cn('flex flex-col gap-3', local.class)}
900
- novalidate
901
- onSubmit={onSubmit}
902
- >
903
- <For each={keys()}>
904
- {(key) => (
905
- <FieldRow
906
- fieldKey={key}
907
- field={def().properties[key]}
908
- required={(def().required ?? []).includes(key)}
909
- inlineMax={inlineMax()}
910
- value={() => values[key]}
911
- error={() => errors[key]}
912
- disabled={disabled()}
913
- onInput={(v) => setField(key, v)}
914
- onBlur={() => validateField(key)}
915
- />
916
- )}
917
- </For>
918
- </form>
919
- </Show>
920
- </Card>
921
- </Show>
922
- </ErrorBoundary>
923
- </Show>
924
- );
925
- }
926
-
927
- // A stable per-instance form id so the footer submit button can target the form.
928
- let formIdCounter = 0;
929
- const formIdValue = `kai-form-${++formIdCounter}`;
930
- function formId(): string {
931
- return formIdValue;
932
- }
933
-
934
- // ─────────────────────────────────────────────────────────────────────────────
935
- // Read-only resolved view presenter.
936
- // ─────────────────────────────────────────────────────────────────────────────
937
-
938
- function ResolvedForm(props: { rows: FormSummaryRow[]; optimistic: boolean }): JSX.Element {
939
- return (
940
- <div class="flex flex-col gap-3" role={props.optimistic ? 'status' : undefined}>
941
- <p class="flex items-center gap-2 text-sm font-medium text-foreground">
942
- <Check size={16} aria-hidden="true" />
943
- <span>Submitted</span>
944
- </p>
945
- <dl class="grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1.5">
946
- <For each={props.rows}>
947
- {(row) => (
948
- <>
949
- <dt class="text-xs text-muted-foreground">{row.label}</dt>
950
- <dd class="m-0 text-sm font-medium text-foreground">{row.value}</dd>
951
- </>
952
- )}
953
- </For>
954
- </dl>
955
- </div>
956
- );
957
- }
958
-
959
- // ─────────────────────────────────────────────────────────────────────────────
960
- // Per-field row: label + control + help + error, dispatching to the right widget.
961
- // ─────────────────────────────────────────────────────────────────────────────
962
-
963
- interface FieldRowProps {
964
- fieldKey: string;
965
- field: FormField;
966
- required: boolean;
967
- inlineMax: number;
968
- value: () => unknown;
969
- error: () => string | undefined;
970
- disabled: boolean;
971
- onInput: (value: unknown) => void;
972
- onBlur: () => void;
973
- }
974
-
975
- function FieldRow(props: FieldRowProps): JSX.Element {
976
- const id = `f-${props.fieldKey}-${Math.random().toString(36).slice(2, 8)}`;
977
- const labelId = `${id}-label`;
978
- const errorId = `${id}-err`;
979
- const descId = `${id}-desc`;
980
- const formatId = `${id}-fmt`;
981
- const label = () => props.field.title ?? humanize(props.fieldKey);
982
- const widget = createMemo(() => widgetFor(props.field, props.inlineMax));
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
-
994
- const describedBy = () =>
995
- [
996
- props.field.description ? descId : '',
997
- maskHint().hint ? formatId : '',
998
- props.error() ? errorId : '',
999
- ]
1000
- .filter(Boolean)
1001
- .join(' ') || undefined;
1002
-
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
- );
1024
-
1025
-
1026
- return (
1027
- <div class="flex flex-col gap-2 rounded-xl bg-surface p-3.5" data-field={props.fieldKey}>
1028
- <Show when={!ownLegend()}>
1029
- <label id={labelId} for={groupOnly() ? undefined : id} class="text-sm font-medium text-foreground">
1030
- {label()}
1031
- <Show when={props.required}>
1032
- <span class="text-destructive dark:text-red-400" aria-hidden="true">{' *'}</span>
1033
- </Show>
1034
- </label>
1035
- </Show>
1036
-
1037
- <Show when={props.field.description}>
1038
- <p id={descId} class="text-xs text-muted-foreground">
1039
- {props.field.description}
1040
- </p>
1041
- </Show>
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
-
1052
- <WidgetSwitch widget={widget()} common={common} fieldKey={props.fieldKey} />
1053
-
1054
- <Show when={props.error()}>
1055
- <p id={errorId} role="alert" class="text-xs text-destructive dark:text-red-400">
1056
- {props.error()}
1057
- </p>
1058
- </Show>
1059
- </div>
1060
- );
1061
- }
1062
-
1063
- /**
1064
- * The widget prop bag for one field row. **Internal** — exported only so the
1065
- * subscription contract can be pinned directly
1066
- * (`tests/components/form-field-subscriptions.test.tsx`); it is not re-exported
1067
- * from the package.
1068
- *
1069
- * GETTERS, NOT AN OBJECT FACTORY, and this is the whole point (K-D12b). The
1070
- * factory read `props.value()` alongside everything else and rebuilt the bag on
1071
- * every call, so a reader of ANY prop — `invalid`, say — subscribed to `value`
1072
- * too and re-ran on every keystroke. Combined with the `<input>` being rebuilt
1073
- * when its class expression re-ran (K-D12a), that is what made typing into a
1074
- * `kai-form` text field lose focus after each character.
1075
- *
1076
- * One stable object with one getter per prop: each reader tracks exactly the
1077
- * signal behind the prop it read. The object identity is constant, so the
1078
- * `WidgetSwitch` prop below never changes either.
1079
- */
1080
- export function fieldCommon(
1081
- props: FieldRowProps,
1082
- id: string,
1083
- placeholder: () => string | undefined,
1084
- label: () => string,
1085
- describedBy: () => string | undefined = () => undefined,
1086
- mask: () => FieldMaskHint = () => EMPTY_MASK_HINT,
1087
- labelledBy: () => string | undefined = () => undefined,
1088
- ): ReturnType<FieldRowCommon> {
1089
- return {
1090
- id,
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(); },
1097
- get field() { return props.field; },
1098
- get disabled() { return props.disabled || props.field.readOnly === true; },
1099
- get placeholder() { return placeholder(); },
1100
- get required() { return props.required; },
1101
- get invalid() { return Boolean(props.error()); },
1102
- get describedBy() { return describedBy(); },
1103
- get label() { return label(); },
1104
- get labelledBy() { return labelledBy(); },
1105
- onInput: (v) => props.onInput(v),
1106
- onBlur: () => props.onBlur(),
1107
- };
1108
- }
1109
-
1110
- interface WidgetSwitchProps {
1111
- widget: WidgetKind;
1112
- fieldKey: string;
1113
- common: ReturnType<FieldRowCommon>;
1114
- }
1115
- type FieldRowCommon = () => {
1116
- id: string;
1117
- value: unknown;
1118
- mask: FieldMaskHint;
1119
- field: FormField;
1120
- disabled: boolean;
1121
- placeholder?: string;
1122
- required: boolean;
1123
- invalid: boolean;
1124
- describedBy?: string;
1125
- label: string;
1126
- /** The id of the row's visible <label>, for widgets that name a GROUP. */
1127
- labelledBy?: string;
1128
- onInput: (v: unknown) => void;
1129
- onBlur: () => void;
1130
- };
1131
-
1132
- /** Dispatch to the concrete widget for `widget`. */
1133
- function WidgetSwitch(props: WidgetSwitchProps): JSX.Element {
1134
- const w = () => props.widget;
1135
- const c = () => props.common;
1136
- return (
1137
- <>
1138
- <Show when={w() === 'text' || w() === 'email' || w() === 'url' || w() === 'date' || w() === 'datetime' || w() === 'time' || w() === 'password'}>
1139
- <TextWidget {...c()} variant={w() as 'text' | 'email' | 'url' | 'date' | 'datetime' | 'time' | 'password'} />
1140
- </Show>
1141
- <Show when={w() === 'textarea'}>
1142
- <TextareaWidget {...c()} />
1143
- </Show>
1144
- <Show when={w() === 'number'}>
1145
- <NumberWidget {...c()} />
1146
- </Show>
1147
- <Show when={w() === 'slider'}>
1148
- <SliderWidget {...c()} />
1149
- </Show>
1150
- <Show when={w() === 'rating'}>
1151
- <RatingWidget {...c()} />
1152
- </Show>
1153
- <Show when={w() === 'switch'}>
1154
- <SwitchWidget {...c()} />
1155
- </Show>
1156
- <Show when={w() === 'checkbox'}>
1157
- <CheckboxWidget {...c()} />
1158
- </Show>
1159
- <Show when={w() === 'radio'}>
1160
- <RadioGroupWidget {...c()} />
1161
- </Show>
1162
- <Show when={w() === 'select'}>
1163
- <SelectWidget {...c()} />
1164
- </Show>
1165
- <Show when={w() === 'checkbox-group'}>
1166
- <CheckboxGroupWidget {...c()} />
1167
- </Show>
1168
- <Show when={w() === 'multiselect'}>
1169
- <MultiSelectWidget {...c()} />
1170
- </Show>
1171
- <Show when={w() === 'taglist'}>
1172
- <TagListWidget {...c()} />
1173
- </Show>
1174
- <Show when={w() === 'repeater'}>
1175
- <RepeaterWidget {...c()} inlineMax={DEFAULT_INLINE_MAX} />
1176
- </Show>
1177
- <Show when={w() === 'fieldset'}>
1178
- <FieldsetWidget {...c()} inlineMax={DEFAULT_INLINE_MAX} />
1179
- </Show>
1180
- <Show when={w() === 'unsupported'}>
1181
- <p class="rounded-md border border-dashed border-border p-2 text-xs text-muted-foreground">
1182
- Unsupported field "{props.fieldKey}".
1183
- </p>
1184
- </Show>
1185
- </>
1186
- );
1187
- }
1188
-
1189
- // ─────────────────────────────────────────────────────────────────────────────
1190
- // Composite widgets that need the FormField recursion (fieldset + repeater).
1191
- // They live here (not form-widgets) to reuse FieldRow/Switch + the helpers.
1192
- // ─────────────────────────────────────────────────────────────────────────────
1193
-
1194
- interface CompositeProps {
1195
- id: string;
1196
- value: unknown;
1197
- field: FormField;
1198
- disabled: boolean;
1199
- required: boolean;
1200
- invalid: boolean;
1201
- describedBy?: string;
1202
- label: string;
1203
- inlineMax: number;
1204
- onInput: (v: unknown) => void;
1205
- onBlur: () => void;
1206
- }
1207
-
1208
- function FieldsetWidget(props: CompositeProps): JSX.Element {
1209
- const subProps = () => props.field.properties ?? {};
1210
- const obj = () => (props.value && typeof props.value === 'object' ? (props.value as Record<string, unknown>) : {});
1211
- const setKey = (k: string, v: unknown): void => {
1212
- props.onInput({ ...obj(), [k]: v });
1213
- };
1214
- return (
1215
- <fieldset class="flex flex-col gap-3 rounded-lg border border-border p-3">
1216
- <legend class="px-1 text-sm font-medium text-foreground">{props.label}</legend>
1217
- <For each={Object.keys(subProps())}>
1218
- {(k) => (
1219
- <FieldRow
1220
- fieldKey={k}
1221
- field={subProps()[k]}
1222
- required={(props.field.required ?? []).includes(k)}
1223
- inlineMax={props.inlineMax}
1224
- value={() => obj()[k]}
1225
- error={() => undefined}
1226
- disabled={props.disabled}
1227
- onInput={(v) => setKey(k, v)}
1228
- onBlur={props.onBlur}
1229
- />
1230
- )}
1231
- </For>
1232
- </fieldset>
1233
- );
1234
- }
1235
-
1236
- function RepeaterWidget(props: CompositeProps): JSX.Element {
1237
- const itemSchema = () => (props.field.items as FormField) ?? { type: 'object', properties: {} };
1238
- const rows = () => (Array.isArray(props.value) ? (props.value as unknown[]) : []);
1239
- const setRows = (next: unknown[]): void => props.onInput(next);
1240
- const addRow = (): void => setRows([...rows(), {}]);
1241
- const removeRow = (i: number): void => setRows(rows().filter((_, idx) => idx !== i));
1242
- const setRowKey = (i: number, k: string, v: unknown): void => {
1243
- const next = rows().slice();
1244
- next[i] = { ...(next[i] as Record<string, unknown>), [k]: v };
1245
- setRows(next);
1246
- };
1247
-
1248
- return (
1249
- <fieldset class="flex flex-col gap-3 rounded-lg border border-border p-3" data-control>
1250
- <legend class="px-1 text-sm font-medium text-foreground">{props.label}</legend>
1251
- <Index each={rows()}>
1252
- {(row, i) => (
1253
- <div class="flex flex-col gap-2 rounded-md border border-border/60 p-2">
1254
- <div class="flex items-center justify-between">
1255
- <span class="text-xs text-muted-foreground">Item {i + 1}</span>
1256
- <Button
1257
- type="button"
1258
- size="icon-sm"
1259
- variant="ghost"
1260
- aria-label={`Remove row ${i + 1}`}
1261
- disabled={props.disabled}
1262
- onClick={() => removeRow(i)}
1263
- >
1264
-
1265
- </Button>
1266
- </div>
1267
- <For each={Object.keys(itemSchema().properties ?? {})}>
1268
- {(k) => (
1269
- <FieldRow
1270
- fieldKey={k}
1271
- field={itemSchema().properties![k]}
1272
- required={(itemSchema().required ?? []).includes(k)}
1273
- inlineMax={props.inlineMax}
1274
- value={() => (row() as Record<string, unknown>)?.[k]}
1275
- error={() => undefined}
1276
- disabled={props.disabled}
1277
- onInput={(v) => setRowKey(i, k, v)}
1278
- onBlur={props.onBlur}
1279
- />
1280
- )}
1281
- </For>
1282
- </div>
1283
- )}
1284
- </Index>
1285
- <Button type="button" variant="outline" size="sm" disabled={props.disabled} onClick={addRow}>
1286
- Add item
1287
- </Button>
1288
- </fieldset>
1289
- );
1290
- }
1291
-
1292
- /** Minimal CSS.escape fallback for attribute-selector building. */
1293
- function cssEscape(s: string): string {
1294
- if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') return CSS.escape(s);
1295
- return s.replace(/["\\]/g, '\\$&');
1296
- }