@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.
- package/dist/components/chat-thread.d.ts +9 -0
- package/dist/{create-tween-D3Y5qiij.js → create-tween-COnPwAQN.js} +1 -1
- package/dist/{create-tween-DGKY1FDY.js → create-tween-DmTUaXWE.js} +1 -1
- package/dist/{create-tween-paho5Rh-.js → create-tween-qs-jjvNA.js} +1 -1
- package/dist/custom-elements.json +43 -7
- package/dist/elements/agent-card.js +1 -1
- package/dist/elements/artifact.js +1 -1
- package/dist/elements/attachments.js +1 -1
- package/dist/elements/audio-visualizer.js +1 -1
- package/dist/elements/avatar.js +1 -1
- package/dist/elements/badge.js +1 -1
- package/dist/elements/button.js +1 -1
- package/dist/elements/card.js +1 -1
- package/dist/elements/cards.js +1 -1
- package/dist/elements/chain-of-thought.js +1 -1
- package/dist/elements/chat-scope-picker.js +1 -1
- package/dist/elements/chat-workspace.js +1 -1
- package/dist/elements/chat.js +1 -1
- package/dist/elements/checkpoint.js +1 -1
- package/dist/elements/choice.js +1 -1
- package/dist/elements/chunks/{Icon-C-2LvGDG.js → Icon-CHG5z5L7.js} +1 -1
- package/dist/elements/chunks/{action-icons-CaYoImEk.js → action-icons-BAtCEEgg.js} +1 -1
- package/dist/elements/chunks/arrow-left-CFPTeL1Y.js +1 -0
- package/dist/elements/chunks/{artifact-BBIscOxJ.js → artifact-fBZOE4SO.js} +1 -1
- package/dist/elements/chunks/{attachments-BB56zoji.js → attachments-DpYMe5VQ.js} +1 -1
- package/dist/elements/chunks/{audio-visualizer-BxOx4THz.js → audio-visualizer-DFwzAcuL.js} +1 -1
- package/dist/elements/chunks/{badge-SxLSZ19R.js → badge-D6zodm6E.js} +1 -1
- package/dist/elements/chunks/{button-CmkUkz6c.js → button-C42vrGvO.js} +1 -1
- package/dist/elements/chunks/{card-renderer-Db4ffkWv.js → card-renderer-DMZsSuvU.js} +1 -1
- package/dist/elements/chunks/{chat-thread-B3Ks-cKr.js → chat-thread-CNNEhol-.js} +1 -1
- package/dist/elements/chunks/check-7Q3K-iXE.js +1 -0
- package/dist/elements/chunks/chevron-down-CpNYLn7E.js +1 -0
- package/dist/elements/chunks/chevron-right-DrH85Fha.js +1 -0
- package/dist/elements/chunks/{choice-card-Q9cYYDyM.js → choice-card-YoWjpsFV.js} +1 -1
- package/dist/elements/chunks/circle-C9KRjKYv.js +1 -0
- package/dist/elements/chunks/circle-check-Dmi7Y4zw.js +1 -0
- package/dist/elements/chunks/{circle-x-D0MEfxOM.js → circle-x-QIxXda6k.js} +1 -1
- package/dist/elements/chunks/{code-block-C8VzAraE.js → code-block-CqeugReY.js} +1 -1
- package/dist/elements/chunks/{collapsible-Cvj61obE.js → collapsible-sTv-_5lF.js} +1 -1
- package/dist/elements/chunks/{composer-D86ztyL0.js → composer-ldQyeVEB.js} +1 -1
- package/dist/elements/chunks/{confirm-card-BdyDAGLw.js → confirm-card-C0RNOwfn.js} +1 -1
- package/dist/elements/chunks/context-5qgypNHC.js +1 -0
- package/dist/elements/chunks/conversation-list-DLzOacXz.js +1 -0
- package/dist/elements/chunks/{create-tween-C_A_B638.js → create-tween-V4TXZ93X.js} +1 -1
- package/dist/elements/chunks/default-input-DeeRSDHc.js +1 -0
- package/dist/elements/chunks/{define-BzPnNsXg.js → define-lE-QvQFn.js} +1 -1
- package/dist/elements/chunks/{disclosure-DVKsoc1F.js → disclosure-DZSa9jog.js} +1 -1
- package/dist/elements/chunks/{download-BjA54IMJ.js → download-B2ZmnGqW.js} +1 -1
- package/dist/elements/chunks/{dropdown-BjVVHWXH.js → dropdown-R3B7cSRE.js} +1 -1
- package/dist/elements/chunks/{ellipsis-BNedmqfi.js → ellipsis-BmxceY4l.js} +1 -1
- package/dist/elements/chunks/{embed-DQsl4XEq.js → embed-BVrtTZPo.js} +1 -1
- package/dist/elements/chunks/{external-link-Di-dw2CU.js → external-link-BbdR7YO7.js} +1 -1
- package/dist/elements/chunks/{file-text-B3sAODCV.js → file-text-D7dijo1A.js} +1 -1
- package/dist/elements/chunks/{file-tree-Dgto-bMn.js → file-tree-0upJ5jeb.js} +1 -1
- package/dist/elements/chunks/{folder-D6rMadFV.js → folder-D8z58t7B.js} +1 -1
- package/dist/elements/chunks/{form-CILxvhQ7.js → form-p5BKJNw5.js} +1 -1
- package/dist/elements/chunks/{hover-card-BtiF4XiU.js → hover-card-CN9s2md5.js} +1 -1
- package/dist/elements/chunks/{icon-DKkmze3y.js → icon-CNiKP37E.js} +1 -1
- package/dist/elements/chunks/{info-CWRjZkuy.js → info-BSnuJiHI.js} +1 -1
- package/dist/elements/chunks/{input--QJBK6BA.js → input-BauDmzxX.js} +1 -1
- package/dist/elements/chunks/{kbd-C3zuKh8R.js → kbd-BlHJaP9b.js} +1 -1
- package/dist/elements/chunks/{link-BL5JA7Q0.js → link-DEi5iUWC.js} +1 -1
- package/dist/elements/chunks/{link-preview-CU37bbXh.js → link-preview-BaHyM6W4.js} +1 -1
- package/dist/elements/chunks/{loader-XmpyxyRH.js → loader-CmJG_Fkq.js} +1 -1
- package/dist/elements/chunks/{markdown-3Gv5UZ_o.js → markdown-D2jaMpCN.js} +1 -1
- package/dist/elements/chunks/{message-X5NZbq-W.js → message-C5UfXZ2Z.js} +1 -1
- package/dist/elements/chunks/{message-CnBA2OUt.js → message-CXWLmmLn.js} +1 -1
- package/dist/elements/chunks/message-square-hXylwsy0.js +1 -0
- package/dist/elements/chunks/{minimize-2-DwyrUTcM.js → minimize-2-C9ytZFMU.js} +1 -1
- package/dist/elements/chunks/model-switcher-CivGfD7-.js +1 -0
- package/dist/elements/chunks/{overlay-Cepd-KcS.js → overlay-DJL_sbOm.js} +1 -1
- package/dist/elements/chunks/{paperclip-CdwKjrlu.js → paperclip-CFaB8naM.js} +1 -1
- package/dist/elements/chunks/{progress-bar-DMVXBD3b.js → progress-bar-Br5SksfR.js} +1 -1
- package/dist/elements/chunks/{prompt-suggestion-Di_WZE_5.js → prompt-suggestion-CMIvtAhU.js} +1 -1
- package/dist/elements/chunks/{reasoning-D0jMch5u.js → reasoning-DpZcNzLq.js} +1 -1
- package/dist/elements/chunks/{resizable-1znbL1Gv.js → resizable-Q8i3T9a_.js} +1 -1
- package/dist/elements/chunks/{rotate-cw-DVhhgx8n.js → rotate-cw-B5IHCBFV.js} +1 -1
- package/dist/elements/chunks/{scroll-area-Bs4bFS1P.js → scroll-area-eLeydBje.js} +1 -1
- package/dist/elements/chunks/{scroll-button-Bet5NLc4.js → scroll-button-DWbz0yA1.js} +1 -1
- package/dist/elements/chunks/{separator-C5Gh8qGC.js → separator-Xic78AD_.js} +1 -1
- package/dist/elements/chunks/{settings-5vmEFg0K.js → settings-B8H51pAx.js} +1 -1
- package/dist/elements/chunks/{settings-group-BWLB38kz.js → settings-group-CsG9eYLe.js} +1 -1
- package/dist/elements/chunks/{share-BZ3G_DaB.js → share-CNTx0z__.js} +1 -1
- package/dist/elements/chunks/{skeleton-DafPJQTp.js → skeleton-CLlHTcMI.js} +1 -1
- package/dist/elements/chunks/{source-D_qj_6uJ.js → source-BoVcU5ok.js} +1 -1
- package/dist/elements/chunks/{star-Cuv7j2IE.js → star-CxQVTVhI.js} +1 -1
- package/dist/elements/chunks/{store-C1StPHfW.js → store-CLtS5CUG.js} +1 -1
- package/dist/elements/chunks/{tasks-card-EyoBZfpA.js → tasks-card-CzlkLmUt.js} +1 -1
- package/dist/elements/chunks/{text-shimmer-D9gRSh34.js → text-shimmer-D5NhpIQ1.js} +1 -1
- package/dist/elements/chunks/{textarea-DuhtHfcm.js → textarea-x5Uxsmtm.js} +1 -1
- package/dist/elements/chunks/{thumbs-up-B9hwv43w.js → thumbs-up-UobcJnx9.js} +1 -1
- package/dist/elements/chunks/{toast-store-Bm3DUEDe.js → toast-store-BOfs9eXq.js} +1 -1
- package/dist/elements/chunks/{tool-CDtW7L0z.js → tool-Be6Dh_dt.js} +1 -1
- package/dist/elements/chunks/{tooltip-eB226mGK.js → tooltip-RxMxyCLF.js} +1 -1
- package/dist/elements/chunks/{triangle-alert-C1SNzTqK.js → triangle-alert-C4yxH2XN.js} +1 -1
- package/dist/elements/chunks/{use-card-resolution-YZh2MvI5.js → use-card-resolution-jva9ehH6.js} +1 -1
- package/dist/elements/chunks/{variant-aurora-Cj1F7D0l.js → variant-aurora-Dx4VRcjS.js} +1 -1
- package/dist/elements/chunks/{variant-custom-T9gpt7QE.js → variant-custom-Y1ZTWgEa.js} +1 -1
- package/dist/elements/chunks/{variant-wave-CxU9z-TE.js → variant-wave-B9gml9Cg.js} +1 -1
- package/dist/elements/chunks/{video-CK4R7Rus.js → video-CrPHsytT.js} +1 -1
- package/dist/elements/chunks/x-_-OPHept.js +1 -0
- package/dist/elements/coachmark.js +1 -1
- package/dist/elements/code-block.js +1 -1
- package/dist/elements/command.js +1 -1
- package/dist/elements/compare.js +1 -1
- package/dist/elements/composer.js +1 -1
- package/dist/elements/confirm-card.js +1 -1
- package/dist/elements/context-meter.js +1 -1
- package/dist/elements/conversation-list.js +1 -1
- package/dist/elements/default-input.d.ts +27 -0
- package/dist/elements/dialog.js +1 -1
- package/dist/elements/editable-label.js +1 -1
- package/dist/elements/embed.js +1 -1
- package/dist/elements/empty.js +1 -1
- package/dist/elements/feedback-bar.js +1 -1
- package/dist/elements/file-tree.js +1 -1
- package/dist/elements/file-upload.js +1 -1
- package/dist/elements/form.js +1 -1
- package/dist/elements/hover-card.js +1 -1
- package/dist/elements/icon.js +1 -1
- package/dist/elements/image.js +1 -1
- package/dist/elements/input.js +1 -1
- package/dist/elements/kbd.js +1 -1
- package/dist/elements/link-preview.js +1 -1
- package/dist/elements/loader.js +1 -1
- package/dist/elements/markdown.js +1 -1
- package/dist/elements/menu.js +1 -1
- package/dist/elements/message-skills.js +1 -1
- package/dist/elements/message.js +1 -1
- package/dist/elements/model-switcher.js +1 -1
- package/dist/elements/nav.js +1 -1
- package/dist/elements/notice.js +1 -1
- package/dist/elements/pane-group.js +1 -1
- package/dist/elements/pane.js +1 -1
- package/dist/elements/popover.js +1 -1
- package/dist/elements/progress-bar.js +1 -1
- package/dist/elements/prompt-dock.js +1 -1
- package/dist/elements/prompt-input.js +1 -1
- package/dist/elements/prompt-suggestions.js +1 -1
- package/dist/elements/reasoning.js +1 -1
- package/dist/elements/remote.js +1 -1
- package/dist/elements/resizable.js +1 -1
- package/dist/elements/response-stream.js +2 -2
- package/dist/elements/screen.js +1 -1
- package/dist/elements/scroll-area.js +1 -1
- package/dist/elements/scroll-button.js +1 -1
- package/dist/elements/search.js +1 -1
- package/dist/elements/segmented.js +1 -1
- package/dist/elements/separator.js +1 -1
- package/dist/elements/setting-item.js +1 -1
- package/dist/elements/settings-group.js +1 -1
- package/dist/elements/skeleton.js +1 -1
- package/dist/elements/slot-text.d.ts +51 -0
- package/dist/elements/slots.d.ts +5 -0
- package/dist/elements/source.js +1 -1
- package/dist/elements/status.js +1 -1
- package/dist/elements/switch.js +1 -1
- package/dist/elements/tabs.js +1 -1
- package/dist/elements/tasks.js +1 -1
- package/dist/elements/text-shimmer.js +1 -1
- package/dist/elements/thinking-bar.js +1 -1
- package/dist/elements/thread.js +1 -1
- package/dist/elements/toast.js +1 -1
- package/dist/elements/tool.js +1 -1
- package/dist/elements/tooltip.js +1 -1
- package/dist/elements/voice-input.js +1 -1
- package/dist/elements/voice-output.js +1 -1
- package/dist/elements.d.ts +12 -6
- package/dist/index.js +1535 -1487
- package/dist/index.server.js +2330 -2296
- package/dist/kai.es.js +1 -1
- package/dist/llms/llms-full.txt +12 -3
- package/dist/mcp.es.js +809 -75
- package/dist/react/index.d.ts +13 -3
- package/dist/react.js +14 -14
- package/dist/register-impl-AxIlqjo7.js +145 -0
- package/dist/{solid-BCJUQqwS.js → solid-BGYSJyHG.js} +4901 -4691
- package/dist/{solid-BdENBcEq.js → solid-Dt2nEwfl.js} +4406 -4320
- package/dist/solid.js +1 -1
- package/dist/solid.server.js +1 -1
- package/dist/{variant-aurora-DOb_o4pC.js → variant-aurora-B44X69AG.js} +1 -1
- package/dist/{variant-aurora-BKHHiOgL.js → variant-aurora-BRhV8Flk.js} +2 -2
- package/dist/{variant-aurora-1wMcXRUS.js → variant-aurora-Ds5ou9BH.js} +2 -2
- package/dist/{variant-custom-DWSSgmd2.js → variant-custom-B20FhtYj.js} +2 -2
- package/dist/{variant-custom-CJGB0Kbw.js → variant-custom-BvA1Schl.js} +1 -1
- package/dist/{variant-custom-Co0CkXJF.js → variant-custom-D6Rtvneb.js} +2 -2
- package/dist/{variant-wave-CEODq-9q.js → variant-wave-BUQtkryq.js} +1 -1
- package/dist/{variant-wave-C3-45cL6.js → variant-wave-BVYnqy9L.js} +2 -2
- package/dist/{variant-wave-jdXBnCUZ.js → variant-wave-DXC6_LcH.js} +2 -2
- package/dist/wire/encode.d.ts +79 -6
- package/dist/wire/files.d.ts +116 -0
- package/dist/wire/index.d.ts +3 -1
- package/dist/wire/media-types.d.ts +106 -0
- package/dist/wire.js +596 -368
- package/frameworks/react/index.tsx +9 -5
- package/llms-full.txt +12 -3
- package/package.json +2 -1
- package/src/agent-tooling/archetypes.ts +43 -12
- package/src/agent-tooling/integrations/anthropic.ts +29 -5
- package/src/agent-tooling/integrations/cloudflare.ts +5 -0
- package/src/agent-tooling/integrations/langgraph.ts +13 -0
- package/src/agent-tooling/integrations/mastra.ts +46 -5
- package/src/agent-tooling/integrations/mock.ts +4 -0
- package/src/agent-tooling/integrations/ollama.ts +7 -0
- package/src/agent-tooling/integrations/openai.ts +2 -0
- package/src/agent-tooling/integrations/openrouter.ts +2 -0
- package/src/agent-tooling/integrations/pi.ts +26 -1
- package/src/agent-tooling/integrations/pydantic-ai.ts +37 -3
- package/src/agent-tooling/integrations/vercel-ai-sdk.ts +42 -6
- package/src/agent-tooling/mcp/tools/scaffold.ts +853 -32
- package/src/agent-tooling/registry.ts +57 -0
- package/src/agent-tooling/types.ts +132 -0
- package/src/components/chat-thread.tsx +10 -1
- package/src/components/context.tsx +18 -12
- package/src/components/conversation-item.tsx +6 -0
- package/src/components/model-switcher.tsx +38 -8
- package/src/components/toast.tsx +6 -1
- package/src/elements/button.tsx +26 -3
- package/src/elements/chat.tsx +28 -2
- package/src/elements/default-input.tsx +128 -5
- package/src/elements/element-meta.json +30 -4
- package/src/elements/element-types.d.ts +12 -6
- package/src/elements/menu.tsx +30 -3
- package/src/elements/slot-text.ts +73 -0
- package/src/elements/slots.ts +10 -0
- package/src/index.ts +9 -5
- package/src/wire/encode.ts +229 -14
- package/src/wire/files.ts +342 -0
- package/src/wire/index.ts +21 -0
- package/src/wire/media-types.ts +344 -0
- package/dist/elements/chunks/arrow-left-Bb46H7w9.js +0 -1
- package/dist/elements/chunks/check-C8desc_h.js +0 -1
- package/dist/elements/chunks/chevron-down-DKtc2HNB.js +0 -1
- package/dist/elements/chunks/chevron-right-Dw4GPhwL.js +0 -1
- package/dist/elements/chunks/circle-BeokBsvB.js +0 -1
- package/dist/elements/chunks/circle-check-wJSqt859.js +0 -1
- package/dist/elements/chunks/context-WHDl-xSx.js +0 -1
- package/dist/elements/chunks/conversation-list-CdR14_Y4.js +0 -1
- package/dist/elements/chunks/default-input-CSZqUM81.js +0 -1
- package/dist/elements/chunks/message-square-CnpAm_P0.js +0 -1
- package/dist/elements/chunks/model-switcher-CclaJEHt.js +0 -1
- package/dist/elements/chunks/x-D1qx0aLd.js +0 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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. */
|
package/src/elements/menu.tsx
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
|
|
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
|
-
|
|
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
|
+
}
|
package/src/elements/slots.ts
CHANGED
|
@@ -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
|
|
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 —
|
|
12
|
-
// type for every public component — lives on `@kitn.ai/ui/solid`
|
|
13
|
-
// which is its own build target so only Solid consumers pay for
|
|
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
|
//
|