@kitn.ai/ui 0.21.0 → 0.22.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 (243) hide show
  1. package/dist/components/chat-thread.d.ts +9 -0
  2. package/dist/{create-tween-D3Y5qiij.js → create-tween-COnPwAQN.js} +1 -1
  3. package/dist/{create-tween-DGKY1FDY.js → create-tween-DmTUaXWE.js} +1 -1
  4. package/dist/{create-tween-paho5Rh-.js → create-tween-qs-jjvNA.js} +1 -1
  5. package/dist/custom-elements.json +43 -7
  6. package/dist/elements/agent-card.js +1 -1
  7. package/dist/elements/artifact.js +1 -1
  8. package/dist/elements/attachments.js +1 -1
  9. package/dist/elements/audio-visualizer.js +1 -1
  10. package/dist/elements/avatar.js +1 -1
  11. package/dist/elements/badge.js +1 -1
  12. package/dist/elements/button.js +1 -1
  13. package/dist/elements/card.js +1 -1
  14. package/dist/elements/cards.js +1 -1
  15. package/dist/elements/chain-of-thought.js +1 -1
  16. package/dist/elements/chat-scope-picker.js +1 -1
  17. package/dist/elements/chat-workspace.js +1 -1
  18. package/dist/elements/chat.js +1 -1
  19. package/dist/elements/checkpoint.js +1 -1
  20. package/dist/elements/choice.js +1 -1
  21. package/dist/elements/chunks/{Icon-C-2LvGDG.js → Icon-CHG5z5L7.js} +1 -1
  22. package/dist/elements/chunks/{action-icons-CaYoImEk.js → action-icons-BAtCEEgg.js} +1 -1
  23. package/dist/elements/chunks/arrow-left-CFPTeL1Y.js +1 -0
  24. package/dist/elements/chunks/{artifact-BBIscOxJ.js → artifact-fBZOE4SO.js} +1 -1
  25. package/dist/elements/chunks/{attachments-BB56zoji.js → attachments-DpYMe5VQ.js} +1 -1
  26. package/dist/elements/chunks/{audio-visualizer-BxOx4THz.js → audio-visualizer-DFwzAcuL.js} +1 -1
  27. package/dist/elements/chunks/{badge-SxLSZ19R.js → badge-D6zodm6E.js} +1 -1
  28. package/dist/elements/chunks/{button-CmkUkz6c.js → button-C42vrGvO.js} +1 -1
  29. package/dist/elements/chunks/{card-renderer-Db4ffkWv.js → card-renderer-DMZsSuvU.js} +1 -1
  30. package/dist/elements/chunks/{chat-thread-B3Ks-cKr.js → chat-thread-CNNEhol-.js} +1 -1
  31. package/dist/elements/chunks/check-7Q3K-iXE.js +1 -0
  32. package/dist/elements/chunks/chevron-down-CpNYLn7E.js +1 -0
  33. package/dist/elements/chunks/chevron-right-DrH85Fha.js +1 -0
  34. package/dist/elements/chunks/{choice-card-Q9cYYDyM.js → choice-card-YoWjpsFV.js} +1 -1
  35. package/dist/elements/chunks/circle-C9KRjKYv.js +1 -0
  36. package/dist/elements/chunks/circle-check-Dmi7Y4zw.js +1 -0
  37. package/dist/elements/chunks/{circle-x-D0MEfxOM.js → circle-x-QIxXda6k.js} +1 -1
  38. package/dist/elements/chunks/{code-block-C8VzAraE.js → code-block-CqeugReY.js} +1 -1
  39. package/dist/elements/chunks/{collapsible-Cvj61obE.js → collapsible-sTv-_5lF.js} +1 -1
  40. package/dist/elements/chunks/{composer-D86ztyL0.js → composer-ldQyeVEB.js} +1 -1
  41. package/dist/elements/chunks/{confirm-card-BdyDAGLw.js → confirm-card-C0RNOwfn.js} +1 -1
  42. package/dist/elements/chunks/context-5qgypNHC.js +1 -0
  43. package/dist/elements/chunks/conversation-list-DLzOacXz.js +1 -0
  44. package/dist/elements/chunks/{create-tween-C_A_B638.js → create-tween-V4TXZ93X.js} +1 -1
  45. package/dist/elements/chunks/default-input-DeeRSDHc.js +1 -0
  46. package/dist/elements/chunks/{define-BzPnNsXg.js → define-lE-QvQFn.js} +1 -1
  47. package/dist/elements/chunks/{disclosure-DVKsoc1F.js → disclosure-DZSa9jog.js} +1 -1
  48. package/dist/elements/chunks/{download-BjA54IMJ.js → download-B2ZmnGqW.js} +1 -1
  49. package/dist/elements/chunks/{dropdown-BjVVHWXH.js → dropdown-R3B7cSRE.js} +1 -1
  50. package/dist/elements/chunks/{ellipsis-BNedmqfi.js → ellipsis-BmxceY4l.js} +1 -1
  51. package/dist/elements/chunks/{embed-DQsl4XEq.js → embed-BVrtTZPo.js} +1 -1
  52. package/dist/elements/chunks/{external-link-Di-dw2CU.js → external-link-BbdR7YO7.js} +1 -1
  53. package/dist/elements/chunks/{file-text-B3sAODCV.js → file-text-D7dijo1A.js} +1 -1
  54. package/dist/elements/chunks/{file-tree-Dgto-bMn.js → file-tree-0upJ5jeb.js} +1 -1
  55. package/dist/elements/chunks/{folder-D6rMadFV.js → folder-D8z58t7B.js} +1 -1
  56. package/dist/elements/chunks/{form-CILxvhQ7.js → form-p5BKJNw5.js} +1 -1
  57. package/dist/elements/chunks/{hover-card-BtiF4XiU.js → hover-card-CN9s2md5.js} +1 -1
  58. package/dist/elements/chunks/{icon-DKkmze3y.js → icon-CNiKP37E.js} +1 -1
  59. package/dist/elements/chunks/{info-CWRjZkuy.js → info-BSnuJiHI.js} +1 -1
  60. package/dist/elements/chunks/{input--QJBK6BA.js → input-BauDmzxX.js} +1 -1
  61. package/dist/elements/chunks/{kbd-C3zuKh8R.js → kbd-BlHJaP9b.js} +1 -1
  62. package/dist/elements/chunks/{link-BL5JA7Q0.js → link-DEi5iUWC.js} +1 -1
  63. package/dist/elements/chunks/{link-preview-CU37bbXh.js → link-preview-BaHyM6W4.js} +1 -1
  64. package/dist/elements/chunks/{loader-XmpyxyRH.js → loader-CmJG_Fkq.js} +1 -1
  65. package/dist/elements/chunks/{markdown-3Gv5UZ_o.js → markdown-D2jaMpCN.js} +1 -1
  66. package/dist/elements/chunks/{message-X5NZbq-W.js → message-C5UfXZ2Z.js} +1 -1
  67. package/dist/elements/chunks/{message-CnBA2OUt.js → message-CXWLmmLn.js} +1 -1
  68. package/dist/elements/chunks/message-square-hXylwsy0.js +1 -0
  69. package/dist/elements/chunks/{minimize-2-DwyrUTcM.js → minimize-2-C9ytZFMU.js} +1 -1
  70. package/dist/elements/chunks/model-switcher-CivGfD7-.js +1 -0
  71. package/dist/elements/chunks/{overlay-Cepd-KcS.js → overlay-DJL_sbOm.js} +1 -1
  72. package/dist/elements/chunks/{paperclip-CdwKjrlu.js → paperclip-CFaB8naM.js} +1 -1
  73. package/dist/elements/chunks/{progress-bar-DMVXBD3b.js → progress-bar-Br5SksfR.js} +1 -1
  74. package/dist/elements/chunks/{prompt-suggestion-Di_WZE_5.js → prompt-suggestion-CMIvtAhU.js} +1 -1
  75. package/dist/elements/chunks/{reasoning-D0jMch5u.js → reasoning-DpZcNzLq.js} +1 -1
  76. package/dist/elements/chunks/{resizable-1znbL1Gv.js → resizable-Q8i3T9a_.js} +1 -1
  77. package/dist/elements/chunks/{rotate-cw-DVhhgx8n.js → rotate-cw-B5IHCBFV.js} +1 -1
  78. package/dist/elements/chunks/{scroll-area-Bs4bFS1P.js → scroll-area-eLeydBje.js} +1 -1
  79. package/dist/elements/chunks/{scroll-button-Bet5NLc4.js → scroll-button-DWbz0yA1.js} +1 -1
  80. package/dist/elements/chunks/{separator-C5Gh8qGC.js → separator-Xic78AD_.js} +1 -1
  81. package/dist/elements/chunks/{settings-5vmEFg0K.js → settings-B8H51pAx.js} +1 -1
  82. package/dist/elements/chunks/{settings-group-BWLB38kz.js → settings-group-CsG9eYLe.js} +1 -1
  83. package/dist/elements/chunks/{share-BZ3G_DaB.js → share-CNTx0z__.js} +1 -1
  84. package/dist/elements/chunks/{skeleton-DafPJQTp.js → skeleton-CLlHTcMI.js} +1 -1
  85. package/dist/elements/chunks/{source-D_qj_6uJ.js → source-BoVcU5ok.js} +1 -1
  86. package/dist/elements/chunks/{star-Cuv7j2IE.js → star-CxQVTVhI.js} +1 -1
  87. package/dist/elements/chunks/{store-C1StPHfW.js → store-CLtS5CUG.js} +1 -1
  88. package/dist/elements/chunks/{tasks-card-EyoBZfpA.js → tasks-card-CzlkLmUt.js} +1 -1
  89. package/dist/elements/chunks/{text-shimmer-D9gRSh34.js → text-shimmer-D5NhpIQ1.js} +1 -1
  90. package/dist/elements/chunks/{textarea-DuhtHfcm.js → textarea-x5Uxsmtm.js} +1 -1
  91. package/dist/elements/chunks/{thumbs-up-B9hwv43w.js → thumbs-up-UobcJnx9.js} +1 -1
  92. package/dist/elements/chunks/{toast-store-Bm3DUEDe.js → toast-store-BOfs9eXq.js} +1 -1
  93. package/dist/elements/chunks/{tool-CDtW7L0z.js → tool-Be6Dh_dt.js} +1 -1
  94. package/dist/elements/chunks/{tooltip-eB226mGK.js → tooltip-RxMxyCLF.js} +1 -1
  95. package/dist/elements/chunks/{triangle-alert-C1SNzTqK.js → triangle-alert-C4yxH2XN.js} +1 -1
  96. package/dist/elements/chunks/{use-card-resolution-YZh2MvI5.js → use-card-resolution-jva9ehH6.js} +1 -1
  97. package/dist/elements/chunks/{variant-aurora-Cj1F7D0l.js → variant-aurora-Dx4VRcjS.js} +1 -1
  98. package/dist/elements/chunks/{variant-custom-T9gpt7QE.js → variant-custom-Y1ZTWgEa.js} +1 -1
  99. package/dist/elements/chunks/{variant-wave-CxU9z-TE.js → variant-wave-B9gml9Cg.js} +1 -1
  100. package/dist/elements/chunks/{video-CK4R7Rus.js → video-CrPHsytT.js} +1 -1
  101. package/dist/elements/chunks/x-_-OPHept.js +1 -0
  102. package/dist/elements/coachmark.js +1 -1
  103. package/dist/elements/code-block.js +1 -1
  104. package/dist/elements/command.js +1 -1
  105. package/dist/elements/compare.js +1 -1
  106. package/dist/elements/composer.js +1 -1
  107. package/dist/elements/confirm-card.js +1 -1
  108. package/dist/elements/context-meter.js +1 -1
  109. package/dist/elements/conversation-list.js +1 -1
  110. package/dist/elements/default-input.d.ts +27 -0
  111. package/dist/elements/dialog.js +1 -1
  112. package/dist/elements/editable-label.js +1 -1
  113. package/dist/elements/embed.js +1 -1
  114. package/dist/elements/empty.js +1 -1
  115. package/dist/elements/feedback-bar.js +1 -1
  116. package/dist/elements/file-tree.js +1 -1
  117. package/dist/elements/file-upload.js +1 -1
  118. package/dist/elements/form.js +1 -1
  119. package/dist/elements/hover-card.js +1 -1
  120. package/dist/elements/icon.js +1 -1
  121. package/dist/elements/image.js +1 -1
  122. package/dist/elements/input.js +1 -1
  123. package/dist/elements/kbd.js +1 -1
  124. package/dist/elements/link-preview.js +1 -1
  125. package/dist/elements/loader.js +1 -1
  126. package/dist/elements/markdown.js +1 -1
  127. package/dist/elements/menu.js +1 -1
  128. package/dist/elements/message-skills.js +1 -1
  129. package/dist/elements/message.js +1 -1
  130. package/dist/elements/model-switcher.js +1 -1
  131. package/dist/elements/nav.js +1 -1
  132. package/dist/elements/notice.js +1 -1
  133. package/dist/elements/pane-group.js +1 -1
  134. package/dist/elements/pane.js +1 -1
  135. package/dist/elements/popover.js +1 -1
  136. package/dist/elements/progress-bar.js +1 -1
  137. package/dist/elements/prompt-dock.js +1 -1
  138. package/dist/elements/prompt-input.js +1 -1
  139. package/dist/elements/prompt-suggestions.js +1 -1
  140. package/dist/elements/reasoning.js +1 -1
  141. package/dist/elements/remote.js +1 -1
  142. package/dist/elements/resizable.js +1 -1
  143. package/dist/elements/response-stream.js +2 -2
  144. package/dist/elements/screen.js +1 -1
  145. package/dist/elements/scroll-area.js +1 -1
  146. package/dist/elements/scroll-button.js +1 -1
  147. package/dist/elements/search.js +1 -1
  148. package/dist/elements/segmented.js +1 -1
  149. package/dist/elements/separator.js +1 -1
  150. package/dist/elements/setting-item.js +1 -1
  151. package/dist/elements/settings-group.js +1 -1
  152. package/dist/elements/skeleton.js +1 -1
  153. package/dist/elements/slot-text.d.ts +51 -0
  154. package/dist/elements/slots.d.ts +5 -0
  155. package/dist/elements/source.js +1 -1
  156. package/dist/elements/status.js +1 -1
  157. package/dist/elements/switch.js +1 -1
  158. package/dist/elements/tabs.js +1 -1
  159. package/dist/elements/tasks.js +1 -1
  160. package/dist/elements/text-shimmer.js +1 -1
  161. package/dist/elements/thinking-bar.js +1 -1
  162. package/dist/elements/thread.js +1 -1
  163. package/dist/elements/toast.js +1 -1
  164. package/dist/elements/tool.js +1 -1
  165. package/dist/elements/tooltip.js +1 -1
  166. package/dist/elements/voice-input.js +1 -1
  167. package/dist/elements/voice-output.js +1 -1
  168. package/dist/elements.d.ts +12 -6
  169. package/dist/index.js +1535 -1487
  170. package/dist/index.server.js +2330 -2296
  171. package/dist/kai.es.js +1 -1
  172. package/dist/llms/llms-full.txt +12 -3
  173. package/dist/mcp.es.js +809 -75
  174. package/dist/react/index.d.ts +13 -3
  175. package/dist/react.js +14 -14
  176. package/dist/register-impl-AxIlqjo7.js +145 -0
  177. package/dist/{solid-BCJUQqwS.js → solid-BGYSJyHG.js} +4901 -4691
  178. package/dist/{solid-BdENBcEq.js → solid-Dt2nEwfl.js} +4406 -4320
  179. package/dist/solid.js +1 -1
  180. package/dist/solid.server.js +1 -1
  181. package/dist/{variant-aurora-DOb_o4pC.js → variant-aurora-B44X69AG.js} +1 -1
  182. package/dist/{variant-aurora-BKHHiOgL.js → variant-aurora-BRhV8Flk.js} +2 -2
  183. package/dist/{variant-aurora-1wMcXRUS.js → variant-aurora-Ds5ou9BH.js} +2 -2
  184. package/dist/{variant-custom-DWSSgmd2.js → variant-custom-B20FhtYj.js} +2 -2
  185. package/dist/{variant-custom-CJGB0Kbw.js → variant-custom-BvA1Schl.js} +1 -1
  186. package/dist/{variant-custom-Co0CkXJF.js → variant-custom-D6Rtvneb.js} +2 -2
  187. package/dist/{variant-wave-CEODq-9q.js → variant-wave-BUQtkryq.js} +1 -1
  188. package/dist/{variant-wave-C3-45cL6.js → variant-wave-BVYnqy9L.js} +2 -2
  189. package/dist/{variant-wave-jdXBnCUZ.js → variant-wave-DXC6_LcH.js} +2 -2
  190. package/dist/wire/encode.d.ts +79 -6
  191. package/dist/wire/files.d.ts +116 -0
  192. package/dist/wire/index.d.ts +3 -1
  193. package/dist/wire/media-types.d.ts +106 -0
  194. package/dist/wire.js +596 -368
  195. package/frameworks/react/index.tsx +9 -5
  196. package/llms-full.txt +12 -3
  197. package/package.json +2 -1
  198. package/src/agent-tooling/archetypes.ts +43 -12
  199. package/src/agent-tooling/integrations/anthropic.ts +29 -5
  200. package/src/agent-tooling/integrations/cloudflare.ts +5 -0
  201. package/src/agent-tooling/integrations/langgraph.ts +13 -0
  202. package/src/agent-tooling/integrations/mastra.ts +46 -5
  203. package/src/agent-tooling/integrations/mock.ts +4 -0
  204. package/src/agent-tooling/integrations/ollama.ts +7 -0
  205. package/src/agent-tooling/integrations/openai.ts +2 -0
  206. package/src/agent-tooling/integrations/openrouter.ts +2 -0
  207. package/src/agent-tooling/integrations/pi.ts +26 -1
  208. package/src/agent-tooling/integrations/pydantic-ai.ts +37 -3
  209. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +42 -6
  210. package/src/agent-tooling/mcp/tools/scaffold.ts +853 -32
  211. package/src/agent-tooling/registry.ts +57 -0
  212. package/src/agent-tooling/types.ts +132 -0
  213. package/src/components/chat-thread.tsx +10 -1
  214. package/src/components/context.tsx +18 -12
  215. package/src/components/conversation-item.tsx +6 -0
  216. package/src/components/model-switcher.tsx +38 -8
  217. package/src/components/toast.tsx +6 -1
  218. package/src/elements/button.tsx +26 -3
  219. package/src/elements/chat.tsx +28 -2
  220. package/src/elements/default-input.tsx +128 -5
  221. package/src/elements/element-meta.json +30 -4
  222. package/src/elements/element-types.d.ts +12 -6
  223. package/src/elements/menu.tsx +30 -3
  224. package/src/elements/slot-text.ts +73 -0
  225. package/src/elements/slots.ts +10 -0
  226. package/src/index.ts +9 -5
  227. package/src/wire/encode.ts +229 -14
  228. package/src/wire/files.ts +342 -0
  229. package/src/wire/index.ts +21 -0
  230. package/src/wire/media-types.ts +344 -0
  231. package/dist/elements/chunks/arrow-left-Bb46H7w9.js +0 -1
  232. package/dist/elements/chunks/check-C8desc_h.js +0 -1
  233. package/dist/elements/chunks/chevron-down-DKtc2HNB.js +0 -1
  234. package/dist/elements/chunks/chevron-right-Dw4GPhwL.js +0 -1
  235. package/dist/elements/chunks/circle-BeokBsvB.js +0 -1
  236. package/dist/elements/chunks/circle-check-wJSqt859.js +0 -1
  237. package/dist/elements/chunks/context-WHDl-xSx.js +0 -1
  238. package/dist/elements/chunks/conversation-list-CdR14_Y4.js +0 -1
  239. package/dist/elements/chunks/default-input-CSZqUM81.js +0 -1
  240. package/dist/elements/chunks/message-square-CnpAm_P0.js +0 -1
  241. package/dist/elements/chunks/model-switcher-CclaJEHt.js +0 -1
  242. package/dist/elements/chunks/x-D1qx0aLd.js +0 -1
  243. package/dist/register-impl-bzrZ44ft.js +0 -145
