@kitn.ai/ui 0.26.0 → 0.27.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 (474) hide show
  1. package/README.md +16 -5
  2. package/bin/mcp.js +16 -3
  3. package/bin/route.js +23 -0
  4. package/bin/route.test.js +58 -0
  5. package/dist/{elements/chunks/Icon--Y8vsFal.js → Icon-BnFKqqtY.js} +1 -1
  6. package/dist/action-icons-CBskfBMz.js +1 -0
  7. package/dist/arrow-down-Br0qtpsz.js +1 -0
  8. package/dist/arrow-left-Dj7xgE12.js +1 -0
  9. package/dist/artifact-CvXEm1lS.js +1 -0
  10. package/dist/assets/dev-5XzXYR21.js +91 -0
  11. package/dist/attachments-C43ZegEq.js +1 -0
  12. package/dist/{elements/chunks/audio-visualizer-D7lB6mvR.js → audio-visualizer-B-79y1Tj.js} +1 -1
  13. package/dist/badge-BpUbq8c0.js +1 -0
  14. package/dist/bash-InADTalH.js +1 -6
  15. package/dist/bell-sn17Ip9v.js +1 -0
  16. package/dist/{elements/chunks/button-flUbeufF.js → button-BH6rN0U-.js} +1 -1
  17. package/dist/card-renderer-Cb1gQmth.js +1 -0
  18. package/dist/card-routing-DifQgS7n.js +1 -0
  19. package/dist/check-DPeeMUYx.js +1 -0
  20. package/dist/checkbox-Dc9Yov_B.js +1 -0
  21. package/dist/checkbox-group-B1kDLLYk.js +1 -0
  22. package/dist/chevron-down-CHSWk7ZT.js +1 -0
  23. package/dist/chevron-right-B7l_fMgx.js +1 -0
  24. package/dist/choice-card-BT3c1S8g.js +1 -0
  25. package/dist/circle-Bb0yOpqM.js +1 -0
  26. package/dist/circle-check-BGZpCiz7.js +1 -0
  27. package/dist/circle-x-wobWkoSx.js +1 -0
  28. package/dist/{elements/chunks/cn-CU7UAWzk.js → cn-DN5AWfiS.js} +1 -1
  29. package/dist/{elements/chunks/code-block-BdLiTitd.js → code-block-CUQ5mtv0.js} +1 -1
  30. package/dist/{elements/chunks/collapsible-BzbXpPzr.js → collapsible-CPJbf6FM.js} +1 -1
  31. package/dist/components/attachment-types.d.ts +7 -0
  32. package/dist/components/audio-visualizer/fit-scale.d.ts +1 -1
  33. package/dist/components/card-renderer.d.ts +13 -0
  34. package/dist/components/card.d.ts +4 -0
  35. package/dist/components/chat-thread.d.ts +46 -1
  36. package/dist/components/conversation-item.d.ts +19 -7
  37. package/dist/components/conversation-list.d.ts +17 -5
  38. package/dist/components/form-widgets.d.ts +67 -2
  39. package/dist/components/form.d.ts +48 -1
  40. package/dist/components/message.d.ts +25 -3
  41. package/dist/components/reasoning.d.ts +6 -0
  42. package/dist/components/scroll-button.d.ts +11 -0
  43. package/dist/components/thread.d.ts +5 -0
  44. package/dist/components/tool-types.d.ts +9 -0
  45. package/dist/composer-CjngLzal.js +106 -0
  46. package/dist/confirm-card-DCx_A4bU.js +1 -0
  47. package/dist/construct-cli.es.js +1169 -0
  48. package/dist/{elements/chunks/context-VLIL3sJ-.js → context-BY8V-Nlp.js} +1 -1
  49. package/dist/controllable-D43BKmtX.js +1 -0
  50. package/dist/conversation-list-D4lLU_yS.js +1 -0
  51. package/dist/copy-B6DO4PqB.js +1 -0
  52. package/dist/core-AYMC6_lb.js +12 -5874
  53. package/dist/{create-tween-CQED4T0g.js → create-tween-BPzaefTp.js} +1 -1
  54. package/dist/{create-tween-BFT6c-Y8.js → create-tween-CnMHviKB.js} +1 -1
  55. package/dist/{create-tween-CJNlzoaO.js → create-tween-D1aiGZZh.js} +1 -1
  56. package/dist/{elements/chunks/create-tween-c4Z8TDYz.js → create-tween-DUSQUKpr.js} +2 -2
  57. package/dist/css-M7EaDHN_.js +1 -6
  58. package/dist/custom-elements.json +1400 -218
  59. package/dist/default-input-CucQEOqH.js +1 -0
  60. package/dist/define-CGGX-7Vg.js +1 -0
  61. package/dist/define.d.ts +4 -0
  62. package/dist/define.js +510 -0
  63. package/dist/define.server.d.ts +5 -0
  64. package/dist/define.server.js +506 -0
  65. package/dist/{elements/chunks/disclosure-CcNbyLy0.js → disclosure-CbLtajUQ.js} +1 -1
  66. package/dist/{elements/chunks/dropdown-BgWTthUx.js → dropdown-BwhViJFd.js} +1 -1
  67. package/dist/elements/agent-card.js +1 -1
  68. package/dist/elements/artifact.js +1 -1
  69. package/dist/elements/attachments.js +1 -1
  70. package/dist/elements/audio-visualizer.js +1 -1
  71. package/dist/elements/autoloader.js +1 -1
  72. package/dist/elements/avatar.js +1 -1
  73. package/dist/elements/badge.js +1 -1
  74. package/dist/elements/button.js +1 -1
  75. package/dist/elements/card.js +1 -1
  76. package/dist/elements/cards.js +1 -1
  77. package/dist/elements/chain-of-thought.js +1 -1
  78. package/dist/elements/chat-scope-picker.js +1 -1
  79. package/dist/elements/chat-workspace.js +1 -1
  80. package/dist/elements/chat.js +1 -1
  81. package/dist/elements/checkbox-group.d.ts +1 -0
  82. package/dist/elements/checkbox-group.js +1 -0
  83. package/dist/elements/checkbox.d.ts +1 -0
  84. package/dist/elements/checkbox.js +1 -0
  85. package/dist/elements/checkpoint.js +1 -1
  86. package/dist/elements/choice.js +1 -1
  87. package/dist/elements/coachmark.js +1 -1
  88. package/dist/elements/code-block.js +1 -1
  89. package/dist/elements/command.js +1 -1
  90. package/dist/elements/compare.js +1 -1
  91. package/dist/elements/composer.js +1 -1
  92. package/dist/elements/confirm-card.js +1 -1
  93. package/dist/elements/context-meter.js +1 -1
  94. package/dist/elements/conversation-item.js +1 -1
  95. package/dist/elements/conversation-list.js +1 -1
  96. package/dist/elements/define-entry.d.ts +12 -0
  97. package/dist/elements/dialog.js +1 -1
  98. package/dist/elements/dock.js +101 -7
  99. package/dist/elements/dropdown.js +1 -1
  100. package/dist/elements/editable-label.js +1 -1
  101. package/dist/elements/element-data-types.d.ts +37 -0
  102. package/dist/elements/embed.js +1 -1
  103. package/dist/elements/empty.js +1 -1
  104. package/dist/elements/feedback-bar.js +1 -1
  105. package/dist/elements/file-tree.js +1 -1
  106. package/dist/elements/file-upload.js +1 -1
  107. package/dist/elements/form.js +1 -1
  108. package/dist/elements/hover-card.js +1 -1
  109. package/dist/elements/icon.js +1 -1
  110. package/dist/elements/image.js +1 -1
  111. package/dist/elements/input.js +1 -1
  112. package/dist/elements/kbd.js +1 -1
  113. package/dist/elements/link-preview.js +1 -1
  114. package/dist/elements/loader.js +1 -1
  115. package/dist/elements/markdown.js +1 -1
  116. package/dist/elements/menu.js +1 -1
  117. package/dist/elements/message-skills.js +1 -1
  118. package/dist/elements/message.js +1 -1
  119. package/dist/elements/model-switcher.js +1 -1
  120. package/dist/elements/nav.js +1 -1
  121. package/dist/elements/notice.js +1 -1
  122. package/dist/elements/pane-grid.d.ts +1 -0
  123. package/dist/elements/pane-grid.js +1 -0
  124. package/dist/elements/pane-group.js +1 -1
  125. package/dist/elements/pane.js +1 -1
  126. package/dist/elements/popover.js +1 -1
  127. package/dist/elements/progress-bar.js +1 -1
  128. package/dist/elements/prompt-dock.js +1 -1
  129. package/dist/elements/prompt-input.js +1 -1
  130. package/dist/elements/prompt-suggestions.js +1 -1
  131. package/dist/elements/radio-group.d.ts +1 -0
  132. package/dist/elements/radio-group.js +1 -0
  133. package/dist/elements/reasoning.js +1 -1
  134. package/dist/elements/remote.js +1 -1
  135. package/dist/elements/resizable.js +1 -1
  136. package/dist/elements/response-stream.js +2 -2
  137. package/dist/elements/screen.js +1 -1
  138. package/dist/elements/scroll-area.js +1 -1
  139. package/dist/elements/scroll-button.js +1 -1
  140. package/dist/elements/search.js +1 -1
  141. package/dist/elements/segmented.js +1 -1
  142. package/dist/elements/select.d.ts +1 -0
  143. package/dist/elements/select.js +1 -0
  144. package/dist/elements/separator.js +1 -1
  145. package/dist/elements/setting-item.js +1 -1
  146. package/dist/elements/settings-group.js +1 -1
  147. package/dist/elements/skeleton.js +1 -1
  148. package/dist/elements/slider.d.ts +1 -0
  149. package/dist/elements/slider.js +1 -0
  150. package/dist/elements/slots.d.ts +1 -1
  151. package/dist/elements/source.js +1 -1
  152. package/dist/elements/status.js +1 -1
  153. package/dist/elements/switch.js +1 -1
  154. package/dist/elements/tabs.js +1 -1
  155. package/dist/elements/tasks.js +1 -1
  156. package/dist/elements/text-shimmer.js +1 -1
  157. package/dist/elements/thinking-bar.js +1 -1
  158. package/dist/elements/thread.js +1 -1
  159. package/dist/elements/toast.js +1 -1
  160. package/dist/elements/tool.js +1 -1
  161. package/dist/elements/tooltip.js +1 -1
  162. package/dist/elements/voice-input.js +1 -1
  163. package/dist/elements/voice-output.js +1 -1
  164. package/dist/elements.d.ts +362 -24
  165. package/dist/{elements/chunks/ellipsis-7gU33mL2.js → ellipsis-DFr4Vqr3.js} +1 -1
  166. package/dist/embed-5s_vU4J0.js +1 -0
  167. package/dist/engine-javascript-vq0WuIJl.js +14 -2516
  168. package/dist/{elements/chunks/external-link-CSOEsNlx.js → external-link-u9d85ORQ.js} +1 -1
  169. package/dist/file-text-CX7_x42o.js +1 -0
  170. package/dist/{elements/chunks/file-tree-zvB2i7P9.js → file-tree-D3kBjXI5.js} +1 -1
  171. package/dist/folder-V-YdIL8f.js +1 -0
  172. package/dist/form-BRNUMCN2.js +1 -0
  173. package/dist/github-dark-dimmed-DUshB20C.js +1 -4
  174. package/dist/github-light-JYsPkUQd.js +1 -4
  175. package/dist/hover-card-DaeiYrTA.js +1 -0
  176. package/dist/html-CPZ3oZQ7.js +1 -10
  177. package/dist/icon-BpixdZNq.js +1 -0
  178. package/dist/{elements/chunks/index-BCRhi84a.js → index-xu_pdcPi.js} +1 -1
  179. package/dist/index.d.ts +1 -1
  180. package/dist/index.js +7330 -5852
  181. package/dist/index.server.js +5823 -4759
  182. package/dist/info-8mFsIjS3.js +1 -0
  183. package/dist/input-d1Yuu-hz.js +1 -0
  184. package/dist/javascript-C25yR2R2.js +1 -6
  185. package/dist/json-DxJze_jm.js +1 -6
  186. package/dist/kai.es.js +1 -1
  187. package/dist/kbd-961zMPFB.js +1 -0
  188. package/dist/link-DlZZBGaR.js +1 -0
  189. package/dist/{elements/chunks/link-preview-OQ9IEq0f.js → link-preview-DIIbV2Sd.js} +1 -1
  190. package/dist/loader-YQszHFaW.js +1 -0
  191. package/dist/{elements/chunks/markdown-DgBFaGAz.js → markdown-CVk1MPsj.js} +1 -1
  192. package/dist/mcp.es.js +1285 -79
  193. package/dist/message-CwKVEdZg.js +1 -0
  194. package/dist/message-DS8lzHc1.js +1 -0
  195. package/dist/message-circle-Xf8Q-vZp.js +1 -0
  196. package/dist/message-square-Cxnwgkns.js +1 -0
  197. package/dist/{elements/chunks/minimize-2-DPIUKTrT.js → minimize-2-UEZmP2QT.js} +1 -1
  198. package/dist/{elements/chunks/model-switcher-DsT8tFB0.js → model-switcher-Kk1jEQ5t.js} +1 -1
  199. package/dist/{elements/chunks/overlay-DuiwdXjm.js → overlay-C1od6LOz.js} +1 -1
  200. package/dist/panel-right-ryET0yyn.js +1 -0
  201. package/dist/paperclip-BDNoZFJP.js +1 -0
  202. package/dist/play-OFIvYXUQ.js +1 -0
  203. package/dist/primitives/card-data-types.d.ts +49 -0
  204. package/dist/primitives/card-routing.d.ts +1 -33
  205. package/dist/primitives/card-validate.d.ts +2 -2
  206. package/dist/primitives/chat-config.d.ts +29 -2
  207. package/dist/primitives/field-mask.d.ts +96 -0
  208. package/dist/primitives/field-semantics.d.ts +36 -0
  209. package/dist/primitives/input-mask.d.ts +46 -0
  210. package/dist/primitives/url-scheme-policy.d.ts +34 -0
  211. package/dist/primitives/use-resize-observer.d.ts +2 -2
  212. package/dist/progress-bar-BvYX0aoy.js +1 -0
  213. package/dist/prompt-suggestion-BRFu2lcu.js +1 -0
  214. package/dist/radio-DcL23cNJ.js +1 -0
  215. package/dist/react/index.d.ts +193 -19
  216. package/dist/react.js +216 -180
  217. package/dist/reasoning-Cackq4Rh.js +1 -0
  218. package/dist/register-impl-C7rENvPK.js +424 -0
  219. package/dist/{elements/chunks/resizable-D181b4fM.js → resizable-Cp9HUYih.js} +1 -1
  220. package/dist/rotate-ccw-0DLRDHH5.js +1 -0
  221. package/dist/rotate-cw-DOB9IGcU.js +1 -0
  222. package/dist/schemas/form.schema.json +21 -1
  223. package/dist/schemas/tool-defs.d.ts +11 -1
  224. package/dist/schemas.js +81 -81
  225. package/dist/scroll-area-emVmWP4K.js +1 -0
  226. package/dist/scroll-button-CeP_gv1x.js +1 -0
  227. package/dist/select-CWmu00tS.js +1 -0
  228. package/dist/{elements/chunks/separator-CeJHQqLW.js → separator-CE6vly6z.js} +1 -1
  229. package/dist/{elements/chunks/settings-CqaHgJF9.js → settings-CE-78SvZ.js} +1 -1
  230. package/dist/{elements/chunks/settings-group-CjqjLCOn.js → settings-group-ZFWNxFQR.js} +1 -1
  231. package/dist/{elements/chunks/skeleton-D8akBnKS.js → skeleton-BebdP4mf.js} +1 -1
  232. package/dist/slider-BAKRYUlz.js +1 -0
  233. package/dist/{solid-BtdqNZI7.js → solid-BE5ui6sy.js} +8727 -7212
  234. package/dist/{solid-CnavwBye.js → solid-DFhsce3O.js} +7015 -5886
  235. package/dist/solid.d.ts +10 -0
  236. package/dist/solid.js +240 -234
  237. package/dist/solid.server.js +240 -234
  238. package/dist/source-B0FlguSx.js +1 -0
  239. package/dist/star-CBMZvLuN.js +1 -0
  240. package/dist/state/index.d.ts +1 -1
  241. package/dist/state/mock.d.ts +32 -2
  242. package/dist/state.js +133 -108
  243. package/dist/{elements/chunks/store-Cjq0qwYN.js → store-7eoadydQ.js} +1 -1
  244. package/dist/svelte--5p79yCD.js +1 -15
  245. package/dist/switch-CViARZCy.js +1 -0
  246. package/dist/tasks-card-MjCcOhR_.js +1 -0
  247. package/dist/text-shimmer-DKlwRF2G.js +1 -0
  248. package/dist/textarea-3ay2Me2-.js +1 -0
  249. package/dist/theme.tokens.css +82 -13
  250. package/dist/{elements/chunks/thumbs-up-DX2Ijk3H.js → thumbs-up-DQcu2HMO.js} +1 -1
  251. package/dist/toast-store-C6yKOhbk.js +1 -0
  252. package/dist/{elements/chunks/tool-DKQ2D1DP.js → tool-BCYNlfKy.js} +1 -1
  253. package/dist/{elements/chunks/tooltip-B0G99y8w.js → tooltip-05zI4hjs.js} +1 -1
  254. package/dist/trash-2-BR1RxGky.js +1 -0
  255. package/dist/{elements/chunks/triangle-alert-F9cyUEyy.js → triangle-alert-DHohDOCy.js} +1 -1
  256. package/dist/tsx-B8rCNbgL.js +1 -6
  257. package/dist/typescript-RycA9KXf.js +1 -6
  258. package/dist/ui/checkbox-group.d.ts +83 -0
  259. package/dist/ui/checkbox.d.ts +40 -0
  260. package/dist/ui/dock.d.ts +22 -0
  261. package/dist/ui/icon.d.ts +3 -0
  262. package/dist/ui/input.d.ts +48 -2
  263. package/dist/ui/radio.d.ts +85 -0
  264. package/dist/ui/select.d.ts +71 -0
  265. package/dist/ui/slider.d.ts +65 -0
  266. package/dist/ui/switch.d.ts +9 -2
  267. package/dist/upload-BbGW6A_u.js +1 -0
  268. package/dist/url-scheme-policy-DHpoTJwB.js +1 -0
  269. package/dist/use-card-resolution-CJW4grFa.js +1 -0
  270. package/dist/{variant-aurora-B8II3-I1.js → variant-aurora-BUOVgPOB.js} +2 -2
  271. package/dist/{variant-aurora-CMNWOSJR.js → variant-aurora-CUsUJvFI.js} +2 -2
  272. package/dist/{variant-aurora-CcuDMWwQ.js → variant-aurora-CW2i4bfr.js} +1 -1
  273. package/dist/{elements/chunks/variant-aurora-qt6Zct8M.js → variant-aurora-xiMPty8f.js} +1 -1
  274. package/dist/{variant-custom-L16JLLZy.js → variant-custom-BQtOnmeO.js} +1 -1
  275. package/dist/{variant-custom-C714mqKl.js → variant-custom-CAm9L-E5.js} +2 -2
  276. package/dist/{variant-custom-BdvYC7af.js → variant-custom-CAnzzppW.js} +2 -2
  277. package/dist/{elements/chunks/variant-custom-tJp22_BQ.js → variant-custom-D91HjAKz.js} +1 -1
  278. package/dist/{variant-wave-ecwykeyL.js → variant-wave-Ca1Ls4_y.js} +2 -2
  279. package/dist/{variant-wave-D7ruiA1W.js → variant-wave-CxX39DKd.js} +2 -2
  280. package/dist/{elements/chunks/variant-wave-CaQ5ti39.js → variant-wave-DYyzIIZv.js} +1 -1
  281. package/dist/{variant-wave-2S4La2Ka.js → variant-wave-uY5crU_K.js} +1 -1
  282. package/dist/vue-BmIZj4XD.js +1 -33
  283. package/dist/x-C5m1JFgi.js +1 -0
  284. package/frameworks/react/index.tsx +198 -20
  285. package/llms-full.txt +1178 -21
  286. package/llms.txt +6 -3
  287. package/package.json +17 -4
  288. package/src/agent-tooling/catalog/invariants.ts +1 -1
  289. package/src/agent-tooling/catalog/surfaces.ts +72 -0
  290. package/src/agent-tooling/construct/cli-entry.ts +8 -0
  291. package/src/agent-tooling/construct/cli.ts +131 -0
  292. package/src/agent-tooling/construct/codegen.ts +1353 -0
  293. package/src/agent-tooling/construct/construct.v1.schema.json +250 -0
  294. package/src/agent-tooling/construct/dev.ts +148 -0
  295. package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +7 -0
  296. package/src/agent-tooling/construct/fixtures/ops-console.construct.json +44 -0
  297. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +23 -0
  298. package/src/agent-tooling/construct/schema.ts +371 -0
  299. package/src/agent-tooling/mcp/server.ts +2 -1
  300. package/src/agent-tooling/mcp/tools/construct.ts +130 -0
  301. package/src/agent-tooling/mcp/tools/debug.ts +126 -4
  302. package/src/agent-tooling/mcp/tools/reference.ts +104 -3
  303. package/src/agent-tooling/recipes/composed-thread.ts +714 -0
  304. package/src/agent-tooling/recipes/index.ts +22 -0
  305. package/src/agent-tooling/recipes/types.ts +32 -0
  306. package/src/agent-tooling/registry.ts +5 -0
  307. package/src/components/attachment-types.ts +7 -0
  308. package/src/components/audio-visualizer/fit-scale.ts +1 -1
  309. package/src/components/card-renderer.tsx +39 -8
  310. package/src/components/card.tsx +10 -2
  311. package/src/components/chat-scope-picker.tsx +2 -2
  312. package/src/components/chat-thread.tsx +72 -9
  313. package/src/components/choice-card.tsx +102 -37
  314. package/src/components/coachmark.tsx +1 -1
  315. package/src/components/code-block.tsx +2 -2
  316. package/src/components/composer.tsx +89 -15
  317. package/src/components/confirm-card.tsx +20 -4
  318. package/src/components/conversation-item.tsx +33 -9
  319. package/src/components/conversation-list.tsx +25 -10
  320. package/src/components/empty.tsx +14 -3
  321. package/src/components/file-tree.tsx +5 -1
  322. package/src/components/form-widgets.tsx +257 -195
  323. package/src/components/form.tsx +297 -9
  324. package/src/components/loader.tsx +12 -5
  325. package/src/components/message-skills.tsx +1 -1
  326. package/src/components/message.tsx +71 -11
  327. package/src/components/prompt-input.tsx +7 -1
  328. package/src/components/prompt-suggestion.tsx +4 -1
  329. package/src/components/reasoning.tsx +30 -3
  330. package/src/components/response-compare.tsx +2 -2
  331. package/src/components/scroll-button.tsx +56 -4
  332. package/src/components/source.tsx +6 -1
  333. package/src/components/tasks-card.tsx +58 -16
  334. package/src/components/thread.tsx +10 -1
  335. package/src/components/toast.tsx +1 -1
  336. package/src/components/tool-types.ts +9 -0
  337. package/src/elements/attachments.tsx +3 -1
  338. package/src/elements/chat-workspace.tsx +1 -1
  339. package/src/elements/chat.tsx +22 -4
  340. package/src/elements/checkbox-group.tsx +164 -0
  341. package/src/elements/checkbox.tsx +134 -0
  342. package/src/elements/compiled.css +1 -1
  343. package/src/elements/composer.tsx +12 -1
  344. package/src/elements/conversation-item.tsx +62 -22
  345. package/src/elements/conversation-list.tsx +2 -2
  346. package/src/elements/define-entry.ts +12 -0
  347. package/src/elements/dock.tsx +11 -0
  348. package/src/elements/element-data-types.ts +40 -0
  349. package/src/elements/element-manifest.json +24 -0
  350. package/src/elements/element-meta.json +736 -46
  351. package/src/elements/element-nonscalar.json +15 -0
  352. package/src/elements/element-types.d.ts +362 -24
  353. package/src/elements/feedback-bar.tsx +5 -0
  354. package/src/elements/icon-names.json +29 -0
  355. package/src/elements/input.tsx +312 -23
  356. package/src/elements/message.tsx +2 -0
  357. package/src/elements/pane-grid.tsx +111 -0
  358. package/src/elements/prompt-input.tsx +8 -2
  359. package/src/elements/radio-group.tsx +129 -0
  360. package/src/elements/register-impl.ts +6 -0
  361. package/src/elements/scroll-button.tsx +54 -6
  362. package/src/elements/select.tsx +165 -0
  363. package/src/elements/slider.tsx +171 -0
  364. package/src/elements/slots.ts +1 -1
  365. package/src/elements/styles.css +89 -3
  366. package/src/elements/thread.tsx +2 -0
  367. package/src/elements/toast.tsx +14 -1
  368. package/src/index.ts +2 -1
  369. package/src/primitives/card-data-types.ts +49 -0
  370. package/src/primitives/card-host.tsx +4 -2
  371. package/src/primitives/card-routing.ts +7 -56
  372. package/src/primitives/card-schemas/form.schema.json +21 -1
  373. package/src/primitives/card-validate.ts +10 -2
  374. package/src/primitives/chat-config.tsx +33 -6
  375. package/src/primitives/field-mask.ts +256 -0
  376. package/src/primitives/field-semantics.ts +115 -0
  377. package/src/primitives/input-mask.ts +853 -0
  378. package/src/primitives/toast-store.ts +66 -4
  379. package/src/primitives/url-scheme-policy.ts +70 -0
  380. package/src/primitives/use-resize-observer.ts +2 -2
  381. package/src/schemas/tool-defs.ts +12 -2
  382. package/src/solid.ts +10 -0
  383. package/src/state/index.ts +1 -1
  384. package/src/state/mock.ts +92 -6
  385. package/src/ui/agent-card.tsx +2 -2
  386. package/src/ui/avatar.tsx +1 -1
  387. package/src/ui/checkbox-group.tsx +153 -0
  388. package/src/ui/checkbox.tsx +63 -0
  389. package/src/ui/dock.tsx +145 -6
  390. package/src/ui/icon.tsx +53 -0
  391. package/src/ui/input.tsx +249 -9
  392. package/src/ui/kbd.tsx +1 -1
  393. package/src/ui/nav.tsx +1 -1
  394. package/src/ui/pane-group.tsx +2 -2
  395. package/src/ui/radio.tsx +150 -0
  396. package/src/ui/select.tsx +168 -0
  397. package/src/ui/slider.tsx +178 -0
  398. package/src/ui/switch.tsx +49 -18
  399. package/src/utils/cn.ts +11 -5
  400. package/theme.css +84 -13
  401. package/dist/elements/chunks/action-icons-D9u3OzYW.js +0 -1
  402. package/dist/elements/chunks/arrow-left-CVh1TeN7.js +0 -1
  403. package/dist/elements/chunks/artifact-BkYcfIlZ.js +0 -1
  404. package/dist/elements/chunks/attachments-DwfNlubq.js +0 -1
  405. package/dist/elements/chunks/badge-Q0-Cmwzz.js +0 -1
  406. package/dist/elements/chunks/bash-InADTalH.js +0 -1
  407. package/dist/elements/chunks/card-renderer-3Dyl-FpN.js +0 -1
  408. package/dist/elements/chunks/card-routing-BK09BdmL.js +0 -1
  409. package/dist/elements/chunks/check-B5kBPKeo.js +0 -1
  410. package/dist/elements/chunks/chevron-down-DZ1GS39h.js +0 -1
  411. package/dist/elements/chunks/chevron-right-CvP7OUd2.js +0 -1
  412. package/dist/elements/chunks/choice-card-D4QUIOWZ.js +0 -1
  413. package/dist/elements/chunks/circle-CDf9g6MQ.js +0 -1
  414. package/dist/elements/chunks/circle-check-CA47Nqbz.js +0 -1
  415. package/dist/elements/chunks/circle-x-CJ_HVbC8.js +0 -1
  416. package/dist/elements/chunks/composer-Byve-n8K.js +0 -69
  417. package/dist/elements/chunks/confirm-card-B0flE3MN.js +0 -1
  418. package/dist/elements/chunks/controllable-DTwTIlwp.js +0 -1
  419. package/dist/elements/chunks/conversation-item-BDztezOG.js +0 -1
  420. package/dist/elements/chunks/copy-Nf-kqxXH.js +0 -1
  421. package/dist/elements/chunks/core-AYMC6_lb.js +0 -12
  422. package/dist/elements/chunks/css-M7EaDHN_.js +0 -1
  423. package/dist/elements/chunks/default-input-C2iLFq0b.js +0 -1
  424. package/dist/elements/chunks/define-wjHR1MkU.js +0 -1
  425. package/dist/elements/chunks/download-C4V7tBdb.js +0 -1
  426. package/dist/elements/chunks/embed-D3KewsIJ.js +0 -1
  427. package/dist/elements/chunks/engine-javascript-vq0WuIJl.js +0 -141
  428. package/dist/elements/chunks/file-text-D1G8khle.js +0 -1
  429. package/dist/elements/chunks/folder-WW2LGLid.js +0 -1
  430. package/dist/elements/chunks/form-CQirdC4W.js +0 -1
  431. package/dist/elements/chunks/github-dark-dimmed-DUshB20C.js +0 -1
  432. package/dist/elements/chunks/github-light-JYsPkUQd.js +0 -1
  433. package/dist/elements/chunks/hover-card-CJJkfHTK.js +0 -1
  434. package/dist/elements/chunks/html-CPZ3oZQ7.js +0 -1
  435. package/dist/elements/chunks/icon-Cfgmf4fa.js +0 -1
  436. package/dist/elements/chunks/info-DX3YMOt6.js +0 -1
  437. package/dist/elements/chunks/input-BtW9U6Sq.js +0 -1
  438. package/dist/elements/chunks/javascript-C25yR2R2.js +0 -1
  439. package/dist/elements/chunks/json-DxJze_jm.js +0 -1
  440. package/dist/elements/chunks/kbd-LGXwuT_V.js +0 -1
  441. package/dist/elements/chunks/link-2My69BqT.js +0 -1
  442. package/dist/elements/chunks/loader-iOxc_Tu-.js +0 -1
  443. package/dist/elements/chunks/message-VMZohrXx.js +0 -1
  444. package/dist/elements/chunks/message-circle-CpXMM-_n.js +0 -1
  445. package/dist/elements/chunks/message-square-DxcWtETQ.js +0 -1
  446. package/dist/elements/chunks/message-tZmV2A3p.js +0 -1
  447. package/dist/elements/chunks/paperclip-CRZVp4z_.js +0 -1
  448. package/dist/elements/chunks/progress-bar-DNy66e_E.js +0 -1
  449. package/dist/elements/chunks/prompt-suggestion-Bn64Nl7L.js +0 -1
  450. package/dist/elements/chunks/reasoning-Emn9N4-T.js +0 -1
  451. package/dist/elements/chunks/rotate-cw-CPWe37D9.js +0 -1
  452. package/dist/elements/chunks/scroll-area-ClZkZxIk.js +0 -1
  453. package/dist/elements/chunks/scroll-button-DPE-t3x_.js +0 -1
  454. package/dist/elements/chunks/share-CyfnNEqp.js +0 -1
  455. package/dist/elements/chunks/source-UWCvmVak.js +0 -1
  456. package/dist/elements/chunks/star-DoHFAikQ.js +0 -1
  457. package/dist/elements/chunks/svelte--5p79yCD.js +0 -1
  458. package/dist/elements/chunks/tasks-card-CmZ5xh6v.js +0 -1
  459. package/dist/elements/chunks/text-shimmer-DYn09zrz.js +0 -1
  460. package/dist/elements/chunks/textarea-skiGHURM.js +0 -1
  461. package/dist/elements/chunks/toast-store-CFnQpThX.js +0 -1
  462. package/dist/elements/chunks/tsx-B8rCNbgL.js +0 -1
  463. package/dist/elements/chunks/typescript-RycA9KXf.js +0 -1
  464. package/dist/elements/chunks/use-card-resolution-DoA2f_EU.js +0 -1
  465. package/dist/elements/chunks/vue-BmIZj4XD.js +0 -1
  466. package/dist/elements/chunks/x-B6UnOiLw.js +0 -1
  467. package/dist/llms/llms-full.txt +0 -2949
  468. package/dist/llms/llms.txt +0 -156
  469. package/dist/register-impl-DpE7icIb.js +0 -293
  470. package/dist/ui/stat.d.ts +0 -16
  471. package/src/ui/stat.tsx +0 -41
  472. /package/dist/{elements/chunks/card-tags-D8lZ-C_U.js → card-tags-D8lZ-C_U.js} +0 -0
  473. /package/dist/{elements/chunks/link-preview-DNILK391.js → link-preview-DNILK391.js} +0 -0
  474. /package/dist/{elements/chunks/slots-DaNjX0rc.js → slots-DaNjX0rc.js} +0 -0
