@kitn.ai/ui 0.27.0 → 0.29.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 (612) hide show
  1. package/README.md +10 -2
  2. package/dist/{Icon-BnFKqqtY.js → Icon-DObhsRtm.js} +1 -1
  3. package/dist/{action-icons-CBskfBMz.js → action-icons-CcgNN2oT.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-CjRU3JgV.js +1 -0
  7. package/dist/arrow-left-Bnpb1UEW.js +1 -0
  8. package/dist/{artifact-CvXEm1lS.js → artifact-Bwt7OgcR.js} +1 -1
  9. package/dist/{attachments-C43ZegEq.js → attachments-Qr4kg-2b.js} +1 -1
  10. package/dist/{audio-visualizer-B-79y1Tj.js → audio-visualizer-DToYdfSc.js} +1 -1
  11. package/dist/{badge-BpUbq8c0.js → badge-CdEu_jn2.js} +1 -1
  12. package/dist/{bell-sn17Ip9v.js → bell-DQAAOpDz.js} +1 -1
  13. package/dist/{button-BH6rN0U-.js → button-DkuOnGLy.js} +1 -1
  14. package/dist/{card-renderer-Cb1gQmth.js → card-renderer-Jx70m6_l.js} +1 -1
  15. package/dist/{card-routing-DifQgS7n.js → card-routing-0YidER_3.js} +1 -1
  16. package/dist/check-njkVdZu2.js +1 -0
  17. package/dist/{checkbox-Dc9Yov_B.js → checkbox-D7iRB3nL.js} +1 -1
  18. package/dist/{checkbox-group-B1kDLLYk.js → checkbox-group-B2_rpoPM.js} +1 -1
  19. package/dist/chevron-down-B72s6PXR.js +1 -0
  20. package/dist/chevron-right-BatuHzUm.js +1 -0
  21. package/dist/{choice-card-BT3c1S8g.js → choice-card-m0BYqqOK.js} +1 -1
  22. package/dist/circle-check-B62pODB3.js +1 -0
  23. package/dist/{circle-x-wobWkoSx.js → circle-x-BPG0iPst.js} +1 -1
  24. package/dist/circle-zx0ByzY9.js +1 -0
  25. package/dist/code-block-ClMLKb7C.js +1 -0
  26. package/dist/{collapsible-CPJbf6FM.js → collapsible-B4DlWicL.js} +1 -1
  27. package/dist/components/chat-thread.d.ts +89 -1
  28. package/dist/components/conversation-item.d.ts +16 -0
  29. package/dist/components/conversation-panel.d.ts +29 -0
  30. package/dist/components/home-panel.d.ts +34 -0
  31. package/dist/components/widget-tab-bar.d.ts +16 -0
  32. package/dist/{composer-CjngLzal.js → composer-sQ0HcvyG.js} +3 -3
  33. package/dist/{confirm-card-DCx_A4bU.js → confirm-card-rQWDQsnC.js} +1 -1
  34. package/dist/construct-cli.es.js +185 -17
  35. package/dist/construct.d.ts +5 -0
  36. package/dist/construct.js +327 -0
  37. package/dist/{context-BY8V-Nlp.js → context-0kg4ZX1J.js} +1 -1
  38. package/dist/controllable-CnGpMF6r.js +1 -0
  39. package/dist/conversation-item-BxwQLaFJ.js +1 -0
  40. package/dist/conversation-list-BCfGN1JU.js +1 -0
  41. package/dist/{copy-B6DO4PqB.js → copy-n9u9rqpi.js} +1 -1
  42. package/dist/core-C8fzo39E.js +1 -12
  43. package/dist/{create-tween-CnMHviKB.js → create-tween-C-q-JCM-.js} +1 -1
  44. package/dist/{create-tween-D1aiGZZh.js → create-tween-CRrJXxBd.js} +1 -1
  45. package/dist/{create-tween-BPzaefTp.js → create-tween-DZ3RRDHD.js} +1 -1
  46. package/dist/{create-tween-DUSQUKpr.js → create-tween-uPkArjIG.js} +1 -1
  47. package/dist/custom-elements.json +51 -2
  48. package/dist/{default-input-CucQEOqH.js → default-input-CaMrJkgH.js} +1 -1
  49. package/dist/define-By-uL588.js +1 -0
  50. package/dist/define.js +2 -2
  51. package/dist/define.server.js +2 -2
  52. package/dist/{disclosure-CbLtajUQ.js → disclosure-WUImBbDv.js} +1 -1
  53. package/dist/{dropdown-BwhViJFd.js → dropdown-wZVeJOMd.js} +1 -1
  54. package/dist/elements/agent-card.js +1 -1
  55. package/dist/elements/artifact.js +1 -1
  56. package/dist/elements/attachments.js +1 -1
  57. package/dist/elements/audio-visualizer.js +1 -1
  58. package/dist/elements/avatar.js +1 -1
  59. package/dist/elements/badge.js +1 -1
  60. package/dist/elements/button.js +1 -1
  61. package/dist/elements/card.js +1 -1
  62. package/dist/elements/cards.js +1 -1
  63. package/dist/elements/chain-of-thought.js +1 -1
  64. package/dist/elements/chat-scope-picker.js +1 -1
  65. package/dist/elements/chat-workspace.js +1 -1
  66. package/dist/elements/chat.js +1 -1
  67. package/dist/elements/checkbox-group.js +1 -1
  68. package/dist/elements/checkbox.js +1 -1
  69. package/dist/elements/checkpoint.js +1 -1
  70. package/dist/elements/choice.js +1 -1
  71. package/dist/elements/coachmark.js +1 -1
  72. package/dist/elements/code-block.js +1 -1
  73. package/dist/elements/command.js +1 -1
  74. package/dist/elements/compare.js +1 -1
  75. package/dist/elements/composer.js +1 -1
  76. package/dist/elements/confirm-card.js +1 -1
  77. package/dist/elements/context-meter.js +1 -1
  78. package/dist/elements/conversation-item.js +1 -1
  79. package/dist/elements/conversation-list.js +1 -1
  80. package/dist/elements/dialog.js +1 -1
  81. package/dist/elements/dock.js +3 -3
  82. package/dist/elements/dropdown.js +1 -1
  83. package/dist/elements/editable-label.js +1 -1
  84. package/dist/elements/embed.js +1 -1
  85. package/dist/elements/empty.js +1 -1
  86. package/dist/elements/feedback-bar.js +1 -1
  87. package/dist/elements/file-tree.js +1 -1
  88. package/dist/elements/file-upload.js +1 -1
  89. package/dist/elements/form.js +1 -1
  90. package/dist/elements/hover-card.js +1 -1
  91. package/dist/elements/icon.js +1 -1
  92. package/dist/elements/image.js +1 -1
  93. package/dist/elements/input.js +1 -1
  94. package/dist/elements/kbd.js +1 -1
  95. package/dist/elements/link-preview.js +1 -1
  96. package/dist/elements/loader.js +1 -1
  97. package/dist/elements/markdown.js +1 -1
  98. package/dist/elements/menu.js +1 -1
  99. package/dist/elements/message-skills.js +1 -1
  100. package/dist/elements/message.js +1 -1
  101. package/dist/elements/model-switcher.js +1 -1
  102. package/dist/elements/nav.js +1 -1
  103. package/dist/elements/notice.js +1 -1
  104. package/dist/elements/pane-grid.js +1 -1
  105. package/dist/elements/pane-group.js +1 -1
  106. package/dist/elements/pane.js +1 -1
  107. package/dist/elements/popover.js +1 -1
  108. package/dist/elements/progress-bar.js +1 -1
  109. package/dist/elements/prompt-dock.js +1 -1
  110. package/dist/elements/prompt-input.js +1 -1
  111. package/dist/elements/prompt-suggestions.js +1 -1
  112. package/dist/elements/radio-group.js +1 -1
  113. package/dist/elements/reasoning.js +1 -1
  114. package/dist/elements/remote.js +1 -1
  115. package/dist/elements/resizable.js +1 -1
  116. package/dist/elements/response-stream.js +1 -1
  117. package/dist/elements/screen.js +1 -1
  118. package/dist/elements/scroll-area.js +1 -1
  119. package/dist/elements/scroll-button.js +1 -1
  120. package/dist/elements/search.js +1 -1
  121. package/dist/elements/segmented.js +1 -1
  122. package/dist/elements/select.js +1 -1
  123. package/dist/elements/separator.js +1 -1
  124. package/dist/elements/setting-item.js +1 -1
  125. package/dist/elements/settings-group.js +1 -1
  126. package/dist/elements/skeleton.js +1 -1
  127. package/dist/elements/slider.js +1 -1
  128. package/dist/elements/source.js +1 -1
  129. package/dist/elements/status.js +1 -1
  130. package/dist/elements/switch.js +1 -1
  131. package/dist/elements/tabs.js +1 -1
  132. package/dist/elements/tasks.js +1 -1
  133. package/dist/elements/text-shimmer.js +1 -1
  134. package/dist/elements/thinking-bar.js +1 -1
  135. package/dist/elements/thread.js +1 -1
  136. package/dist/elements/toast.js +1 -1
  137. package/dist/elements/tool.js +1 -1
  138. package/dist/elements/tooltip.js +1 -1
  139. package/dist/elements/voice-input.js +1 -1
  140. package/dist/elements/voice-output.js +1 -1
  141. package/dist/elements.d.ts +20 -4
  142. package/dist/{ellipsis-DFr4Vqr3.js → ellipsis-CbOpJnU3.js} +1 -1
  143. package/dist/{embed-5s_vU4J0.js → embed-CrfjkbUG.js} +1 -1
  144. package/dist/engine-javascript-C1x7zo1_.js +1 -141
  145. package/dist/{external-link-u9d85ORQ.js → external-link-ByXAwBBQ.js} +1 -1
  146. package/dist/{file-text-CX7_x42o.js → file-text-DMib2qwt.js} +1 -1
  147. package/dist/{file-tree-D3kBjXI5.js → file-tree-CJqM2aP7.js} +1 -1
  148. package/dist/{folder-V-YdIL8f.js → folder-_BAb929C.js} +1 -1
  149. package/dist/{form-BRNUMCN2.js → form-DSZgZQZk.js} +1 -1
  150. package/dist/{hover-card-DaeiYrTA.js → hover-card-BQbul9eq.js} +1 -1
  151. package/dist/{icon-BpixdZNq.js → icon-CdcfRhYI.js} +1 -1
  152. package/dist/index.d.ts +3 -1
  153. package/dist/index.js +2842 -2703
  154. package/dist/index.server.js +2820 -2681
  155. package/dist/{info-8mFsIjS3.js → info-DsFjGuI_.js} +1 -1
  156. package/dist/{input-d1Yuu-hz.js → input-DKbmkMos.js} +1 -1
  157. package/dist/kai.es.js +1 -1
  158. package/dist/{kbd-961zMPFB.js → kbd-PvdGhX4s.js} +1 -1
  159. package/dist/{link-preview-DIIbV2Sd.js → link-preview-DbM1866N.js} +1 -1
  160. package/dist/{link-DlZZBGaR.js → link-zF8PzWfF.js} +1 -1
  161. package/dist/{loader-YQszHFaW.js → loader-BM4CEz4G.js} +1 -1
  162. package/dist/{markdown-CVk1MPsj.js → markdown-LMvmAyBk.js} +7 -7
  163. package/dist/mcp.es.js +79 -5
  164. package/dist/{message-CwKVEdZg.js → message-BSsgvHvf.js} +1 -1
  165. package/dist/{message-DS8lzHc1.js → message-BYZECyek.js} +1 -1
  166. package/dist/message-circle-DruwObfX.js +1 -0
  167. package/dist/message-square-BG6q6mlm.js +1 -0
  168. package/dist/{minimize-2-UEZmP2QT.js → minimize-2-BQRoBV1V.js} +1 -1
  169. package/dist/{model-switcher-Kk1jEQ5t.js → model-switcher-CMpCOYcB.js} +1 -1
  170. package/dist/{overlay-C1od6LOz.js → overlay-Cb-MkyDa.js} +1 -1
  171. package/dist/{panel-right-ryET0yyn.js → panel-right-i8warFxT.js} +1 -1
  172. package/dist/{paperclip-BDNoZFJP.js → paperclip-ByY3UVO6.js} +1 -1
  173. package/dist/play-BvKKq3fX.js +1 -0
  174. package/dist/primitives/conversation-store.d.ts +54 -0
  175. package/dist/{progress-bar-BvYX0aoy.js → progress-bar-jVAHvMUf.js} +1 -1
  176. package/dist/{prompt-suggestion-BRFu2lcu.js → prompt-suggestion-CD-qtNgC.js} +1 -1
  177. package/dist/{radio-DcL23cNJ.js → radio-BsoBMhkF.js} +1 -1
  178. package/dist/react/index.d.ts +393 -0
  179. package/dist/react.js +20 -20
  180. package/dist/{reasoning-Cackq4Rh.js → reasoning-4kBPtlTx.js} +1 -1
  181. package/dist/register-impl-B1hLkrI8.js +424 -0
  182. package/dist/{resizable-Cp9HUYih.js → resizable-uPmY9VY-.js} +1 -1
  183. package/dist/{rotate-ccw-0DLRDHH5.js → rotate-ccw-B2xn93py.js} +1 -1
  184. package/dist/{rotate-cw-DOB9IGcU.js → rotate-cw-CMVLoCyH.js} +1 -1
  185. package/dist/{scroll-area-emVmWP4K.js → scroll-area-BqLUo8ZZ.js} +1 -1
  186. package/dist/{scroll-button-CeP_gv1x.js → scroll-button-DslXiJyL.js} +1 -1
  187. package/dist/{select-CWmu00tS.js → select-2cLRtPGT.js} +1 -1
  188. package/dist/{separator-CE6vly6z.js → separator-BWQtNJcM.js} +1 -1
  189. package/dist/{settings-CE-78SvZ.js → settings-BBR63jLg.js} +1 -1
  190. package/dist/{settings-group-ZFWNxFQR.js → settings-group-B2ciy-Iw.js} +1 -1
  191. package/dist/{skeleton-BebdP4mf.js → skeleton-CkHI-HgC.js} +1 -1
  192. package/dist/{slider-BAKRYUlz.js → slider-DDR9U-i0.js} +1 -1
  193. package/dist/{solid-BE5ui6sy.js → solid-BzCFdL25.js} +7720 -7058
  194. package/dist/{solid-DFhsce3O.js → solid-DruVOXnf.js} +5864 -5283
  195. package/dist/solid.d.ts +6 -2
  196. package/dist/solid.js +217 -210
  197. package/dist/solid.server.js +217 -210
  198. package/dist/{source-B0FlguSx.js → source-GEnFF528.js} +1 -1
  199. package/dist/{star-CBMZvLuN.js → star-CilHq8GP.js} +1 -1
  200. package/dist/store-Ciyk6eZG.js +1 -0
  201. package/dist/{switch-CViARZCy.js → switch-KkeQIbuH.js} +1 -1
  202. package/dist/{tasks-card-MjCcOhR_.js → tasks-card-BrN4bggJ.js} +1 -1
  203. package/dist/{text-shimmer-DKlwRF2G.js → text-shimmer-Dcj1MHuq.js} +1 -1
  204. package/dist/{textarea-3ay2Me2-.js → textarea-Dpvbngbg.js} +1 -1
  205. package/dist/theme.tokens.css +28 -0
  206. package/dist/{thumbs-up-DQcu2HMO.js → thumbs-up-CsT9Wzb9.js} +1 -1
  207. package/dist/{toast-store-C6yKOhbk.js → toast-store-tgK6MuMv.js} +1 -1
  208. package/dist/{tool-BCYNlfKy.js → tool-DzUjIBT8.js} +1 -1
  209. package/dist/{tooltip-05zI4hjs.js → tooltip-Bkkg_4t0.js} +1 -1
  210. package/dist/{trash-2-BR1RxGky.js → trash-2-jBKxjGGQ.js} +1 -1
  211. package/dist/{triangle-alert-DHohDOCy.js → triangle-alert-DlQ68ppf.js} +1 -1
  212. package/dist/types.d.ts +48 -3
  213. package/dist/ui/dock.d.ts +29 -0
  214. package/dist/{upload-BbGW6A_u.js → upload-TcAHXv9W.js} +1 -1
  215. package/dist/{url-scheme-policy-DHpoTJwB.js → url-scheme-policy-DR5DgvkM.js} +1 -1
  216. package/dist/{use-card-resolution-CJW4grFa.js → use-card-resolution-DaZqAdWf.js} +1 -1
  217. package/dist/{variant-aurora-xiMPty8f.js → variant-aurora-BhLuYxJR.js} +1 -1
  218. package/dist/{variant-aurora-BUOVgPOB.js → variant-aurora-CNmfHhcx.js} +2 -2
  219. package/dist/{variant-aurora-CUsUJvFI.js → variant-aurora-D6GsdTPA.js} +2 -2
  220. package/dist/{variant-aurora-CW2i4bfr.js → variant-aurora-DbW_OWL2.js} +1 -1
  221. package/dist/{variant-custom-D91HjAKz.js → variant-custom-BRvaNHSi.js} +1 -1
  222. package/dist/{variant-custom-BQtOnmeO.js → variant-custom-BvYHR62b.js} +1 -1
  223. package/dist/{variant-custom-CAnzzppW.js → variant-custom-DMTrEO5w.js} +2 -2
  224. package/dist/{variant-custom-CAm9L-E5.js → variant-custom-DV2x254s.js} +2 -2
  225. package/dist/{variant-wave-Ca1Ls4_y.js → variant-wave-BNMGrDfj.js} +2 -2
  226. package/dist/{variant-wave-uY5crU_K.js → variant-wave-BpDd8t3Z.js} +1 -1
  227. package/dist/{variant-wave-CxX39DKd.js → variant-wave-CyBueXj9.js} +2 -2
  228. package/dist/{variant-wave-DYyzIIZv.js → variant-wave-sp-qwsdI.js} +1 -1
  229. package/dist/x-Z5UntkIJ.js +1 -0
  230. package/frameworks/react/index.tsx +14 -4
  231. package/llms-full.txt +7 -2
  232. package/package.json +16 -16
  233. package/src/elements/element-meta.json +49 -6
  234. package/theme.css +28 -0
  235. package/bin/route.test.js +0 -58
  236. package/dist/arrow-down-Br0qtpsz.js +0 -1
  237. package/dist/arrow-left-Dj7xgE12.js +0 -1
  238. package/dist/check-DPeeMUYx.js +0 -1
  239. package/dist/chevron-down-CHSWk7ZT.js +0 -1
  240. package/dist/chevron-right-B7l_fMgx.js +0 -1
  241. package/dist/circle-Bb0yOpqM.js +0 -1
  242. package/dist/circle-check-BGZpCiz7.js +0 -1
  243. package/dist/code-block-CUQ5mtv0.js +0 -1
  244. package/dist/controllable-D43BKmtX.js +0 -1
  245. package/dist/conversation-list-D4lLU_yS.js +0 -1
  246. package/dist/define-CGGX-7Vg.js +0 -1
  247. package/dist/message-circle-Xf8Q-vZp.js +0 -1
  248. package/dist/message-square-Cxnwgkns.js +0 -1
  249. package/dist/play-OFIvYXUQ.js +0 -1
  250. package/dist/register-impl-C7rENvPK.js +0 -424
  251. package/dist/store-7eoadydQ.js +0 -1
  252. package/dist/x-C5m1JFgi.js +0 -1
  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 -131
  263. package/src/agent-tooling/construct/codegen.ts +0 -1353
  264. package/src/agent-tooling/construct/construct.v1.schema.json +0 -250
  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 -23
  269. package/src/agent-tooling/construct/schema.ts +0 -371
  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 -518
  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 -218
  336. package/src/components/conversation-list.tsx +0 -429
  337. package/src/components/dismissed-stub.tsx +0 -80
  338. package/src/components/embed.tsx +0 -200
  339. package/src/components/empty.tsx +0 -177
  340. package/src/components/feedback-bar.tsx +0 -166
  341. package/src/components/file-tree.tsx +0 -530
  342. package/src/components/file-upload.tsx +0 -167
  343. package/src/components/form-widgets.tsx +0 -525
  344. package/src/components/form.tsx +0 -1296
  345. package/src/components/image.tsx +0 -65
  346. package/src/components/link-preview.tsx +0 -195
  347. package/src/components/loader.tsx +0 -367
  348. package/src/components/markdown.tsx +0 -162
  349. package/src/components/message-skills.tsx +0 -40
  350. package/src/components/message.tsx +0 -765
  351. package/src/components/model-switcher.tsx +0 -132
  352. package/src/components/prompt-input.tsx +0 -188
  353. package/src/components/prompt-suggestion.tsx +0 -150
  354. package/src/components/reasoning.tsx +0 -293
  355. package/src/components/response-compare-types.ts +0 -152
  356. package/src/components/response-compare.tsx +0 -455
  357. package/src/components/response-stream.tsx +0 -109
  358. package/src/components/screen.tsx +0 -181
  359. package/src/components/scroll-button.tsx +0 -93
  360. package/src/components/source.tsx +0 -192
  361. package/src/components/tasks-card.tsx +0 -801
  362. package/src/components/text-shimmer.tsx +0 -37
  363. package/src/components/thinking-bar.tsx +0 -50
  364. package/src/components/thread.tsx +0 -228
  365. package/src/components/toast.tsx +0 -503
  366. package/src/components/tool-classify.ts +0 -26
  367. package/src/components/tool-types.ts +0 -49
  368. package/src/components/tool.tsx +0 -190
  369. package/src/components/use-card-resolution.ts +0 -44
  370. package/src/components/voice-input.tsx +0 -222
  371. package/src/components/voice-output.tsx +0 -242
  372. package/src/components/workspace-shell.tsx +0 -285
  373. package/src/diagnostics/hook.ts +0 -352
  374. package/src/diagnostics/index.ts +0 -82
  375. package/src/diagnostics/report-request.ts +0 -296
  376. package/src/elements/agent-card.tsx +0 -67
  377. package/src/elements/artifact.tsx +0 -194
  378. package/src/elements/attachments.tsx +0 -140
  379. package/src/elements/audio-visualizer.tsx +0 -227
  380. package/src/elements/autoloader.ts +0 -89
  381. package/src/elements/avatar.tsx +0 -33
  382. package/src/elements/badge.tsx +0 -32
  383. package/src/elements/button.tsx +0 -149
  384. package/src/elements/card.tsx +0 -133
  385. package/src/elements/cards.tsx +0 -266
  386. package/src/elements/chain-of-thought.tsx +0 -141
  387. package/src/elements/chat-scope-picker.tsx +0 -65
  388. package/src/elements/chat-types.ts +0 -102
  389. package/src/elements/chat-workspace.tsx +0 -188
  390. package/src/elements/chat.tsx +0 -212
  391. package/src/elements/checkbox-group.tsx +0 -164
  392. package/src/elements/checkbox.tsx +0 -134
  393. package/src/elements/checkpoint.tsx +0 -47
  394. package/src/elements/choice.tsx +0 -100
  395. package/src/elements/coachmark.tsx +0 -90
  396. package/src/elements/code-block.tsx +0 -60
  397. package/src/elements/command.tsx +0 -197
  398. package/src/elements/compare.tsx +0 -111
  399. package/src/elements/compiled.css +0 -2
  400. package/src/elements/composer.tsx +0 -162
  401. package/src/elements/confirm-card.tsx +0 -76
  402. package/src/elements/context-meter.tsx +0 -107
  403. package/src/elements/conversation-item.tsx +0 -120
  404. package/src/elements/conversation-list.tsx +0 -203
  405. package/src/elements/css.ts +0 -5
  406. package/src/elements/default-input.tsx +0 -392
  407. package/src/elements/define-entry.ts +0 -12
  408. package/src/elements/define.tsx +0 -563
  409. package/src/elements/diagnostic-events.ts +0 -114
  410. package/src/elements/dialog.tsx +0 -145
  411. package/src/elements/disclosure.ts +0 -95
  412. package/src/elements/dock.tsx +0 -215
  413. package/src/elements/dropdown.tsx +0 -123
  414. package/src/elements/editable-label.tsx +0 -140
  415. package/src/elements/element-data-types.ts +0 -152
  416. package/src/elements/element-diagnostics.ts +0 -392
  417. package/src/elements/element-manifest.json +0 -354
  418. package/src/elements/element-nonscalar.json +0 -185
  419. package/src/elements/element-types.d.ts +0 -4337
  420. package/src/elements/embed.tsx +0 -35
  421. package/src/elements/empty.tsx +0 -29
  422. package/src/elements/feedback-bar.tsx +0 -66
  423. package/src/elements/file-tree.tsx +0 -62
  424. package/src/elements/file-upload.tsx +0 -44
  425. package/src/elements/form.tsx +0 -94
  426. package/src/elements/hover-card.tsx +0 -80
  427. package/src/elements/icon.tsx +0 -41
  428. package/src/elements/image.tsx +0 -32
  429. package/src/elements/input.tsx +0 -492
  430. package/src/elements/kbd.tsx +0 -45
  431. package/src/elements/link-preview.tsx +0 -34
  432. package/src/elements/loader.tsx +0 -25
  433. package/src/elements/markdown.tsx +0 -38
  434. package/src/elements/menu.tsx +0 -241
  435. package/src/elements/message-skills.tsx +0 -83
  436. package/src/elements/message.tsx +0 -361
  437. package/src/elements/model-switcher.tsx +0 -121
  438. package/src/elements/nav.tsx +0 -87
  439. package/src/elements/notice.tsx +0 -53
  440. package/src/elements/pane-grid.tsx +0 -111
  441. package/src/elements/pane-group.tsx +0 -119
  442. package/src/elements/pane.tsx +0 -129
  443. package/src/elements/popover.tsx +0 -81
  444. package/src/elements/progress-bar.tsx +0 -39
  445. package/src/elements/prompt-dock.tsx +0 -82
  446. package/src/elements/prompt-input.tsx +0 -240
  447. package/src/elements/prompt-suggestions.tsx +0 -113
  448. package/src/elements/radio-group.tsx +0 -129
  449. package/src/elements/reasoning.tsx +0 -71
  450. package/src/elements/register-impl.ts +0 -148
  451. package/src/elements/register.ts +0 -76
  452. package/src/elements/remote.tsx +0 -225
  453. package/src/elements/resizable.globals.d.ts +0 -36
  454. package/src/elements/resizable.tsx +0 -704
  455. package/src/elements/response-stream.tsx +0 -40
  456. package/src/elements/screen.tsx +0 -90
  457. package/src/elements/scroll-area.tsx +0 -33
  458. package/src/elements/scroll-button.tsx +0 -179
  459. package/src/elements/search.tsx +0 -185
  460. package/src/elements/segmented.tsx +0 -129
  461. package/src/elements/select.tsx +0 -165
  462. package/src/elements/separator.tsx +0 -36
  463. package/src/elements/setting-item.tsx +0 -50
  464. package/src/elements/settings-group.tsx +0 -37
  465. package/src/elements/skeleton.tsx +0 -45
  466. package/src/elements/slider.tsx +0 -171
  467. package/src/elements/slot-text.ts +0 -73
  468. package/src/elements/slots.ts +0 -787
  469. package/src/elements/source.tsx +0 -126
  470. package/src/elements/status.tsx +0 -42
  471. package/src/elements/styles.css +0 -328
  472. package/src/elements/switch.tsx +0 -118
  473. package/src/elements/tabs.tsx +0 -95
  474. package/src/elements/tasks.tsx +0 -103
  475. package/src/elements/text-shimmer.tsx +0 -28
  476. package/src/elements/thinking-bar.tsx +0 -43
  477. package/src/elements/thread.tsx +0 -139
  478. package/src/elements/toast.tsx +0 -106
  479. package/src/elements/tool.tsx +0 -78
  480. package/src/elements/tooltip.tsx +0 -85
  481. package/src/elements/validate-messages.ts +0 -76
  482. package/src/elements/voice-input.tsx +0 -100
  483. package/src/elements/voice-output.tsx +0 -82
  484. package/src/index.ts +0 -428
  485. package/src/primitives/audio-bands.ts +0 -174
  486. package/src/primitives/card-component-types.ts +0 -63
  487. package/src/primitives/card-contract.ts +0 -80
  488. package/src/primitives/card-data-types.ts +0 -361
  489. package/src/primitives/card-host.tsx +0 -37
  490. package/src/primitives/card-recovery.ts +0 -160
  491. package/src/primitives/card-registry.tsx +0 -78
  492. package/src/primitives/card-resolution.ts +0 -36
  493. package/src/primitives/card-routing.ts +0 -85
  494. package/src/primitives/card-schemas/artifact.schema.json +0 -92
  495. package/src/primitives/card-schemas/card-envelope.schema.json +0 -14
  496. package/src/primitives/card-schemas/card-event.schema.json +0 -12
  497. package/src/primitives/card-schemas/choice.schema.json +0 -75
  498. package/src/primitives/card-schemas/confirm.schema.json +0 -65
  499. package/src/primitives/card-schemas/embed.schema.json +0 -63
  500. package/src/primitives/card-schemas/form.result.schema.json +0 -7
  501. package/src/primitives/card-schemas/form.schema.json +0 -53
  502. package/src/primitives/card-schemas/link.schema.json +0 -54
  503. package/src/primitives/card-schemas/tasks.result.schema.json +0 -16
  504. package/src/primitives/card-schemas/tasks.schema.json +0 -83
  505. package/src/primitives/card-tags.ts +0 -61
  506. package/src/primitives/card-validate-cards.ts +0 -255
  507. package/src/primitives/card-validate-schemas.ts +0 -58
  508. package/src/primitives/card-validate.ts +0 -131
  509. package/src/primitives/chat-config.tsx +0 -103
  510. package/src/primitives/composer-model.ts +0 -49
  511. package/src/primitives/composer-triggers.ts +0 -35
  512. package/src/primitives/controllable.ts +0 -21
  513. package/src/primitives/create-kai-chat.ts +0 -66
  514. package/src/primitives/create-tween.ts +0 -266
  515. package/src/primitives/embed-providers.ts +0 -254
  516. package/src/primitives/field-mask.ts +0 -256
  517. package/src/primitives/field-semantics.ts +0 -115
  518. package/src/primitives/highlighter.ts +0 -157
  519. package/src/primitives/input-mask.ts +0 -853
  520. package/src/primitives/link-preview.ts +0 -87
  521. package/src/primitives/message-feedback.ts +0 -124
  522. package/src/primitives/pdf-preview.ts +0 -121
  523. package/src/primitives/toast-store.ts +0 -338
  524. package/src/primitives/url-scheme-policy.ts +0 -70
  525. package/src/primitives/use-audio-analysis.ts +0 -340
  526. package/src/primitives/use-auto-resize.ts +0 -31
  527. package/src/primitives/use-resize-observer.ts +0 -45
  528. package/src/primitives/use-sequencer.ts +0 -72
  529. package/src/primitives/use-speech-recognition.ts +0 -146
  530. package/src/primitives/use-stick-to-bottom.ts +0 -75
  531. package/src/primitives/use-text-stream.ts +0 -143
  532. package/src/primitives/use-voice-recorder.ts +0 -62
  533. package/src/primitives/visualizer-sequences.ts +0 -350
  534. package/src/remote/host-embed.ts +0 -345
  535. package/src/remote/index.ts +0 -2
  536. package/src/remote/origin.ts +0 -30
  537. package/src/remote/provider-runtime.ts +0 -262
  538. package/src/remote/provider.ts +0 -2
  539. package/src/remote/validate.ts +0 -22
  540. package/src/remote/version.ts +0 -12
  541. package/src/remote/wire.ts +0 -48
  542. package/src/schemas/from-tool-call.ts +0 -151
  543. package/src/schemas/index.ts +0 -341
  544. package/src/schemas/provider-subsets.ts +0 -538
  545. package/src/schemas/registry.ts +0 -355
  546. package/src/schemas/tool-defs.ts +0 -793
  547. package/src/solid.ts +0 -165
  548. package/src/state/index.ts +0 -44
  549. package/src/state/messages.ts +0 -53
  550. package/src/state/mock.ts +0 -297
  551. package/src/state/parts.ts +0 -295
  552. package/src/state/persistence.ts +0 -180
  553. package/src/state/stream.ts +0 -246
  554. package/src/state/suggestions.ts +0 -9
  555. package/src/state/threads.ts +0 -142
  556. package/src/test-utils/fake-clock.ts +0 -88
  557. package/src/types.ts +0 -53
  558. package/src/ui/action-icons.ts +0 -53
  559. package/src/ui/agent-card.tsx +0 -218
  560. package/src/ui/avatar.tsx +0 -23
  561. package/src/ui/badge.tsx +0 -21
  562. package/src/ui/button.tsx +0 -49
  563. package/src/ui/card.tsx +0 -245
  564. package/src/ui/checkbox-group.tsx +0 -153
  565. package/src/ui/checkbox.tsx +0 -63
  566. package/src/ui/collapsible.tsx +0 -163
  567. package/src/ui/command.tsx +0 -103
  568. package/src/ui/dialog.tsx +0 -223
  569. package/src/ui/dock.tsx +0 -584
  570. package/src/ui/dropdown.tsx +0 -636
  571. package/src/ui/editable-label.tsx +0 -112
  572. package/src/ui/hover-card.tsx +0 -345
  573. package/src/ui/icon.tsx +0 -167
  574. package/src/ui/input.tsx +0 -391
  575. package/src/ui/kbd.tsx +0 -118
  576. package/src/ui/nav.tsx +0 -282
  577. package/src/ui/notice.tsx +0 -84
  578. package/src/ui/overlay.tsx +0 -277
  579. package/src/ui/pane-grid.tsx +0 -117
  580. package/src/ui/pane-group.tsx +0 -291
  581. package/src/ui/pane.tsx +0 -193
  582. package/src/ui/popover.tsx +0 -106
  583. package/src/ui/progress-bar.tsx +0 -83
  584. package/src/ui/prompt-dock.tsx +0 -145
  585. package/src/ui/radio.tsx +0 -150
  586. package/src/ui/resizable.tsx +0 -693
  587. package/src/ui/scroll-area.tsx +0 -26
  588. package/src/ui/segmented.tsx +0 -109
  589. package/src/ui/select.tsx +0 -168
  590. package/src/ui/separator.tsx +0 -10
  591. package/src/ui/settings-group.tsx +0 -67
  592. package/src/ui/skeleton.tsx +0 -73
  593. package/src/ui/slider.tsx +0 -178
  594. package/src/ui/status.tsx +0 -48
  595. package/src/ui/switch.tsx +0 -119
  596. package/src/ui/tabs.tsx +0 -168
  597. package/src/ui/textarea.tsx +0 -21
  598. package/src/ui/tooltip.tsx +0 -118
  599. package/src/utils/cn.ts +0 -30
  600. package/src/wire/chunk.ts +0 -248
  601. package/src/wire/consume.ts +0 -599
  602. package/src/wire/diagnostics.ts +0 -727
  603. package/src/wire/encode-probe.ts +0 -214
  604. package/src/wire/encode.ts +0 -947
  605. package/src/wire/files.ts +0 -342
  606. package/src/wire/formats/anthropic.ts +0 -274
  607. package/src/wire/formats/openai.ts +0 -228
  608. package/src/wire/index.ts +0 -95
  609. package/src/wire/media-types.ts +0 -344
  610. package/src/wire/read.ts +0 -396
  611. package/src/wire/sink-helpers.ts +0 -50
  612. 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
- }