@@ -16,6 +16,32 @@ import {
16
16
  } from '../components/attachments';
17
17
  import { actionIcon } from '../ui/action-icons';
18
18
  import type { CustomAction } from './chat-types';
19
+ // The LEAF module, deliberately not the `../wire` barrel: this needs the media
20
+ // declaration, and importing the barrel would pull the whole stream adapter into
21
+ // the elements bundle for a table of strings. Sharing the module itself (rather
22
+ // than copying the list into the composer) is the entire point -- a second list
23
+ // here is the drift this design exists to prevent.
24
+ import { resolveMediaPolicy, type MediaTypeFilter } from '../wire/media-types';
25
+ // Also a leaf, and imported for the same reason: when the browser cannot name a
26
+ // file, the question "can this be sent?" is answered by decoding its bytes, and
27
+ // this module is where that decode already lives. Asking IT rather than growing
28
+ // a second decoder here is what keeps the composer's answer and the encoder's
29
+ // answer the same answer, byte for byte, instead of two that agree today.
30
+ import { classifyAttachment } from '../wire/files';
31
+
32
+ /** One file the composer refused, as facts rather than as a message. The kit
33
+ * says what happened; the application decides what the user reads. */
34
+ export interface RejectedAttachment {
35
+ filename: string;
36
+ /** The browser's media type for the file, or `''` when it could not tell. An
37
+ * empty one is not by itself why a file was rejected: an unnamed file is
38
+ * decided by decoding its bytes, so `''` here means the decode is what said
39
+ * no (binary), or that your `accept` left no text type for it to land in. */
40
+ mediaType: string;
41
+ /** `'filtered'` = this kit could have sent it, your `accept` excluded it.
42
+ * `'unsupported'` = no API takes this as message content at all. */
43
+ reason: 'filtered' | 'unsupported';
44
+ }
19
45
 