package/src/ui/dock.tsx CHANGED
@@ -42,6 +42,19 @@ export interface DockProps {
42
42
  unread?: boolean;
43
43
  /** Disable the launcher. */
44
44
  disabled?: boolean;
45
+ /** Suppress the dock's own built-in mobile `[part="close"]` X. Off (rendered) by
46
+ * default — the built-in X is the FALLBACK close route for panel content with no
47
+ * header of its own, which is the general case the dock has to cover since it
48
+ * never reads what it holds. Set `true` when the slotted/composed content
49
+ * supplies its own close affordance in its own header row (e.g. `ChatThread`'s
50
+ * `headerEndContent`) — with both present the two X's stack, one floating over
51
+ * the other's row, which is exactly the "doesn't look intentional" feedback this
52
+ * prop exists to let a caller avoid. TRADEOFF: the mobile panel reserves a
53
+ * padding band above its content so the built-in X never paints over it; that
54
+ * band stays reserved unless you set `hideClose` true, so only set it once your
55
+ * own control is actually in place — otherwise you get the band with no X to
56
+ * justify it. */
57
+ hideClose?: boolean;
45
58
  /** Where focus lands on open. Defaults to `content`. */
46
59
  focusOnOpen?: DockFocusOnOpen;
47
60
  /** Controlled open state. When set, the component never changes it itself — drive it
@@ -139,7 +152,12 @@ const DOCK_CSS = `
139
152
  /* ABOVE the panel, always. At <=480px the panel goes fixed/inset:0 and it comes
140
153
  after the launcher in DOM order, so at z-index:auto it painted OVER the launcher
141
154
  and a touch user lost every pointer route to closing. Both reference
142
- implementations stacked the launcher over the panel for the same reason. */
155
+ implementations stacked the launcher over the panel for the same reason. The
156
+ launcher itself is now HIDDEN while the panel is open at that width (see the
157
+ <=480px block below) in favour of the in-panel [part="close"] X, but this
158
+ z-index still matters for the moment between "open" landing and the panel's
159
+ own transition finishing, and for any consumer CSS that reintroduces the
160
+ launcher there. */
143
161
  z-index: 1;
144
162
  flex: none;
145
163
  display: inline-flex;
@@ -168,6 +186,58 @@ const DOCK_CSS = `
168
186
  [data-kai-dock] [part="launcher"]:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; }
169
187
  [data-kai-dock] [part="launcher"]:disabled { opacity: 0.55; cursor: not-allowed; }
170
188
 
189
+ /* The mobile-only close affordance rendered INSIDE the panel. Hidden by default and
190
+ only switched on inside the <=480px media block below, alongside the rule that
191
+ hides the launcher while the panel is open at that width — same query drives both,
192
+ CSS-only, no JS viewport logic. Absolutely positioned against the panel, which is
193
+ the panel's own containing block once the narrow-viewport rule below sets it to
194
+ position: fixed.
195
+
196
+ The inset is a CONSUMER-OVERRIDABLE TOKEN, same idiom as every other geometry
197
+ value in this file (--kai-dock-width and friends). The dock is content-agnostic —
198
+ it never reads what is slotted into the panel — so it cannot know whether that
199
+ content renders its own trailing controls (a ChatThread header-end slot with
200
+ share/settings icons, say) in the same top-right corner.
201
+
202
+ The mobile block below reserves a BAND of the panel's own padding above whatever
203
+ is slotted, sized to this button's own footprint, so nothing paints under this X
204
+ by default -- verified fix (round 1) against a real ChatThread header-end slot
205
+ colliding with it at 375px. Owner feedback on the real widget (round 2): for a
206
+ construct with only a title in the header, that band read as a dead empty strip
207
+ with the X floating alone in it -- "that doesn't look like the empty component
208
+ either, lets do better." The actual fix composes instead of reserving space:
209
+ ChatThread now has its own headerEndContent escape hatch (see its prop doc) so a
210
+ caller puts the close control INSIDE the header row, sharing it with the title --
211
+ zero collision by construction, no band needed. hideClose on this component (see
212
+ its prop doc) then suppresses this built-in X for exactly that case, and the
213
+ band above is scoped to :not([data-hide-close]) so it comes off ONLY there --
214
+ every other consumer (anyone who hasn't opted into hideClose, including a
215
+ hand-authored dock with real header-end content) keeps the band, same as round 1
216
+ shipped, so the collision it fixed stays fixed for them. What remains here for
217
+ that default case is a plain absolute overlay, top-right of the panel, with
218
+ these consumer-overridable inset tokens for the rare case it should deliberately
219
+ sit in front of something (e.g. a single small badge). */
220
+ [data-kai-dock] [part="close"] {
221
+ display: none;
222
+ position: absolute;
223
+ inset-block-start: var(--kai-dock-close-inset-block, 0.75rem);
224
+ inset-inline-end: var(--kai-dock-close-inset-inline, 0.75rem);
225
+ z-index: 1;
226
+ align-items: center;
227
+ justify-content: center;
228
+ inline-size: 2.25rem;
229
+ block-size: 2.25rem;
230
+ padding: 0;
231
+ border: 0;
232
+ border-radius: 9999px;
233
+ background: var(--color-background);
234
+ color: var(--color-foreground);
235
+ box-shadow: 0 2px 8px -2px rgb(0 0 0 / 0.3);
236
+ cursor: pointer;
237
+ }
238
+ [data-kai-dock] [part="close"]:hover { background: var(--color-muted, var(--color-background)); }
239
+ [data-kai-dock] [part="close"]:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; }
240
+
171
241
  [data-kai-dock] [part="badge"] {
172
242
  position: absolute;
173
243
  inset-block-start: 3px;
@@ -222,10 +292,12 @@ const DOCK_CSS = `
222
292
  [data-kai-dock][data-position^="top"] [part="panel"][data-closed] { transform: translateY(-0.5rem); }
223
293
 
224
294
  /* Narrow viewports take the panel full-bleed. A SHIPPED DEFAULT, not a prop: both
225
- references wrote this rule independently, which makes it a HOW. The launcher stays
226
- in its corner and ON TOP of the full-bleed panel that is the z-index: 1 in the
227
- launcher rule above, not anything in this block, and without it a phone user has no
228
- pointer route to closing at all (Escape alone strands exactly the touch user). */
295
+ references wrote this rule independently, which makes it a HOW. While the panel is
296
+ OPEN at this width the launcher hides and the panel's own [part="close"] X becomes
297
+ the pointer close route instead a FAB floating over a full-bleed panel read as
298
+ wrong (owner feedback) and Escape alone strands a touch user with no visible
299
+ affordance at all. Closed, the launcher is unaffected: it is the panel's own
300
+ [data-expanded] state that gates both rules below, not the media query alone. */
229
301
  @media (max-width: 480px) {
230
302
  [data-kai-dock] [part="panel"] {
231
303
  position: fixed;
@@ -237,6 +309,41 @@ const DOCK_CSS = `
237
309
  border: 0;
238
310
  border-radius: 0;
239
311
  }
312
+
313
+ /* Reserve a band for the built-in [part="close"] X ABOVE whatever is slotted,
314
+ rather than overlaying it on top of the panel's own content — the fix round 1
315
+ collision protection (verified against a real ChatThread header-end slot at
316
+ 375px: without this, the X painted directly over trailing icon buttons).
317
+ DERIVED from the button's own inset + size tokens, not a second hand-typed
318
+ number.
319
+
320
+ SCOPED to :not([data-hide-close]) — round 2 (owner feedback: the band read as
321
+ a dead strip when nothing but the X occupied it) removed this UNCONDITIONALLY
322
+ and reintroduced the very collision round 1 fixed, for every consumer who
323
+ never opted into hideClose. hideClose is only true for a caller that supplies
324
+ its OWN close control in its own header row (ChatThread's headerEndContent is
325
+ the one that does today) — the built-in X and this band both come off
326
+ together for that case, since there is nothing left for the band to protect.
327
+ Every other consumer -- including a hand-authored kai-dock with real
328
+ slot="header-end" content and no opinion on hideClose -- keeps the band, the
329
+ same as round 1 shipped. */
330
+ [data-kai-dock]:not([data-hide-close]) [part="panel"] {
331
+ padding-block-start: calc(2 * var(--kai-dock-close-inset-block, 0.75rem) + 2.25rem);
332
+ }
333
+
334
+ /* The full-bleed panel gets its own close affordance instead of relying on the
335
+ launcher, which is hidden below while the panel is open at this width — a FAB
336
+ floating over a full-bleed panel with no other visible close route is exactly
337
+ what the owner flagged. Escape and the X both still work; the launcher comes
338
+ back the instant the panel closes because data-expanded is gone by then.
339
+ Absolutely positioned, so the padding-block-start above (a content-box
340
+ concern) does not shift IT — only the in-flow slotted content. Content that
341
+ supplies its own close control passes hideClose, which drops the button from
342
+ the render tree entirely (see the component body) rather than being gated
343
+ here — so this rule stays a plain, unconditional width-query switch; the band
344
+ above is what actually varies with hideClose. */
345
+ [data-kai-dock] [part="close"] { display: inline-flex; }
346
+ [data-kai-dock]:has([part="panel"][data-expanded]) [part="launcher"] { display: none; }
240
347
  }
241
348
 
242
349
  @media (prefers-reduced-motion: reduce) {
@@ -310,6 +417,15 @@ function isWithin(root: Node, node: Node | null): boolean {
310
417
  * own menus; swallowing a key from a page you are a guest on is a decide-quietly move.
311
418
  * - **No focus trap**, deliberately, unlike `kai-dialog`. The page staying usable is
312
419
  * the whole point of "docked", which is also why the panel is `aria-modal="false"`.
420
+ * - **Mobile close route.** At <=480px the panel goes full-bleed and the launcher
421
+ * hides while it is open — a floating launcher over a full-bleed panel is not an
422
+ * obvious close affordance. The panel gets its own `[part="close"]` X, top-right,
423
+ * CSS-gated by the SAME media query and `[data-expanded]`, so it needs no viewport
424
+ * JS. Desktop is unaffected: the launcher keeps toggling and no X ever renders.
425
+ * This is the FALLBACK route, for panel content with no header of its own to hold
426
+ * a close control — content that does (a `ChatThread` using `headerEndContent`,
427
+ * say) passes `hideClose` to drop this X instead of stacking a second one over
428
+ * its own header row.
313
429
  *
314
430
  * WHAT IT REFUSES: it never aborts a request, never persists its own open state, never
315
431
  * decides what "unread" means, and never clears `unread` on open — writing over a
@@ -393,7 +509,17 @@ export function Dock(props: DockProps) {
393
509
  return (
394
510
  <>
395
511
  <style>{DOCK_CSS}</style>
396
- <div data-kai-dock data-position={props.position ?? 'bottom-end'} onKeyDown={onKeyDown}>
512
+ <div
513
+ data-kai-dock
514
+ data-position={props.position ?? 'bottom-end'}
515
+ // Drives the mobile panel's reserved-band rule below: present only when the
516
+ // built-in [part="close"] X is ACTUALLY suppressed (hideClose true), so the
517
+ // band stays on for every consumer who hasn't opted into their own close
518
+ // control — see the [part="close"]/reserved-band comments for why removing
519
+ // it unconditionally regressed the collision it existed to prevent.
520
+ bool:data-hide-close={!!props.hideClose}
521
+ onKeyDown={onKeyDown}
522
+ >
397
523
  <button
398
524
  ref={(el) => { launcher = el; props.launcherRef?.(el); }}
399
525
  part="launcher"
@@ -437,6 +563,19 @@ export function Dock(props: DockProps) {
437
563
  'pointer-events': isOpen() ? 'auto' : 'none',
438
564
  }}
439
565
  >
566
+ {/* Mobile-only (see the <=480px rule in DOCK_CSS): the full-bleed panel's
567
+ own close affordance, since the launcher is hidden by the same query
568
+ while the panel is open. Reuses the launcher's derived close name
569
+ rather than a new prop — same string, same i18n override. Dropped
570
+ from the render tree entirely (not just hidden) when `hideClose` is
571
+ set — the panel content supplies its own, in its own header, and a
572
+ second X floating over that row is the exact "doesn't look
573
+ intentional" feedback `hideClose` exists to avoid. */}
574
+ <Show when={!props.hideClose}>
575
+ <button type="button" part="close" aria-label={launcherName()} onClick={() => setOpen(false)}>
576
+ <DockCloseGlyph />
577
+ </button>
578
+ </Show>
440
579
  {props.children}
441
580
  </div>
442
581
  </div>
package/src/ui/icon.tsx CHANGED
@@ -9,6 +9,16 @@ import {
9
9
  Archive, ArrowUp, Bookmark, Copy, Ellipsis, ExternalLink, Flag,
10
10
  GitBranch, GitPullRequest, ListFilter, RotateCw,
11
11
  Moon, Sun,
12
+ // Device / viewport glyphs.
13
+ Tablet, Smartphone, Laptop,
14
+ // Direction glyphs completing the chevron + arrow quartets.
15
+ ChevronRight, ChevronUp, ArrowRight, ArrowDown, PanelRight,
16
+ // Status glyphs (kai-notice / kai-toast / kai-tool render these already).
17
+ Check, Info, TriangleAlert, CircleCheck, CircleAlert, CircleX,
18
+ // Action glyphs the kit performs but could not be named.
19
+ Trash2, Download, Upload, Link, RotateCcw,
20
+ // Pane / media chrome.
21
+ Maximize2, Minimize2, Eye, EyeOff, Bell, Square, Minus, Play,
12
22
  } from 'lucide-solid';
13
23
 
14
24
  type IconComponent = Component<{ class?: string }>;
@@ -68,8 +78,51 @@ const NAMED_ICONS: Record<string, IconComponent> = {
68
78
  // Theme / appearance glyphs.
69
79
  moon: Moon,
70
80
  sun: Sun,
81
+ // Device / viewport glyphs. `desktop` and `mobile` are aliases onto the same
82
+ // components as `monitor`/`smartphone` — lucide's names are not the words a
83
+ // consumer reaches for, and an alias costs nothing in the bundle.
84
+ desktop: Monitor,
85
+ laptop: Laptop,
86
+ tablet: Tablet,
87
+ smartphone: Smartphone,
88
+ mobile: Smartphone,
89
+ // Direction glyphs — chevron-down/left and arrow-left/up were here without
90
+ // their opposites; both quartets are now complete.
91
+ 'chevron-right': ChevronRight,
92
+ 'chevron-up': ChevronUp,
93
+ 'arrow-right': ArrowRight,
94
+ 'arrow-down': ArrowDown,
95
+ 'panel-right': PanelRight,
96
+ // Status glyphs. kai-notice / kai-toast / kai-tool already paint these, so a
97
+ // host building matching UI needs to be able to name them.
98
+ check: Check,
99
+ info: Info,
100
+ 'triangle-alert': TriangleAlert,
101
+ 'circle-check': CircleCheck,
102
+ 'circle-alert': CircleAlert,
103
+ 'circle-x': CircleX,
104
+ // Action glyphs. `trash`/`download`/`link` match the keys action-icons.ts
105
+ // already uses for message actions; `upload` is download's missing sibling.
106
+ trash: Trash2,
107
+ download: Download,
108
+ upload: Upload,
109
+ link: Link,
110
+ 'rotate-ccw': RotateCcw,
111
+ // Pane / media chrome (kai-pane, kai-artifact, kai-embed, kai-agent-card).
112
+ 'maximize-2': Maximize2,
113
+ 'minimize-2': Minimize2,
114
+ eye: Eye,
115
+ 'eye-off': EyeOff,
116
+ bell: Bell,
117
+ square: Square,
118
+ minus: Minus,
119
+ play: Play,
71
120
  };
72
121
 
122
+ /** Every curated icon name, sorted. Derived from `NAMED_ICONS` so a gallery,
123
+ * a story control or a test can never drift from the map itself. */
124
+ export const ICON_NAMES: readonly string[] = Object.keys(NAMED_ICONS).sort();
125
+
73
126
  /** Render an item icon.
74
127
  *
75
128
  * Resolution order:
package/src/ui/input.tsx CHANGED
@@ -1,5 +1,13 @@
1
- import { type JSX, Show, splitProps, createUniqueId } from 'solid-js';
1
+ import { type JSX, Show, splitProps, createUniqueId, createEffect, onCleanup, untrack } from 'solid-js';
2
2
  import { cn } from '../utils/cn';
3
+ import type { CaseMode } from '../primitives/field-mask';
4
+ import { fieldSemantics, type FieldSemanticType } from '../primitives/field-semantics';
5
+ import {
6
+ createInputMask,
7
+ type CopyPolicy,
8
+ type InputMask,
9
+ type InputMaskRejectReason,
10
+ } from '../primitives/input-mask';
3
11
 
4
12
  export interface InputProps extends Omit<JSX.InputHTMLAttributes<HTMLInputElement>, 'onInput' | 'onChange' | 'size'> {
5
13
  /** Field label rendered above the control and linked via `for`/`id`. */
@@ -16,12 +24,58 @@ export interface InputProps extends Omit<JSX.InputHTMLAttributes<HTMLInputElemen
16
24
  leading?: JSX.Element;
17
25
  /** Trailing affix (icon, inline button). Rendered inside the field row, after the input. */
18
26
  trailing?: JSX.Element;
19
- /** Fires per keystroke with the current value. */
27
+ /** Fires per keystroke with the current value — the CANONICAL value when a mask is
28
+ * active (digits for `tel`/`ssn`/`credit-card`, the formatted text for
29
+ * `custom`), and the raw text of the field otherwise. */
20
30
  onValueInput?: (value: string) => void;
21
- /** Fires on commit (blur) with the current value. */
31
+ /** Fires on commit (blur) with the current value; canonical when a mask is active. */
22
32
  onValueChange?: (value: string) => void;
33
+
34
+ // --- Form-field formats (spec §7.2). All five are SCALARS, which is what lets them
35
+ // survive as HTML attributes on the `<kai-input>` facade. Absent `format` AND absent
36
+ // `semantic` is the behavior of today, byte for byte (owner decision 1 / spec §1.1): no mask,
37
+ // no extra attributes, nothing.
38
+
39
+ /** Tier-2 mask pattern: `#` a digit, `@` an alphanumeric, `*` an obscurable
40
+ * alphanumeric, everything else a positional literal (`@@@-####`).
41
+ *
42
+ * The literal `default` is the OPT-IN sentinel: it resolves to the default format of
43
+ * `semantic` (`tel` → `###-###-####`). It cannot collide with a real pattern — `default`
44
+ * as a format is eight literals with no fill position, i.e. a field that can hold
45
+ * nothing — and an opt-in token is needed because decision 1 forbids a bare semantic
46
+ * type from starting to mask on its own. */
47
+ format?: string;
48
+ /** The placeholder guide shown at unfilled positions, aligned position-for-position
49
+ * with `format` (` - - `, `mm/dd/yyyy`). Without it the field shows only up
50
+ * to the last typed character. */
51
+ guide?: string;
52
+ /** Tier-1 semantic type. On its own it applies `inputmode` / `autocomplete` /
53
+ * `spellcheck` / `autocorrect` / `autocapitalize` and decides the canonical value —
54
+ * it never starts masking by itself. */
55
+ semantic?: FieldSemanticType;
56
+ /** Case folding applied to typed and pasted text. Defaults to `preserve`. */
57
+ caseMode?: CaseMode;
58
+ /** What a copy or cut of a masked field puts on the clipboard. Defaults to `canonical`. */
59
+ copyPolicy?: CopyPolicy;
60
+ /** A mask refused, or partly refused, some content — `full`, `wrong-class`,
61
+ * `over-capacity`, or `format-change-clipped`. Not a scalar and therefore not an
62
+ * attribute: the facade projects this onto its `kai-input-rejected` event.
63
+ *
64
+ * NOT an error state. `format-change-clipped` in particular fires on a routine
65
+ * reactive `format` change, so this widget deliberately does not touch `invalid` —
66
+ * validity belongs to the consumer, and stays there. */
67
+ onMaskReject?: (detail: { reason: InputMaskRejectReason; data: string }) => void;
23
68
  }
24
69
 
70
+ /** The `format` value that means: resolve the default format of the semantic type.
71
+ *
72
+ * EXPORTED because `src/elements/input.tsx` has to recognise the same sentinel when it
73
+ * resolves a seeded value ahead of the masker, and a restated literal there would fail
74
+ * SILENTLY if this string ever changed: `format === 'default'` would simply go false and
75
+ * `compileMask('default')` would compile seven literals, garbling the seed with no error
76
+ * anywhere. One exported constant instead of two copies of a magic string. */
77
+ export const DEFAULT_FORMAT = 'default';
78
+
25
79
  // The single source of the field shell styling — lifted verbatim from the
26
80
  // `inputBase` constant that used to live in `src/components/form-widgets.tsx`.
27
81
  // `Input` now owns it; the form widgets render `Input` rather than re-pasting it.
@@ -37,7 +91,12 @@ const ROW_INPUT =
37
91
  'min-w-0 flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed';
38
92
 
39
93
  const SIZE_SM = 'px-2.5 py-1';
40
- const INVALID = 'border-destructive dark:border-red-400/70';
94
+ // The invalid-state border, EXPORTED because `src/ui/select.tsx` renders the same
95
+ // field box and a second hand-typed copy of this string is exactly the kind of
96
+ // restatement that rots (`docs/coupling-map.md` §4). `INVALID` stays as the local
97
+ // alias so the three call sites below read unchanged.
98
+ export const FIELD_INVALID = 'border-destructive dark:border-red-400/70';
99
+ const INVALID = FIELD_INVALID;
41
100
 
42
101
  // Suppress the native search affordances Chrome/WebKit render for `type="search"`.
43
102
  // Without this the browser's `::-webkit-search-cancel-button` (×) stacks on top of
@@ -61,6 +120,7 @@ export function Input(props: InputProps): JSX.Element {
61
120
  const [local, rest] = splitProps(props, [
62
121
  'class', 'label', 'hint', 'error', 'size', 'invalid',
63
122
  'leading', 'trailing', 'onValueInput', 'onValueChange', 'onBlur', 'id', 'disabled',
123
+ 'format', 'guide', 'semantic', 'caseMode', 'copyPolicy', 'onMaskReject',
64
124
  ]);
65
125
 
66
126
  const id = local.id ?? createUniqueId();
@@ -69,6 +129,138 @@ export function Input(props: InputProps): JSX.Element {
69
129
  const hasHint = () => !!local.error || !!local.hint;
70
130
  const hasAffix = () => local.leading != null || local.trailing != null;
71
131
 
132
+ // -------------------------------------------------------------------------------
133
+ // Form-field formats (spec §7.2). Tier 1 is a handful of attributes; tier 2 is one
134
+ // `createInputMask` bound to whichever `<input>` node is currently mounted.
135
+ // -------------------------------------------------------------------------------
136
+
137
+ const semantics = () => (local.semantic === undefined ? undefined : fieldSemantics(local.semantic));
138
+
139
+ // Both warn paths below run from a TRACKED accessor or from a reconfigure, i.e. from a
140
+ // routine reactive render, and both interpolate a value that reaches this widget from
141
+ // the form card and therefore from MODEL output. So: report each distinct bad value
142
+ // ONCE per widget, and clip what gets interpolated. The console is not a sink — this is
143
+ // about a 500-character pattern printing 500 characters on every keystroke, not about
144
+ // injection.
145
+ const warned = new Set<string>();
146
+ const warnOnce = (key: string, message: string): void => {
147
+ if (warned.has(key)) return;
148
+ warned.add(key);
149
+ console.warn(message);
150
+ };
151
+ const clip = (text: string): string =>
152
+ text.length <= 32 ? text : `${text.slice(0, 32)}… (${text.length} chars)`;
153
+
154
+ /** The pattern to mask with, or `undefined` for no mask at all. */
155
+ const maskFormat = (): string | undefined => {
156
+ const format = local.format;
157
+ if (format === undefined || format === '') return undefined;
158
+ if (format !== DEFAULT_FORMAT) return format;
159
+ const resolved = semantics()?.defaultFormat;
160
+ if (resolved === undefined) {
161
+ // Loud, never a silent no-mask: the consumer asked for a mask and is not getting
162
+ // one. `custom` has no default by design (its `format` IS the pattern), and a
163
+ // semantic type that never arrived is the more likely mistake.
164
+ warnOnce(
165
+ `default:${String(local.semantic)}`,
166
+ `Input: format=${DEFAULT_FORMAT} resolves the default format of a semantic type, and ` +
167
+ `semantic=${JSON.stringify(local.semantic)} has none. Rendering unmasked.`,
168
+ );
169
+ }
170
+ return resolved;
171
+ };
172
+
173
+ let plainEl: HTMLInputElement | undefined;
174
+ let rowEl: HTMLInputElement | undefined;
175
+ let mask: InputMask | undefined;
176
+ /** The node `mask` is bound to. The affix toggle is the one thing that changes it. */
177
+ let maskedEl: HTMLInputElement | undefined;
178
+
179
+ const detachMask = (): void => {
180
+ mask?.detach();
181
+ mask = undefined;
182
+ maskedEl = undefined;
183
+ };
184
+
185
+ const warnBadFormat = (format: string, err: unknown, kept: boolean): void => {
186
+ const reason = err instanceof Error ? err.message : String(err);
187
+ warnOnce(
188
+ `format:${format}`,
189
+ `Input: format ${JSON.stringify(clip(format))} did not compile (${reason}); ` +
190
+ (kept ? 'the field keeps its previous format.' : 'rendering unmasked.'),
191
+ );
192
+ };
193
+
194
+ createEffect(() => {
195
+ // Everything reactive is read HERE, so the effect re-runs on a config change or an
196
+ // affix toggle and on nothing else.
197
+ const format = maskFormat();
198
+ const config = {
199
+ guide: local.guide,
200
+ semantic: local.semantic,
201
+ caseMode: local.caseMode,
202
+ copyPolicy: local.copyPolicy,
203
+ };
204
+ // WHICH NODE IS MOUNTED. `Input` caches two `<input>`s and `<Show>` swaps between
205
+ // them when a leading/trailing affix appears or disappears — the one legitimate node
206
+ // change in this file (spec §8.1). The `ref` of a node fires only when it is BUILT, and
207
+ // the cached node is built once, so a toggle back to an existing node notifies
208
+ // nobody. Re-attachment therefore has to be explicit, and this read is what makes it
209
+ // happen. Everything else about this widget is pinned to keeping the SAME node alive
210
+ // (`tests/ui/input-node-identity.test.tsx`), which is the only reason a long-lived
211
+ // masker on it is safe at all.
212
+ //
213
+ // THE ASSUMPTION THIS RELIES ON, stated: `plainEl`/`rowEl` are plain mutable refs, so
214
+ // this reads a node only because Solid runs the `<Show>` render effect that BUILDS it
215
+ // (and its `ref`) before this user effect. If that ever stopped holding, `el` would be
216
+ // `undefined` on the toggle and the branch below would detach with nothing queued to
217
+ // re-attach — silently. The affix tests in the node-identity file are what would catch it.
218
+ const el = hasAffix() ? rowEl : plainEl;
219
+
220
+ untrack(() => {
221
+ if (format === undefined || el === undefined) {
222
+ detachMask();
223
+ return;
224
+ }
225
+
226
+ if (mask !== undefined && maskedEl === el) {
227
+ // Same node, new configuration: RE-CONFIGURE. Re-creating would drop the undo
228
+ // stack and re-seed from the DOM text. `update()` is atomic — a format that
229
+ // fails to compile leaves the masker exactly as it was — so a half-typed
230
+ // reactive pattern cannot corrupt the field.
231
+ try {
232
+ mask.update({ format, ...config });
233
+ } catch (err) {
234
+ warnBadFormat(format, err, true);
235
+ }
236
+ return;
237
+ }
238
+
239
+ // First attach, or the affix toggle. Carry the value across: the incoming node is
240
+ // a different element and knows nothing about what the user typed into the old one.
241
+ const carried = mask?.getFormattedValue() ?? el.value;
242
+ detachMask();
243
+ try {
244
+ mask = createInputMask(el, {
245
+ ...config,
246
+ format,
247
+ initialValue: carried,
248
+ // The callback of the masker, not the DOM `input` event, is the emission point: a
249
+ // canceled `beforeinput` means a real browser never fires `input` at all for a
250
+ // typed character.
251
+ onInput: ({ canonical }) => local.onValueInput?.(canonical),
252
+ onReject: (detail) => local.onMaskReject?.(detail),
253
+ });
254
+ maskedEl = el;
255
+ } catch (err) {
256
+ // A bad pattern falls back to a plain text field, loudly (spec §7.3).
257
+ warnBadFormat(format, err, false);
258
+ }
259
+ });
260
+ });
261
+
262
+ onCleanup(detachMask);
263
+
72
264
  // The class arrives as an ACCESSOR, not a string, and this is load-bearing.
73
265
  //
74
266
  // Solid evaluates a `<Show>` `fallback` inside the Show's own memo. When the
@@ -83,20 +275,57 @@ export function Input(props: InputProps): JSX.Element {
83
275
  // Solid compiles it into a nested effect that sets the attribute on the
84
276
  // EXISTING node. Same reason the affix branch was always fine: it inserts the
85
277
  // input through a function. Pinned by `tests/ui/input-node-identity.test.tsx`.
86
- const inputEl = (cls: () => string, part: string): JSX.Element => (
278
+ // Tier-1 attributes (spec §2). Each is a DEFAULT the semantic type supplies: an
279
+ // explicit prop always wins, because the consumer knows something the enum does not.
280
+ // With no `semantic` every one of these is `undefined` — the attribute is simply not
281
+ // set, which is what byte-for-byte parity with the behavior of today means here.
282
+ const inputmodeAttr = () => rest.inputmode ?? semantics()?.inputmode;
283
+ const autocompleteAttr = () =>
284
+ (rest.autocomplete ?? semantics()?.autocomplete) as JSX.InputHTMLAttributes<HTMLInputElement>['autocomplete'];
285
+ const spellcheckAttr = () => {
286
+ const explicit = rest.spellcheck;
287
+ // Normalized to the attribute spelling so both forms land as an attribute rather
288
+ // than one of them as a property: `spellcheck={false}` is a real value, not an
289
+ // absent one, and it must stay visible in the DOM.
290
+ if (explicit !== undefined) return explicit === true || explicit === 'true' ? 'true' : 'false';
291
+ const semantic = semantics();
292
+ if (semantic === undefined) return undefined;
293
+ return semantic.spellcheck ? 'true' : 'false';
294
+ };
295
+ const autocorrectAttr = () => rest.autocorrect ?? semantics()?.autocorrect;
296
+ const autocapitalizeAttr = () => rest.autocapitalize ?? semantics()?.autocapitalize;
297
+
298
+ const inputEl = (
299
+ cls: () => string,
300
+ part: string,
301
+ register: (el: HTMLInputElement) => void,
302
+ ): JSX.Element => (
87
303
  <input
88
304
  {...rest}
305
+ ref={register}
89
306
  id={id}
90
307
  part={part}
91
308
  disabled={local.disabled}
92
309
  aria-invalid={isInvalid() ? 'true' : undefined}
93
310
  aria-describedby={hasHint() ? hintId : undefined}
311
+ inputmode={inputmodeAttr()}
312
+ autocomplete={autocompleteAttr()}
313
+ spellcheck={spellcheckAttr()}
314
+ autocorrect={autocorrectAttr()}
315
+ autocapitalize={autocapitalizeAttr()}
94
316
  class={cn(SEARCH_RESET, cls())}
95
- onInput={(e) => local.onValueInput?.(e.currentTarget.value)}
317
+ // When a mask is active the masker owns the emission (see its `onInput` above) and
318
+ // this handler stands down — otherwise a jsdom-style `input` event, which reaches
319
+ // both, would emit twice, and the raw text it reads is the FORMATTED value rather
320
+ // than the canonical one.
321
+ onInput={(e) => {
322
+ if (mask !== undefined) return;
323
+ local.onValueInput?.(e.currentTarget.value);
324
+ }}
96
325
  onBlur={(e) => {
97
326
  const handler = local.onBlur;
98
327
  if (typeof handler === 'function') handler(e);
99
- local.onValueChange?.(e.currentTarget.value);
328
+ local.onValueChange?.(mask?.getCanonicalValue() ?? e.currentTarget.value);
100
329
  }}
101
330
  />
102
331
  );
@@ -107,10 +336,21 @@ export function Input(props: InputProps): JSX.Element {
107
336
  // Created on first use and then REUSED, so toggling an affix on or off does
108
337
  // not discard a focused input either. The lazy cache also keeps the unused
109
338
  // branch's node (and its effects) from being built at all.
339
+ //
340
+ // KEEP PROSE IN THIS FILE FREE OF STRAY QUOTE CHARACTERS. The part-name guard in
341
+ // `src/elements/slots.test.ts` scans this source with a naive quote regex, so one
342
+ // apostrophe in a comment shifts its parity and swallows the part literals below —
343
+ // that guard then reddens over a comment, naming a part that is still right there.
344
+ //
345
+ // Each branch registers its node so the mask effect above can find whichever one is
346
+ // mounted; `ref` fires once per node, at construction, which is exactly when a node
347
+ // first becomes findable.
110
348
  let plainNode: JSX.Element;
111
- const plainInput = () => (plainNode ??= inputEl(fieldClass, 'field input'));
349
+ const plainInput = () =>
350
+ (plainNode ??= inputEl(fieldClass, 'field input', (el) => (plainEl = el)));
112
351
  let rowNode: JSX.Element;
113
- const rowInput = () => (rowNode ??= inputEl(() => ROW_INPUT, 'input'));
352
+ const rowInput = () =>
353
+ (rowNode ??= inputEl(() => ROW_INPUT, 'input', (el) => (rowEl = el)));
114
354
 
115
355
  return (
116
356
  <div class="flex w-full flex-col gap-1.5">
package/src/ui/kbd.tsx CHANGED
@@ -70,7 +70,7 @@ function glyph(token: string, mac: boolean): string {
70
70
  }
71
71
 
72
72
  const SIZE: Record<'sm' | 'md', string> = {
73
- sm: 'h-5 min-w-5 px-1 text-[11px]',
73
+ sm: 'h-5 min-w-5 px-1 text-caption',
74
74
  md: 'h-6 min-w-6 px-1.5 text-xs',
75
75
  };
76
76
 
package/src/ui/nav.tsx CHANGED
@@ -221,7 +221,7 @@ function NavRows(props: NavRowsProps) {
221
221
  </span>
222
222
  </Show>
223
223
  <Show when={item.badge}>
224
- <Badge variant="default" class="px-1.5 py-0 text-[0.625rem] font-medium uppercase tracking-wide">{item.badge}</Badge>
224
+ <Badge variant="default" class="px-1.5 py-0 text-micro font-medium uppercase tracking-wide">{item.badge}</Badge>
225
225
  </Show>
226
226
  <Show when={item.trailing}>{renderIcon(item.trailing, { class: 'size-4 shrink-0 opacity-0 transition-opacity group-hover:opacity-60' })}</Show>
227
227
  </button>
@@ -201,7 +201,7 @@ export function PaneGroup(props: PaneGroupProps) {
201
201
  </Show>
202
202
  <span
203
203
  class={cn(
204
- 'relative flex size-[18px] items-center justify-center rounded text-[11px] font-bold leading-none tabular-nums',
204
+ 'relative flex size-[18px] items-center justify-center rounded text-caption font-bold leading-none tabular-nums',
205
205
  TONE_BG[tone()],
206
206
  TONE_BADGE_FG[tone()],
207
207
  )}
@@ -215,7 +215,7 @@ export function PaneGroup(props: PaneGroupProps) {
215
215
  <Show when={tab().status?.label}>
216
216
  <span
217
217
  class={cn(
218
- 'truncate text-[10px] font-medium',
218
+ 'truncate text-micro font-medium',
219
219
  TONE_TEXT[tone()],
220
220
  alwaysWord() ? 'inline' : 'hidden group-hover/tab:inline',
221
221
  )}