20
46
  export interface DefaultPromptInputProps {
21
47
  /** String = controlled text mirror; ComposerDoc = a seed that pre-populates pills. */
@@ -31,6 +57,19 @@ export interface DefaultPromptInputProps {
31
57
  * `onAttachmentsChange` is provided (e.g. when a `+` menu already covers
32
58
  * file-attach). Defaults to `true`. */
33
59
  attach?: boolean;
60
+ /** Which attachment media types the user may stage, in HTML `accept` syntax
61
+ * (`'image/*,application/pdf'`). Omitted means no filter, exactly as before.
62
+ *
63
+ * NARROWED BY WHAT THE ENCODERS CAN SEND: `'image/*'` here resolves to the
64
+ * four image formats both APIs actually accept, not to every image type the
65
+ * OS will offer. It is the same string, resolved by the same function against
66
+ * the same declaration, as `toOpenAIMessages(msgs, { accept })` -- so a file
67
+ * the picker allows is a file the wire can carry. */
68
+ accept?: MediaTypeFilter;
69
+ /** Files that were dropped because `accept` excluded them. Carries the facts
70
+ * (name, media type, reason) and renders nothing itself: what the user should
71
+ * see is the application's call, not the kit's. */
72
+ onAttachmentsRejected?: (rejected: RejectedAttachment[]) => void;
34
73
  /** Show a Search (Globe) button in the left toolbar; calls `onSearch`. */
35
74
  search?: boolean;
36
75
  /** Show a Voice (Mic) button in the left toolbar; calls `onVoice`. */
@@ -64,7 +103,23 @@ export interface DefaultPromptInputProps {
64
103
  onComposerChange?: (change: ComposerChange) => void;
65
104
  }
66
105
 
67
- function fileToAttachment(file: File): AttachmentData {
106
+ /** The staged file as a `data:` URI.
107
+ *
108
+ * NOT `URL.createObjectURL`. An object URL resolves only inside the tab that
109
+ * minted it, so it renders a perfect thumbnail here and is meaningless to
110
+ * anything downstream — `toOpenAIMessages` / `toAnthropicMessages` refuse it,
111
+ * and before they refused it the attachment reached the model as nothing at
112
+ * all. A data URI previews identically and is the one form both providers
113
+ * actually take. */
114
+ const readAsDataUrl = (file: File): Promise<string> =>
115
+ new Promise((resolve, reject) => {
116
+ const reader = new FileReader();
117
+ reader.onload = () => resolve(String(reader.result));
118
+ reader.onerror = () => reject(reader.error ?? new Error(`Could not read ${file.name}`));
119
+ reader.readAsDataURL(file);
120
+ });
121
+
122
+ async function fileToAttachment(file: File): Promise<AttachmentData> {
68
123
  const id =
69
124
  typeof crypto !== 'undefined' && crypto.randomUUID
70
125
  ? crypto.randomUUID()
@@ -74,7 +129,9 @@ function fileToAttachment(file: File): AttachmentData {
74
129
  type: 'file',
75
130
  filename: file.name,
76
131
  mediaType: file.type || undefined,
77
- url: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined,
132
+ // EVERY file, not just images. A document used to get no `url` at all,
133
+ // which left it unencodable for exactly the same reason a blob: URL is.
134
+ url: await readAsDataUrl(file),
78
135
  };
79
136
  }
80
137
 
@@ -83,9 +140,67 @@ export function DefaultPromptInput(props: DefaultPromptInputProps) {
83
140
  const attachments = () => props.attachments ?? [];
84
141
  const canAttach = () => !!props.onAttachmentsChange;
85
142
 
86
- const addFiles = (files: FileList | null) => {
143
+ // The SAME resolver the encoders call, on the same declaration. Recomputed
144
+ // reactively so a host that swaps `accept` at runtime moves the picker hint
145
+ // and the filter together.
146
+ const mediaPolicy = () => resolveMediaPolicy({ accept: props.accept });
147
+
148
+ const addFiles = async (files: FileList | null) => {
87
149
  if (!files?.length || !props.onAttachmentsChange) return;
88
- props.onAttachmentsChange([...attachments(), ...Array.from(files).map(fileToAttachment)]);
150
+
151
+ // The `accept` ATTRIBUTE below is only a hint — every OS dialog offers an
152
+ // "All Files" escape, and it does not apply to drag-and-drop at all. So the
153
+ // filter that actually holds is this one, in JS, on the files as staged.
154
+ const policy = mediaPolicy();
155
+ const picked = Array.from(files);
156
+ // No `accept` means no filtering, which is what every existing consumer gets
157
+ // today. Opting in is what turns the picker into a guarantee.
158
+ const decisions = picked.map((file) =>
159
+ props.accept === undefined ? undefined : policy.decide(file.type),
160
+ );
161
+ // Read a file when its media type says yes, and ALSO when its media type
162
+ // says nothing at all: `undetermined` means the browser could not name it,
163
+ // and the only honest way to answer is to look at the bytes. Reading is the
164
+ // cost of that answer, so it is spent only on files that could still be
165
+ // staged — a file the filter already rejected is never read.
166
+ const needsBytes = (d: (typeof decisions)[number]): boolean =>
167
+ d === undefined || d.status === 'allowed' || d.status === 'undetermined';
168
+ // One parallel pass, index-aligned with `picked`, so the staged order is the
169
+ // PICK order rather than whichever file finished reading first.
170
+ const read = await Promise.all(
171
+ picked.map((file, i) => (needsBytes(decisions[i]) ? fileToAttachment(file) : undefined)),
172
+ );
173
+
174
+ const staged: AttachmentData[] = [];
175
+ const rejected: RejectedAttachment[] = [];
176
+ picked.forEach((file, i) => {
177
+ const decision = decisions[i];
178
+ const attachment = read[i];
179
+ if (decision === undefined || decision.status === 'allowed') {
180
+ if (attachment) staged.push(attachment);
181
+ return;
182
+ }
183
+ if (decision.status === 'undetermined') {
184
+ // The bytes are in hand now, so the encoder can answer for real. Its
185
+ // verdict IS the composer's verdict: staging anything it would refuse is
186
+ // the exact defect (#186) this whole design exists to prevent.
187
+ if (attachment && classifyAttachment(attachment, policy).status === 'encodable') {
188
+ staged.push(attachment);
189
+ } else {
190
+ // Not `filtered`: the kit could not encode this either, so pointing
191
+ // the developer at their own `accept` would send them the wrong way.
192
+ rejected.push({ filename: file.name, mediaType: file.type, reason: 'unsupported' });
193
+ }
194
+ return;
195
+ }
196
+ rejected.push({ filename: file.name, mediaType: file.type, reason: decision.status });
197
+ });
198
+
199
+ if (rejected.length > 0) props.onAttachmentsRejected?.(rejected);
200
+ if (staged.length === 0) return;
201
+ // Re-read `attachments()` AFTER the await — a second drop while these were
202
+ // being read would otherwise be overwritten by this call's stale snapshot.
203
+ props.onAttachmentsChange?.([...attachments(), ...staged]);
89
204
  };
90
205
  const removeAttachment = (id: string) =>
91
206
  props.onAttachmentsChange?.(attachments().filter((a) => a.id !== id));
@@ -156,8 +271,16 @@ export function DefaultPromptInput(props: DefaultPromptInputProps) {
156
271
  type="file"
157
272
  multiple
158
273
  class="hidden"
274
+ // Derived from the same policy as the filter above, never spelled
275
+ // out again. `accept="image/*"` narrows to the image formats the
276
+ // wire can actually carry, so the OS dialog greys out the SVG
277
+ // that would otherwise 400 at request time.
278
+ accept={props.accept === undefined ? undefined : mediaPolicy().accept}
159
279
  onChange={(e) => {
160
- addFiles(e.currentTarget.files);
280
+ // Reading is async now, so this is deliberately not awaited.
281
+ // `addFiles` captures the FileList synchronously, before its
282
+ // first await, so clearing the input below cannot race it.
283
+ void addFiles(e.currentTarget.files);
161
284
  e.currentTarget.value = ''; // allow re-picking the same file
162
285
  }}
163
286
  />
@@ -843,7 +843,7 @@
843
843
  "type": "undefined | string",
844
844
  "optional": true,
845
845
  "scalar": true,
846
- "description": "Accessible name. REQUIRED for icon-only buttons (no visible text); ignored when you slot visible text, which already names the button.",
846
+ "description": "Accessible name. REQUIRED for icon-only buttons (no visible text); ignored when you slot visible text, which already names the button. An `aria-label` on top of visible text REPLACES that name rather than adding to it, so a button reading \"Save\" that answers to \"Submit\" is unusable by speech input (WCAG 2.5.3, Label in Name). The visible text wins. An `icon` / `icon-sm` size hides the slot, which makes the button icon-only whatever you slotted, so `label` is what names it there.",
847
847
  "displayType": "undefined | string"
848
848
  },
849
849
  {
@@ -1572,6 +1572,14 @@
1572
1572
  "default": "'always'",
1573
1573
  "displayType": "undefined | \"always\" | \"hover\""
1574
1574
  },
1575
+ {
1576
+ "name": "accept",
1577
+ "type": "undefined | string",
1578
+ "optional": true,
1579
+ "scalar": true,
1580
+ "description": "Which attachment media types the user may stage, in HTML `accept` syntax: `<kai-chat accept=\"image/*,application/pdf\">`. A plain string, so unlike `messages` it DOES work as an attribute. Omitted means no filter. MEDIA TYPES ONLY -- exact (`image/png`) or subtype wildcard (`text/*`). HTML allows a file extension here and this does not: `accept=\".py\"` THROWS with the entry named, rather than silently resolving to a picker that accepts nothing. It can only NARROW what the kit can already encode: `accept=\"image/*\"` resolves to the four image formats both APIs take, not to every image type the OS offers. Pass the SAME string to `toOpenAIMessages(msgs, { accept })` and the picker and the wire cannot disagree -- both resolve it through `resolveMediaPolicy` against one declaration. That declaration is readable as `encodableMediaTypes()` from `@kitn.ai/ui/wire`, if you would rather build your own picker than use this prop.",
1581
+ "displayType": "undefined | string"
1582
+ },
1575
1583
  {
1576
1584
  "name": "messages",
1577
1585
  "type": "undefined | { id: string; role: \"user\" | \"assistant\"; parts: ({ type: \"text\"; text: string; raw?: undefined | { source: string; payload: unknown } } | { type: \"reasoning\"; text: string; label?: undefined | string; index?: undefined | number; streamId?: undefined | string; signature?: undefined | string; raw?: undefined | { source: string; payload: unknown } } | { type: \"tool\"; tool: { type: string; kind?: undefined | \"command\" | \"file-change\" | \"search\" | \"fetch\" | \"mcp\" | \"image\" | \"generic\"; state: \"input-streaming\" | \"input-available\" | \"output-available\" | \"output-error\"; input?: undefined | Record<string, unknown>; rawInput?: undefined | string; output?: undefined | Record<string, unknown>; toolCallId?: undefined | string; errorText?: undefined | string; raw?: undefined | { source: string; payload: unknown } }; raw?: undefined | { source: string; payload: unknown } } | { type: \"card\"; envelope: { type: string; id: string; data: unknown; title?: undefined | string; resolution?: undefined | { kind: \"action\"; action: string; payload?: unknown; at?: undefined | string } | { kind: \"submit\"; data: unknown; at?: undefined | string } | { kind: \"dismissed\"; at?: undefined | string } | { kind: \"expired\"; reason?: undefined | string; at?: undefined | string } }; raw?: undefined | { source: string; payload: unknown } } | { type: \"source\"; source: { id?: undefined | string; url?: undefined | string; title?: undefined | string; snippet?: undefined | string; index?: undefined | number }; raw?: undefined | { source: string; payload: unknown } } | { type: \"file\"; attachment: { id: string; type: \"file\" | \"source-document\"; filename?: undefined | string; mediaType?: undefined | string; url?: undefined | string; title?: undefined | string }; raw?: undefined | { source: string; payload: unknown } })[]; actions?: undefined | (\"copy\" | \"like\" | \"dislike\" | \"regenerate\" | \"edit\" | { id: string; label: string; icon?: undefined | string; tooltip?: undefined | string })[]; avatar?: undefined | { src?: undefined | string; fallback?: undefined | string; alt?: undefined | string }; feedback?: undefined | \"like\" | \"dislike\" }[]",
@@ -1608,6 +1616,12 @@
1608
1616
  "description": "The staged attachments changed (file added or removed). Carries the full current list so a consumer can react in real time.",
1609
1617
  "displayDetail": "{ attachments: { id: string; type: \"file\" | \"source-document\"; filename?: undefined | string; mediaType?: undefined | string; url?: undefined | string; title?: undefined | string }[] }"
1610
1618
  },
1619
+ {
1620
+ "name": "kai-attachments-rejected",
1621
+ "detail": "{ rejected: { filename: string; mediaType: string; reason: \"filtered\" | \"unsupported\" }[] }",
1622
+ "description": "One or more picked files were refused because `accept` excluded them. The element renders NO message of its own: it reports the facts (name, media type, whether the kit could have sent it) and what the user should see is the application's call. Only ever fires when `accept` is set.",
1623
+ "displayDetail": "{ rejected: { filename: string; mediaType: string; reason: \"filtered\" | \"unsupported\" }[] }"
1624
+ },
1611
1625
  {
1612
1626
  "name": "kai-message-action",
1613
1627
  "detail": "{ messageId: string; action: string; state?: undefined | \"on\" | \"off\" }",
@@ -2792,7 +2806,19 @@
2792
2806
  "storyId": "solid-advanced-elements-contextcacheusage--docs"
2793
2807
  }
2794
2808
  ],
2795
- "tokens": []
2809
+ "tokens": [],
2810
+ "parts": [
2811
+ {
2812
+ "name": "track",
2813
+ "doc": "The usage meter track inside the hover-card breakdown. Carries `role=\"progressbar\"` and is in the DOM only while the card is open. Restyle its height, radius, or background from outside.",
2814
+ "recipe": "kai-context::part(track) { height: 0.5rem }"
2815
+ },
2816
+ {
2817
+ "name": "fill",
2818
+ "doc": "The used-tokens portion of that meter. Its width follows `usedTokens / maxTokens`; its default color is the severity hue picked by `warnThreshold` / `dangerThreshold`, so recoloring it from outside replaces that signal.",
2819
+ "recipe": "kai-context::part(fill) { background: var(--color-tool-blue) }"
2820
+ }
2821
+ ]
2796
2822
  },
2797
2823
  {
2798
2824
  "tag": "kai-conversations",
@@ -4457,7 +4483,7 @@
4457
4483
  "type": "undefined | string",
4458
4484
  "optional": true,
4459
4485
  "scalar": true,
4460
- "description": "Built-in trigger: a text label (e.g. `\"High\"`).",
4486
+ "description": "Built-in trigger: a text label (e.g. `\"High\"`). This is the trigger's VISIBLE text, so it is also its accessible name, and `label` does not override it: an accessible name that does not contain the visible text is unreachable by speech input, which is what WCAG 2.5.3 (Label in Name) exists for. A slotted `slot=\"trigger\"` replaces this built-in trigger entirely and is named differently; see `label`.",
4461
4487
  "displayType": "undefined | string"
4462
4488
  },
4463
4489
  {
@@ -4473,7 +4499,7 @@
4473
4499
  "type": "undefined | string",
4474
4500
  "optional": true,
4475
4501
  "scalar": true,
4476
- "description": "Accessible name for an icon-only trigger (no visible label).",
4502
+ "description": "Accessible name for a trigger with no visible label. Ignored when `triggerLabel` is set, which is already the visible name. It DOES name a slotted `slot=\"trigger\"`, and that is a difference in what the two slots MEAN, not a limitation. `<kai-button>`'s slot IS the button's label, so text slotted there is the name and `label` steps aside. This slot is VISUAL content, a `+` or an `<svg>`, with the name supplied separately: decoration beside a name, never a second name competing with one. So `label` names the trigger here by design. Slotting a real WORD rather than a glyph makes that word a visible label, and an accessible name has to contain the visible text. Then either drop `label` or make it contain the word you slotted.",
4477
4503
  "displayType": "undefined | string"
4478
4504
  },
4479
4505
  {
@@ -280,7 +280,7 @@ export interface KaiButtonElement extends HTMLElement {
280
280
  icon?: string;
281
281
  /** Trailing icon, after the label (e.g. `"chevron-down"` for a menu affordance). */
282
282
  iconTrailing?: string;
283
- /** Accessible name. REQUIRED for icon-only buttons (no visible text); ignored when you slot visible text, which already names the button. */
283
+ /** Accessible name. REQUIRED for icon-only buttons (no visible text); ignored when you slot visible text, which already names the button. An `aria-label` on top of visible text REPLACES that name rather than adding to it, so a button reading "Save" that answers to "Submit" is unusable by speech input (WCAG 2.5.3, Label in Name). The visible text wins. An `icon` / `icon-sm` size hides the slot, which makes the button icon-only whatever you slotted, so `label` is what names it there. */
284
284
  label?: string;
285
285
  /** Disable the button (non-interactive, dimmed). */
286
286
  disabled?: boolean;
@@ -418,6 +418,8 @@ export interface KaiChatElement extends HTMLElement {
418
418
  kindIcons?: Record<string, string>;
419
419
  /** Whether each message's action bar is always visible (`'always'`, default) or only revealed on hover of that message row (`'hover'`). */
420
420
  actionsReveal?: "always" | "hover";
421
+ /** Which attachment media types the user may stage, in HTML `accept` syntax: `<kai-chat accept="image/*,application/pdf">`. A plain string, so unlike `messages` it DOES work as an attribute. Omitted means no filter. MEDIA TYPES ONLY -- exact (`image/png`) or subtype wildcard (`text/*`). HTML allows a file extension here and this does not: `accept=".py"` THROWS with the entry named, rather than silently resolving to a picker that accepts nothing. It can only NARROW what the kit can already encode: `accept="image/*"` resolves to the four image formats both APIs take, not to every image type the OS offers. Pass the SAME string to `toOpenAIMessages(msgs, { accept })` and the picker and the wire cannot disagree -- both resolve it through `resolveMediaPolicy` against one declaration. That declaration is readable as `encodableMediaTypes()` from `@kitn.ai/ui/wire`, if you would rather build your own picker than use this prop. */
422
+ accept?: string;
421
423
  /** The full message thread to render, newest last. Each entry carries its role, ordered `parts`, and optional actions/avatar/feedback. Set as a JS property (`el.messages = [...]`); a NEW array reference per streaming chunk re-renders (mutating in place does not). Omit for an empty thread. Re-declared here (rather than inherited from `ChatThreadProps`) because the ELEMENT registers a `[]` default and renders the empty state without it, while the SolidJS `<ChatThread>` component still requires it. The facade hands it a validated array either way. Matches `<kai-thread>`. */
422
424
  messages: { id: string; role: "user" | "assistant"; parts: ({ type: "text"; text: string; raw?: { source: string; payload: unknown } } | { type: "reasoning"; text: string; label?: string; index?: number; streamId?: string; signature?: string; raw?: { source: string; payload: unknown } } | { type: "tool"; tool: { type: string; kind?: "command" | "file-change" | "search" | "fetch" | "mcp" | "image" | "generic"; state: "input-streaming" | "input-available" | "output-available" | "output-error"; input?: Record<string, unknown>; rawInput?: string; output?: Record<string, unknown>; toolCallId?: string; errorText?: string; raw?: { source: string; payload: unknown } }; raw?: { source: string; payload: unknown } } | { type: "card"; envelope: { type: string; id: string; data: unknown; title?: string; resolution?: { kind: "action"; action: string; payload?: unknown; at?: string } | { kind: "submit"; data: unknown; at?: string } | { kind: "dismissed"; at?: string } | { kind: "expired"; reason?: string; at?: string } }; raw?: { source: string; payload: unknown } } | { type: "source"; source: { id?: string; url?: string; title?: string; snippet?: string; index?: number }; raw?: { source: string; payload: unknown } } | { type: "file"; attachment: { id: string; type: "file" | "source-document"; filename?: string; mediaType?: string; url?: string; title?: string }; raw?: { source: string; payload: unknown } })[]; actions?: ("copy" | "like" | "dislike" | "regenerate" | "edit" | { id: string; label: string; icon?: string; tooltip?: string })[]; avatar?: { src?: string; fallback?: string; alt?: string }; feedback?: "like" | "dislike" }[];
423
425
  /** Optional card type -> custom-element tag overrides/additions for `card` parts (merged over the built-ins). Property: `el.cardTypes`. Typed as a plain string map (not the `CardTagMap` alias) so the generated React wrapper inlines it instead of emitting an unresolved named type. */
@@ -919,11 +921,11 @@ export interface KaiMenuElement extends HTMLElement {
919
921
  placement?: string;
920
922
  /** Built-in trigger: leading icon (a named icon like `"plus"`, an image URL/data-URI, or text). Use this instead of slotting `slot="trigger"` for the common case; a slotted trigger overrides it. */
921
923
  triggerIcon?: string;
922
- /** Built-in trigger: a text label (e.g. `"High"`). */
924
+ /** Built-in trigger: a text label (e.g. `"High"`). This is the trigger's VISIBLE text, so it is also its accessible name, and `label` does not override it: an accessible name that does not contain the visible text is unreachable by speech input, which is what WCAG 2.5.3 (Label in Name) exists for. A slotted `slot="trigger"` replaces this built-in trigger entirely and is named differently; see `label`. */
923
925
  triggerLabel?: string;
924
926
  /** Built-in trigger: a trailing icon (e.g. `"chevron-down"` for a select look). */
925
927
  triggerIconTrailing?: string;
926
- /** Accessible name for an icon-only trigger (no visible label). */
928
+ /** Accessible name for a trigger with no visible label. Ignored when `triggerLabel` is set, which is already the visible name. It DOES name a slotted `slot="trigger"`, and that is a difference in what the two slots MEAN, not a limitation. `<kai-button>`'s slot IS the button's label, so text slotted there is the name and `label` steps aside. This slot is VISUAL content, a `+` or an `<svg>`, with the name supplied separately: decoration beside a name, never a second name competing with one. So `label` names the trigger here by design. Slotting a real WORD rather than a glyph makes that word a visible label, and an accessible name has to contain the visible text. Then either drop `label` or make it contain the word you slotted. */
927
929
  label?: string;
928
930
  /** Drive/observe open state (Shoelace-style: settable + reflected to the `open` attribute, the menu still self-manages on click/keyboard). Set `el.open = true`, or `<kai-menu open>`; listen for `kai-open-change`. */
929
931
  open?: boolean;
@@ -2031,7 +2033,7 @@ export interface KaiButtonElementProps {
2031
2033
  icon?: string;
2032
2034
  /** Trailing icon, after the label (e.g. `"chevron-down"` for a menu affordance). */
2033
2035
  iconTrailing?: string;
2034
- /** Accessible name. REQUIRED for icon-only buttons (no visible text); ignored when you slot visible text, which already names the button. */
2036
+ /** Accessible name. REQUIRED for icon-only buttons (no visible text); ignored when you slot visible text, which already names the button. An `aria-label` on top of visible text REPLACES that name rather than adding to it, so a button reading "Save" that answers to "Submit" is unusable by speech input (WCAG 2.5.3, Label in Name). The visible text wins. An `icon` / `icon-sm` size hides the slot, which makes the button icon-only whatever you slotted, so `label` is what names it there. */
2035
2037
  label?: string;
2036
2038
  /** Disable the button (non-interactive, dimmed). */
2037
2039
  disabled?: boolean;
@@ -2151,6 +2153,8 @@ export interface KaiChatElementProps {
2151
2153
  kindIcons?: Record<string, string>;
2152
2154
  /** Whether each message's action bar is always visible (`'always'`, default) or only revealed on hover of that message row (`'hover'`). */
2153
2155
  actionsReveal?: "always" | "hover";
2156
+ /** Which attachment media types the user may stage, in HTML `accept` syntax: `<kai-chat accept="image/*,application/pdf">`. A plain string, so unlike `messages` it DOES work as an attribute. Omitted means no filter. MEDIA TYPES ONLY -- exact (`image/png`) or subtype wildcard (`text/*`). HTML allows a file extension here and this does not: `accept=".py"` THROWS with the entry named, rather than silently resolving to a picker that accepts nothing. It can only NARROW what the kit can already encode: `accept="image/*"` resolves to the four image formats both APIs take, not to every image type the OS offers. Pass the SAME string to `toOpenAIMessages(msgs, { accept })` and the picker and the wire cannot disagree -- both resolve it through `resolveMediaPolicy` against one declaration. That declaration is readable as `encodableMediaTypes()` from `@kitn.ai/ui/wire`, if you would rather build your own picker than use this prop. */
2157
+ accept?: string;
2154
2158
  /** The full message thread to render, newest last. Each entry carries its role, ordered `parts`, and optional actions/avatar/feedback. Set as a JS property (`el.messages = [...]`); a NEW array reference per streaming chunk re-renders (mutating in place does not). Omit for an empty thread. Re-declared here (rather than inherited from `ChatThreadProps`) because the ELEMENT registers a `[]` default and renders the empty state without it, while the SolidJS `<ChatThread>` component still requires it. The facade hands it a validated array either way. Matches `<kai-thread>`. */
2155
2159
  messages?: { id: string; role: "user" | "assistant"; parts: ({ type: "text"; text: string; raw?: { source: string; payload: unknown } } | { type: "reasoning"; text: string; label?: string; index?: number; streamId?: string; signature?: string; raw?: { source: string; payload: unknown } } | { type: "tool"; tool: { type: string; kind?: "command" | "file-change" | "search" | "fetch" | "mcp" | "image" | "generic"; state: "input-streaming" | "input-available" | "output-available" | "output-error"; input?: Record<string, unknown>; rawInput?: string; output?: Record<string, unknown>; toolCallId?: string; errorText?: string; raw?: { source: string; payload: unknown } }; raw?: { source: string; payload: unknown } } | { type: "card"; envelope: { type: string; id: string; data: unknown; title?: string; resolution?: { kind: "action"; action: string; payload?: unknown; at?: string } | { kind: "submit"; data: unknown; at?: string } | { kind: "dismissed"; at?: string } | { kind: "expired"; reason?: string; at?: string } }; raw?: { source: string; payload: unknown } } | { type: "source"; source: { id?: string; url?: string; title?: string; snippet?: string; index?: number }; raw?: { source: string; payload: unknown } } | { type: "file"; attachment: { id: string; type: "file" | "source-document"; filename?: string; mediaType?: string; url?: string; title?: string }; raw?: { source: string; payload: unknown } })[]; actions?: ("copy" | "like" | "dislike" | "regenerate" | "edit" | { id: string; label: string; icon?: string; tooltip?: string })[]; avatar?: { src?: string; fallback?: string; alt?: string }; feedback?: "like" | "dislike" }[];
2156
2160
  /** Optional card type -> custom-element tag overrides/additions for `card` parts (merged over the built-ins). Property: `el.cardTypes`. Typed as a plain string map (not the `CardTagMap` alias) so the generated React wrapper inlines it instead of emitting an unresolved named type. */
@@ -2548,11 +2552,11 @@ export interface KaiMenuElementProps {
2548
2552
  placement?: string;
2549
2553
  /** Built-in trigger: leading icon (a named icon like `"plus"`, an image URL/data-URI, or text). Use this instead of slotting `slot="trigger"` for the common case; a slotted trigger overrides it. */
2550
2554
  triggerIcon?: string;
2551
- /** Built-in trigger: a text label (e.g. `"High"`). */
2555
+ /** Built-in trigger: a text label (e.g. `"High"`). This is the trigger's VISIBLE text, so it is also its accessible name, and `label` does not override it: an accessible name that does not contain the visible text is unreachable by speech input, which is what WCAG 2.5.3 (Label in Name) exists for. A slotted `slot="trigger"` replaces this built-in trigger entirely and is named differently; see `label`. */
2552
2556
  triggerLabel?: string;
2553
2557
  /** Built-in trigger: a trailing icon (e.g. `"chevron-down"` for a select look). */
2554
2558
  triggerIconTrailing?: string;
2555
- /** Accessible name for an icon-only trigger (no visible label). */
2559
+ /** Accessible name for a trigger with no visible label. Ignored when `triggerLabel` is set, which is already the visible name. It DOES name a slotted `slot="trigger"`, and that is a difference in what the two slots MEAN, not a limitation. `<kai-button>`'s slot IS the button's label, so text slotted there is the name and `label` steps aside. This slot is VISUAL content, a `+` or an `<svg>`, with the name supplied separately: decoration beside a name, never a second name competing with one. So `label` names the trigger here by design. Slotting a real WORD rather than a glyph makes that word a visible label, and an accessible name has to contain the visible text. Then either drop `label` or make it contain the word you slotted. */
2556
2560
  label?: string;
2557
2561
  /** Drive/observe open state (Shoelace-style: settable + reflected to the `open` attribute, the menu still self-manages on click/keyboard). Set `el.open = true`, or `<kai-menu open>`; listen for `kai-open-change`. */
2558
2562
  open?: boolean;
@@ -3277,6 +3281,8 @@ export interface KaiChainOfThoughtElementEvents {
3277
3281
  export interface KaiChatElementEvents {
3278
3282
  /** The staged attachments changed (file added or removed). Carries the full current list so a consumer can react in real time. */
3279
3283
  onKaiAttachmentsChange?: (event: CustomEvent<{ attachments: { id: string; type: "file" | "source-document"; filename?: undefined | string; mediaType?: undefined | string; url?: undefined | string; title?: undefined | string }[] }>) => void;
3284
+ /** One or more picked files were refused because `accept` excluded them. The element renders NO message of its own: it reports the facts (name, media type, whether the kit could have sent it) and what the user should see is the application's call. Only ever fires when `accept` is set. */
3285
+ onKaiAttachmentsRejected?: (event: CustomEvent<{ rejected: { filename: string; mediaType: string; reason: "filtered" | "unsupported" }[] }>) => void;
3280
3286
  /** An action button on a message was clicked. `action` is the built-in name or custom id. `state` is present only for the toggleable feedback votes: `'on'` when a like/dislike is set, `'off'` when re-tapped to clear. */
3281
3287
  onKaiMessageAction?: (event: CustomEvent<{ messageId: string; action: string; state?: undefined | "on" | "off" }>) => void;
3282
3288
  /** The header model switcher changed. */
@@ -28,11 +28,28 @@ interface Props extends Record<string, unknown> {
28
28
  * URL/data-URI, or text). Use this instead of slotting `slot="trigger"` for
29
29
  * the common case; a slotted trigger overrides it. */
30
30
  triggerIcon?: string;
31
- /** Built-in trigger: a text label (e.g. `"High"`). */
31
+ /** Built-in trigger: a text label (e.g. `"High"`). This is the trigger's
32
+ * VISIBLE text, so it is also its accessible name, and `label` does not
33
+ * override it: an accessible name that does not contain the visible text is
34
+ * unreachable by speech input, which is what WCAG 2.5.3 (Label in Name)
35
+ * exists for. A slotted `slot="trigger"` replaces this built-in trigger
36
+ * entirely and is named differently; see `label`. */
32
37
  triggerLabel?: string;
33
38
  /** Built-in trigger: a trailing icon (e.g. `"chevron-down"` for a select look). */
34
39
  triggerIconTrailing?: string;
35
- /** Accessible name for an icon-only trigger (no visible label). */
40
+ /** Accessible name for a trigger with no visible label. Ignored when
41
+ * `triggerLabel` is set, which is already the visible name.
42
+ *
43
+ * It DOES name a slotted `slot="trigger"`, and that is a difference in what
44
+ * the two slots MEAN, not a limitation. `<kai-button>`'s slot IS the button's
45
+ * label, so text slotted there is the name and `label` steps aside. This slot
46
+ * is VISUAL content, a `+` or an `<svg>`, with the name supplied separately:
47
+ * decoration beside a name, never a second name competing with one. So
48
+ * `label` names the trigger here by design.
49
+ *
50
+ * Slotting a real WORD rather than a glyph makes that word a visible label,
51
+ * and an accessible name has to contain the visible text. Then either drop
52
+ * `label` or make it contain the word you slotted. */
36
53
  label?: string;
37
54
  /** Drive/observe open state (Shoelace-style: settable + reflected to the `open`
38
55
  * attribute, the menu still self-manages on click/keyboard). Set `el.open = true`,
@@ -182,7 +199,17 @@ defineWebComponent<Props, Events>('kai-menu', {
182
199
  ? 'gap-1.5 px-2 py-1.5 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground'
183
200
  : 'justify-center p-1.5 text-foreground hover:bg-muted',
184
201
  )}
185
- aria-label={props.label ?? (props.triggerLabel ? undefined : 'Open menu')}
202
+ // A visible `triggerLabel` IS the name (name-from-contents reaches the
203
+ // slotted/shadow text), so `label` must not be layered over it: aria-label
204
+ // REPLACES the computed name, and a trigger reading "High" that answers to
205
+ // "Reasoning effort" locks speech-input users out (WCAG 2.5.3). Same rule
206
+ // kai-checkpoint and kai-button follow.
207
+ //
208
+ // A SLOTTED trigger keeps `label`, and that is the contract rather than a
209
+ // gap: kai-button's slot IS the label, so text there is the name; this
210
+ // slot is visual content with the name supplied separately. Two slots,
211
+ // two meanings, so the same rule should not apply to both. See `label`.
212
+ aria-label={props.triggerLabel ? undefined : (props.label ?? 'Open menu')}
186
213
  >
187
214
  {/* Slotted trigger wins; otherwise build one from the trigger* props;
188
215
  otherwise fall back to a "more" glyph. */}
@@ -0,0 +1,73 @@
1
+ import { createSignal, type Accessor } from 'solid-js';
2
+
3
+ /**
4
+ * Track whether a `<slot>` is projecting VISIBLE TEXT, so a facade can tell
5
+ * "this control shows its own name" from "this control is icon-only and needs
6
+ * one".
7
+ *
8
+ * WHY IT EXISTS. Slotted light-DOM text is part of the flattened tree, so it
9
+ * names the shadow `<button>` all by itself — measured, not assumed:
10
+ * `scripts/probe-button-accessible-name.mjs` reads chromium's AX tree and
11
+ * `<kai-button>Save</kai-button>` computes the name "Save" from `contents`,
12
+ * with no `aria-label` anywhere. An `aria-label` on top of that does not add a
13
+ * name, it REPLACES one, and a replacement that disagrees with the visible text
14
+ * is a WCAG 2.5.3 (Label in Name) failure: speech-input users say what they
15
+ * see, so a button reading "Save" whose accessible name is "Submit" cannot be
16
+ * activated by voice at all. `kai-checkpoint` already resolved this the same
17
+ * way; this is the reusable form of that decision.
18
+ *
19
+ * WHY A SIGNAL AND NOT A ONE-OFF READ. The facade renders while the HTML parser
20
+ * is still inside the element's own tag, so the light DOM is usually EMPTY at
21
+ * that moment and any synchronous read of `element.childNodes` reports "no text"
22
+ * for the most common case there is. `slotchange` is the event that tells the
23
+ * truth, and it fires on the initial assignment too.
24
+ *
25
+ * The initial value is therefore `false`, meaning "assume icon-only", which
26
+ * makes the pre-`slotchange` frame emit the `aria-label`. That direction is
27
+ * deliberate: an overridden name is a defect, an ABSENT name is a worse one, so
28
+ * the transient state is the named one. `slotchange` is queued as a mutation
29
+ * observer microtask, so it lands before paint and no assistive technology
30
+ * observes the transient.
31
+ */
32
+ export function createSlotText(options: {
33
+ /**
34
+ * Follow `assignedNodes({ flatten: true })`, which descends into nested
35
+ * light-DOM `<slot>` elements (a consumer wrapping the element in their OWN
36
+ * component and forwarding a slot) — and, when nothing is assigned, returns
37
+ * the slot's FALLBACK content.
38
+ *
39
+ * Only safe on a slot whose fallback is exactly the visible label. Do NOT set
40
+ * it on a slot whose fallback can render a non-name (an icon that happens to
41
+ * be plain text, say): the fallback would count as visible text and suppress
42
+ * the very `aria-label` that names the control. `kai-button`'s default slot
43
+ * has no fallback at all, so flatten there only buys the nested case;
44
+ * `kai-menu`'s trigger slot falls back to an icon, so it must not use it.
45
+ */
46
+ flatten?: boolean;
47
+ } = {}): {
48
+ /** Whether the slot is currently showing text. */
49
+ hasText: Accessor<boolean>;
50
+ /** `ref` for the `<slot>`. Re-reads on every `slotchange`. */
51
+ ref: (slot: HTMLSlotElement) => void;
52
+ } {
53
+ const [hasText, setHasText] = createSignal(false);
54
+
55
+ const read = (slot: HTMLSlotElement) => {
56
+ const nodes = slot.assignedNodes({ flatten: options.flatten ?? false });
57
+ setHasText(nodes.some((node) => (node.textContent ?? '').trim() !== ''));
58
+ };
59
+
60
+ return {
61
+ hasText,
62
+ ref: (slot: HTMLSlotElement) => {
63
+ // A slot outside a shadow root assigns nothing, and this ref runs before
64
+ // solid inserts it — so this first read is normally `false` and the
65
+ // slotchange below is what carries the answer. It is here for the case
66
+ // where the element is created with its children already in place
67
+ // (`el.textContent = 'Save'` then appended), where no assignment CHANGE
68
+ // happens after insertion and slotchange may never fire.
69
+ read(slot);
70
+ slot.addEventListener('slotchange', () => read(slot));
71
+ },
72
+ };
73
+ }
@@ -400,6 +400,15 @@ export const PROGRESS_BAR_PARTS: PartDef[] = [
400
400
  { name: 'fill', doc: 'The filled portion; its width follows value/max. Recolor it from outside.', recipe: 'kai-progress-bar::part(fill) { background: var(--color-tool-green) }' },
401
401
  ];
402
402
 
403
+ /** Styleable `::part`s of `<kai-context>`. The usage meter is the same shared
404
+ * ProgressBar `<kai-progress-bar>` renders, mounted inside this element's own
405
+ * shadow root, so `kai-context` exposes the same two names. Both sit inside the
406
+ * hover-card breakdown and are in the DOM only while that card is open. */
407
+ export const CONTEXT_PARTS: PartDef[] = [
408
+ { name: 'track', doc: 'The usage meter track inside the hover-card breakdown. Carries `role="progressbar"` and is in the DOM only while the card is open. Restyle its height, radius, or background from outside.', recipe: 'kai-context::part(track) { height: 0.5rem }' },
409
+ { name: 'fill', doc: 'The used-tokens portion of that meter. Its width follows `usedTokens / maxTokens`; its default color is the severity hue picked by `warnThreshold` / `dangerThreshold`, so recoloring it from outside replaces that signal.', recipe: 'kai-context::part(fill) { background: var(--color-tool-blue) }' },
410
+ ];
411
+
403
412
  /** Styleable `::part`s of `<kai-file-tree>` — the changed-files / diff bits, shown
404
413
  * only when a file carries diff metadata (or the `summary` attribute is set). */
405
414
  export const FILE_TREE_PARTS: PartDef[] = [
@@ -645,6 +654,7 @@ export const ELEMENT_COMPOSITION: Record<string, ElementComposition> = {
645
654
  'kai-nav': { parts: NAV_PARTS },
646
655
  'kai-coachmark': { slots: COACHMARK_SLOTS, parts: COACHMARK_PARTS, children: 'The ANCHOR the coachmark points at: the element it attaches to and positions against. The bubble body is the `content` slot.' },
647
656
  'kai-progress-bar': { parts: PROGRESS_BAR_PARTS },
657
+ 'kai-context': { parts: CONTEXT_PARTS },
648
658
  'kai-file-tree': { parts: FILE_TREE_PARTS },
649
659
  'kai-prompt-dock': { slots: PROMPT_DOCK_SLOTS, parts: PROMPT_DOCK_PARTS, children: 'The input the dock wraps, typically a `<kai-prompt-input>`. The `top`/`bottom` slots are the lips around it.' },
650
660
  'kai-segmented': { parts: SEGMENTED_PARTS },
package/src/index.ts CHANGED
@@ -5,12 +5,16 @@
5
5
  // Every consumer of `@kitn.ai/ui` pays for this barrel, INCLUDING React/Vue/
6
6
  // Svelte consumers who never render a Solid component. It is therefore frozen at
7
7
  // the surface that shipped: adding a Solid component here taxes four frameworks
8
- // to serve one. Closing the 79/79 element coverage gaps on this file grew it
9
- // 591,359 -> 705,031 bytes (+19.2%), which is what motivated the split.
8
+ // to serve one. Closing the element coverage gaps on this file grew it
9
+ // 591,359 -> 705,031 bytes (+19.2%), which is what motivated the split. (That
10
+ // measurement was taken at the then-79 elements; the catalog grows, the bytes
11
+ // quoted do not — they are a record of the split, not a current reading.)
10
12
  //
11
- // The COMPLETE SolidJS surface — all 79 elements' components plus a `<Name>Props`
12
- // type for every public component — lives on `@kitn.ai/ui/solid` (src/solid.ts),
13
- // which is its own build target so only Solid consumers pay for it. `./solid`
13
+ // The COMPLETE SolidJS surface — a component for EVERY registered element plus a
14
+ // `<Name>Props` type for every public component — lives on `@kitn.ai/ui/solid`
15
+ // (src/solid.ts), which is its own build target so only Solid consumers pay for
16
+ // it. `npm run verify:solid-coverage` prints the element count and proves that
17
+ // surface n/n on every run, which is why no count is restated here. `./solid`
14
18
  // re-exports this file, so it is a strict superset: anything here is reachable
15
19
  // there too, and a Solid consumer only ever needs the one import.
16
20
  //