UNPKG

@kitn.ai/ui

Version:

Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS.

667 lines (500 loc) 157 kB
<!-- AUTO-GENERATED by scripts/gen-llms.mjs — do not edit by hand. Run `npm run build`. --> # @kitn.ai/ui — Full Reference # @kitn.ai/ui > Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. 78 custom elements, every one prefixed `kai-` (e.g. `<kai-chat>`, `<kai-artifact>`): streaming responses, markdown + code rendering, reasoning/tool panels, attachments, conversation sidebar, voice input. Zero framework dependency for consumers; the SolidJS runtime it is authored in is bundled in, so the host needs nothing. ## Install ```bash npm install @kitn.ai/ui # SolidJS consumers also need the peer dep: npm install solid-js ``` ## #1 rule: array/object data goes on JS PROPERTIES, not HTML attributes This is the single most common mistake. Arrays and objects (`messages`, `models`, `context`, `suggestions`, `triggers`, …) MUST be assigned as JavaScript properties on the element. They CANNOT be passed as HTML attributes — an HTML attribute is always a string and will be ignored or mis-parsed. ```js const chat = document.querySelector('kai-chat'); chat.messages = [{ id: '1', role: 'assistant', content: 'Hi!' }]; // ✅ property ``` ```html <kai-chat messages="[...]"></kai-chat> <!-- ❌ never works --> ``` Only scalar values (string/number/boolean) work as attributes (e.g. `placeholder`, `loading`, `theme`). ## Two layers **Layer 1 — batteries-included web components** (`import '@kitn.ai/ui/elements'`): Drop an element into any framework (React, Vue, plain HTML). Data in via JS properties; interactions out via non-bubbling CustomEvents. - `<kai-chat>` — full chat UI (message list + prompt input). The primary starting point. - `<kai-conversations>` — sidebar conversation browser with group support. - `<kai-prompt-input>` — standalone composer with send button. **Layer 2 — composable primitives** (`import { … } from '@kitn.ai/ui'`): All 78 elements are also exported individually. Use them for custom layouts or features `<kai-chat>` does not expose (ChainOfThought, FeedbackBar, ThinkingBar, VoiceInput, …). Your bundler tree-shakes the rest. ## Key rules for the web components 1. **Array/object data = JS properties** (see above). Scalars may be attributes. 2. **Events are non-bubbling `CustomEvent`s** — listen directly on the element: `chat.addEventListener('kai-submit', (e) => console.log(e.detail.value))` 3. **`theme` attribute** (`'light' | 'dark' | 'auto'`) works on every element. Default `auto` follows `prefers-color-scheme`. 4. **Theming via CSS custom properties** — override `--kai-color-*` tokens on `:root`; they pierce Shadow DOM. ## ChatMessage schema (required for `<kai-chat>`) ```ts interface ChatMessage { id: string; role: 'user' | 'assistant'; content: string; reasoning?: { text: string; label?: string }; tools?: ToolPart[]; attachments?: AttachmentData[]; actions?: ('copy' | 'like' | 'dislike' | 'regenerate' | 'edit')[]; } ``` ## Framework wiring **Plain HTML / CDN** ```html <script type="module" src="https://unpkg.com/@kitn.ai/ui/elements"></script> <kai-chat style="display:block;height:100vh"></kai-chat> <script type="module"> const chat = document.querySelector('kai-chat'); chat.messages = []; </script> ``` **React** — typed wrappers auto-set properties and expose `on<Event>` props: ```tsx import { Chat } from '@kitn.ai/ui/react'; <Chat messages={messages} onSubmit={(e) => send(e.detail.value)} /> ``` **Vue** — use the element directly; pass arrays via `.prop`: ```vue <kai-chat :messages.prop="messages" @kai-submit="send" /> ``` ## Theming ```css :root { --kai-color-background: #0f0f0f; --kai-color-primary: #7c3aed; --kai-color-muted: #1e1e1e; } ``` For plain HTML/CDN: `<link rel="stylesheet" href="…/@kitn.ai/ui/theme.tokens.css">`. For Tailwind builds: `@import "@kitn.ai/ui/theme.css"` in your CSS. ## Docs - Full element reference (all 78 elements, every prop/event): ./llms-full.txt — https://kitn.dev/llms-full.txt - Machine-readable Custom Elements Manifest: https://unpkg.com/@kitn.ai/ui/dist/custom-elements.json - Working examples: https://github.com/kitn-ai/ui/tree/main/examples - Storybook: https://storybook.kitn.dev - Repository: https://github.com/kitn-ai/ui --- ## How to build a chat app in 5 steps ### 1 — Install ```bash npm install @kitn.ai/ui ``` ### 2 — Pick your layer Drop-in: use `<kai-chat>` for a full chat UI in one tag (`import '@kitn.ai/ui/elements'`). Composable: combine `<kai-message>`, `<kai-prompt-input>`, `<kai-reasoning>`, … in your own markup. ### 3 — Handle `submit` and stream ```js import '@kitn.ai/ui/elements'; const chat = document.querySelector('kai-chat'); chat.messages = []; chat.addEventListener('kai-submit', async (e) => { const userText = e.detail.value; // Append the user message (new array — see streaming note) const history = [...chat.messages, { id: crypto.randomUUID(), role: 'user', content: userText }]; chat.messages = history; chat.loading = true; // Add an empty assistant placeholder to stream into const aid = crypto.randomUUID(); chat.messages = [...history, { id: aid, role: 'assistant', content: '' }]; let answer = ''; for await (const token of streamFromYourAPI(history)) { answer += token; chat.messages = chat.messages.map((m) => (m.id === aid ? { ...m, content: answer } : m)); } chat.loading = false; }); ``` ### 4 — Wire optional features - Reasoning: add `reasoning: { text: '…' }` to an assistant message. - Tool calls: add `tools: [{ type: 'search', state: 'output-available', input: {…}, output: {…} }]`. - Model switcher: `chat.models = [{ id: 'gpt-4o', name: 'GPT-4o' }]; chat.currentModel = 'gpt-4o';` — listen for `modelchange`. - Token meter: `chat.context = { usedTokens: 1200, maxTokens: 128000 };`. - History sidebar: add `<kai-conversations>`; listen for `select` and `newchat`. ### 5 — Theme Override `--kai-color-*` tokens on `:root` (they pierce Shadow DOM). --- ## Streaming recipe (critical) To update messages while streaming, **reassign a NEW array containing a NEW message object** on every chunk. Mutating an existing message object in place will NOT trigger a re-render: ```js // ✅ re-renders chat.messages = chat.messages.map((m) => (m.id === id ? { ...m, content: next } : m)); // ❌ does NOT re-render chat.messages[i].content = next; ``` The same rule applies to every array/object property (`models`, `context`, `suggestions`, …): replace, don't mutate. --- ## Element reference (78 elements, generated from custom-elements.json) Every element also accepts the `theme` attribute. Array/object properties are marked with a `—` attribute: they must be set as JS properties. ### `kai-agent-card` / `AgentCard` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `name` | `name` | `undefined \| string` | The agent's name — the primary label. Attribute: `name`. | | `active` | `active` | `undefined \| false \| true` | Selected / focused state: highlighted border + surface. Attribute: `active`. | | `needsAttention` | `needs-attention` | `undefined \| false \| true` | Raise a prominent "Needs you" pill plus a glowing amber edge — the attention-routing signal that pulls focus to this agent. Attribute: `needs-attention`. | | `status` | — | `undefined \| { tone: "working" \| "idle" \| "done" \| "error" \| "blocked"; label?: undefined \| string; pulse?: undefined \| false \| true }` | Run status — a JS PROPERTY (object), not an attribute. Shape: `{ tone, label?, pulse? }`, where `tone` is one of `working` \| `idle` \| `done` \| `error` \| `blocked` (maps to the kit's tool hues), `label` is an optional short string beside the dot, and `pulse` animates the dot. Set it with `el.status = { tone: 'working', label: 'Working', pulse: true }`. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-activate` | `CustomEvent` | The card was activated — clicked, or Enter / Space while focused. Promote this agent back to focus. | | `kai-menu` | `CustomEvent` | The trailing "..." kebab was clicked. The consumer opens its own menu; the card only surfaces the affordance (the click does not also activate the card). | **Styleable parts** (restyle from outside via `kai-agent-card::part(name)`): | Part | Description | |---|---| | `::part(status)` | The leading tone-colored status dot. — `kai-agent-card::part(status) { width: 0.625rem; height: 0.625rem }` | | `::part(menu)` | The trailing overflow ("...") menu button. — `kai-agent-card::part(menu) { opacity: 1 }` | --- ### `kai-artifact` / `Artifact` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `src` | `src` | `undefined \| string` | URL the preview iframe frames. Consumer-controlled. | | `files` | — | `{ path: string; url?: undefined \| string; code?: undefined \| string; language?: undefined \| string; type?: undefined \| "html" \| "pdf" \| "image" \| "other"; additions?: undefined \| number; deletions?: undefined \| number; status?: undefined \| "added" \| "modified" \| "deleted" \| "renamed" \| "untracked" }[]` | Files for the Code tab tree + each file's preview `url`. Set as a JS property (array). | | `tab` | `tab` | `undefined \| "preview" \| "code"` | Controlled active tab: `preview` or `code`. When set, the artifact follows it (re-asserted on change). Leave unset for an uncontrolled tab (see `defaultTab`). | | `defaultTab` | `default-tab` | `undefined \| "preview" \| "code"` | Uncontrolled INITIAL tab (used only when `tab` is unset). Default `preview`. Seeds the starting tab; the user can then switch freely without the consumer re-asserting a controlled `tab`. | | `activeFile` | `active-file` | `undefined \| string` | Selected file path — syncs the tree highlight, Code source, and preview. | | `sandbox` | `sandbox` | `undefined \| string` | iframe `sandbox` override. Secure default `allow-scripts allow-forms` (NOT `allow-same-origin`). | | `iframeTitle` | `iframe-title` | `undefined \| string` | Accessible title for the preview iframe. | | `maximized` | `maximized` | `undefined \| false \| true` | Reflects the artifact's own maximized view-state (usually driven by the protocol). | | `expandable` | `expandable` | `undefined \| false \| true` | Show the expand-to-fill button (OPT-IN). | | `openInTab` | `open-in-tab` | `undefined \| false \| true` | Show the open-in-new-tab button (OPT-IN). | | `noNav` | `no-nav` | `undefined \| false \| true` | Hide back/forward. | | `noReload` | `no-reload` | `undefined \| false \| true` | Hide reload. | | `noHome` | `no-home` | `undefined \| false \| true` | Hide home. | | `noPathField` | `no-path-field` | `undefined \| false \| true` | Hide the address field. | | `noTabs` | `no-tabs` | `undefined \| false \| true` | Hide the Preview\|Code toggle. | | `standalone` | `standalone` | `undefined \| false \| true` | Standalone chrome: rounded corners + border (else square, borderless in-panel). | | `readonlyPath` | `readonly-path` | `undefined \| false \| true` | Show the address but make it read-only (visible, nav-tracking, non-editable). | | `displayUrl` | `display-url` | `undefined \| string` | Friendly address shown in the path field instead of the real current url (read-only, non-navigable). Use when the framed url is not consumer-facing (e.g. a `data:` blob) so a clean address shows instead of leaking it. Scalar string: set as the `display-url` attribute or the `displayUrl` property. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-file-select` | `CustomEvent<{ path: string }>` | Fired when a file is selected. `detail.path`. | | `kai-maximize-change` | `CustomEvent<{ maximized: false \| true }>` | Artifact's own maximize button toggled (consumer-observable; non-bubbling). | | `kai-navigate` | `CustomEvent<{ url: string }>` | Fired when the preview navigates. `detail.url` = the new location. | | `kai-tab-change` | `CustomEvent<{ tab: "preview" \| "code" }>` | Fired when the Preview\|Code tab changes. `detail.tab`. | --- ### `kai-attachments` / `Attachments` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `items` | — | `{ id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]` | The attachments to render. Set as a JS property (array). | | `variant` | `variant` | `undefined \| "grid" \| "inline" \| "list"` | Layout: `grid` = visual tiles, `inline` = icon + label chips, `list` = rows. | | `hoverCard` | `hover-card` | `undefined \| false \| true` | Wrap each item in a hover card that previews its details. | | `removable` | `removable` | `undefined \| false \| true` | Show a remove button per item; clicking it fires a `kai-remove` event. | | `showMediaType` | `show-media-type` | `undefined \| false \| true` | Also show the media type beneath the filename (non-grid variants). | | `emptyText` | `empty-text` | `undefined \| string` | Text shown when `items` is empty. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-remove` | `CustomEvent<{ id: string }>` | A remove button was clicked. | **Styleable parts** (restyle from outside via `kai-attachments::part(name)`): | Part | Description | |---|---| | `::part(preview)` | The image shown in an attachment’s hover-card preview. Bounded by default (max ~320×256, aspect preserved) so a large image never blows up the card — raise or lower the cap from outside. — `kai-attachments::part(preview) { max-width: 32rem; max-height: 24rem }` | --- ### `kai-avatar` / `Avatar` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `src` | `src` | `undefined \| string` | Image URL/data-URI. When absent, the `fallback` initials show instead. | | `alt` | `alt` | `undefined \| string` | Alt text for the image. Defaults to `fallback`. | | `fallback` | `fallback` | `undefined \| string` | Short text shown when there's no image — usually initials (e.g. "JD", "AI"). | | `size` | `size` | `undefined \| "sm" \| "md" \| "lg"` | Size token: `sm` \| `md` (default) \| `lg`. | _No events._ --- ### `kai-badge` / `Badge` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `variant` | `variant` | `undefined \| "default" \| "count" \| "citation"` | `default` (muted pill) · `count` (compact number badge) · `citation` (filled primary, for inline citation markers). Defaults to `default`. | _No events._ **Styleable parts** (restyle from outside via `kai-badge::part(name)`): | Part | Description | |---|---| | `::part(badge)` | The badge pill. Restyle its background, color, or shape; the `variant` prop (default/count/citation) sets the defaults. — `kai-badge::part(badge) { background: var(--color-primary); color: var(--color-primary-foreground) }` | --- ### `kai-button` / `Button` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `variant` | `variant` | `undefined \| "default" \| "subtle" \| "ghost" \| "outline" \| "destructive"` | Visual style. `default` (filled), `subtle` (muted text, hover tint — the toolbar icon look), `ghost` (transparent, hover fill), `outline`, or `destructive`. Defaults to `default`. | | `size` | `size` | `undefined \| "sm" \| "md" \| "lg" \| "icon" \| "icon-sm"` | Size token. `icon` / `icon-sm` are square (for icon-only buttons); `sm` / `md` / `lg` size text buttons. Defaults to `md`. | | `icon` | `icon` | `undefined \| string` | Leading icon: a named icon (e.g. `"mic"`, `"plus"`), an image URL/data-URI, or plain text. Renders before any slotted label. | | `iconTrailing` | `icon-trailing` | `undefined \| string` | Trailing icon, after the label (e.g. `"chevron-down"` for a menu affordance). | | `label` | `label` | `undefined \| string` | Accessible name. REQUIRED for icon-only buttons (no visible text); ignored when you slot visible text, which already names the button. | | `disabled` | `disabled` | `undefined \| false \| true` | Disable the button (non-interactive, dimmed). | | `full` | `full` | `undefined \| false \| true` | Stretch the button to the full width of its container (a block button) — e.g. a card CTA or a stacked action. Attribute: `full`. | | `align` | `align` | `undefined \| "start" \| "center" \| "end"` | Justify the button's content: `start`, `center` (default), or `end`. Combine with `full` for a full-width, left-aligned button. | | `type` | `type` | `undefined \| "button" \| "submit" \| "reset"` | Native button `type`. Defaults to `button` (so it never submits a form). | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-click` | `CustomEvent` | The button was activated (pointer or keyboard). Carries no detail. The native `click` also bubbles (composed) for consumers who prefer it. | **Slots** (project your own markup via `slot="name"` on a light-DOM child): | Slot | Mode | Description | |---|---|---| | `icon` | replace | A custom leading icon (any inline SVG, inherits `currentColor`). Wins over the `icon` prop. | **Styleable parts** (restyle from outside via `kai-button::part(name)`): | Part | Description | |---|---| | `::part(button)` | The button element. Restyle radius, padding, colors, or weight from outside; the `variant`/`size` props set the defaults. — `kai-button::part(button) { border-radius: 9999px; font-weight: 600 }` | --- ### `kai-card` / `Card` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `appearance` | `appearance` | `undefined \| "outlined" \| "filled" \| "plain" \| "accent"` | Surface treatment: `outlined` (default) \| `filled` \| `plain` \| `accent`. Attribute: `appearance`. | | `orientation` | `orientation` | `undefined \| "vertical" \| "horizontal" \| "responsive"` | `vertical` (default, media on top) \| `horizontal` (media at the start) \| `responsive` (horizontal when the card's container is wide enough, else vertical — a container query on the card's own width). Attribute: `orientation`. | | `collapse` | `collapse` | `undefined \| string` | The card width below which a `responsive` card collapses to vertical and the footer actions stack. A CSS length; default `28rem`. Attribute: `collapse`. | | `dense` | `dense` | `undefined \| false \| true` | Tighter spacing for dense lists. Attribute: `dense`. | | `dismissible` | `dismissible` | `undefined \| false \| true` | Show a close (×) that hides the card and emits `kai-dismiss`. Attribute: `dismissible`. Off by default. | | `href` | `href` | `undefined \| string` | Render the whole card as a link. Attribute: `href`. Wins over `clickable`. | | `target` | `target` | `undefined \| string` | `target` for the `href` anchor. Attribute: `target`. | | `rel` | `rel` | `undefined \| string` | `rel` for the `href` anchor. Attribute: `rel`. | | `clickable` | `clickable` | `undefined \| false \| true` | Make the whole card a button (`role="button"`, Enter/Space, hover affordance) that emits `kai-card-click`. Attribute: `clickable`. Ignored when `href` is set. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-card-click` | `CustomEvent` | A `clickable`/`href` card was activated (click, or Enter/Space). | | `kai-dismiss` | `CustomEvent` | The card was dismissed via its × (it also hides itself). | **Slots** (project your own markup via `slot="name"` on a light-DOM child): | Slot | Mode | Description | |---|---|---| | `media` | inject | Full-bleed media (image/video/illustration) at the top (vertical) or start (horizontal). Clipped to the card corners. | | `header` | inject | Header content, e.g. a title. Rendered above the body. | | `header-actions` | inject | An actions cluster pinned to the end of the header row. | | `footer` | inject | Footer content rendered below the body. | | `footer-actions` | inject | Action buttons pinned to the end of the footer. Do NOT combine with a clickable/href card (nested interactive). | **Styleable parts** (restyle from outside via `kai-card::part(name)`): | Part | Description | |---|---| | `::part(card)` | The card root (a div, or an a when href is set). Restyle its radius, border, or background; set --kai-card-spacing for padding/gaps (the dense prop sets the compact default). — `kai-card::part(card) { border-radius: 1rem; --kai-card-spacing: 1.5rem }` | | `::part(media)` | The full-bleed media region. Cap or crop it from outside (e.g. a fixed height with object-fit). — `kai-card::part(media) { max-height: 12rem }` | | `::part(header)` | The header row (header content + header-actions). Add a divider or adjust its alignment. — `kai-card::part(header) { border-bottom: 1px solid var(--color-border) }` | | `::part(body)` | The default-slot body region. — `kai-card::part(body) { font-size: 0.9375rem }` | | `::part(footer)` | The footer row (footer content + footer-actions). — `kai-card::part(footer) { border-top: 1px solid var(--color-border) }` | | `::part(dismiss)` | The dismiss (×) button shown when dismissible. Recolor or reposition it from outside. — `kai-card::part(dismiss) { color: var(--color-muted-foreground) }` | --- ### `kai-cards` / `Cards` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `cards` | — | `undefined \| { 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 } }[]` | The stream of card envelopes to render. Set as a JS PROPERTY: `el.cards = [...]`. | | `types` | — | `undefined \| Record<string, string>` | Optional type→tag overrides/additions (merged over the built-ins). Property: `el.types`. Typed as a plain string map (not the `CardTagMap` alias) so the generated React wrapper inlines it instead of emitting an unresolved named type. | | `policy` | — | `undefined \| { onSubmit?: undefined \| (cardId: string, data: unknown) => void; onAction?: undefined \| (cardId: string, action: string, payload?: unknown) => void; onSendPrompt?: undefined \| (text: string, opts: { mode: "compose" \| "send"; context?: unknown; }) => void; onOpen?: undefined \| (url: string, target: "tab" \| "artifact") => void; onState?: undefined \| (cardId: string, patch: unknown) => void; onDismiss?: undefined \| (cardId: string) => void; onReopen?: undefined \| (cardId: string) => void; onError?: undefined \| (cardId: string, message: string) => void; maxSendPromptMode?: undefined \| "compose" \| "send" }` | Optional CardPolicy handling child events. Property: `el.policy`. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-card-resolved` | `CustomEvent<{ cardId: string; resolution: { 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 } }>` | A child card transitioned to a resolved/deferred state (an action was chosen, a form/tasks submission landed, or it was dismissed) — re-emitted off the host as a non-bubbling convenience event so a consumer can observe resolution centrally without diffing the cards array. `detail` = `{ cardId, resolution }`. (A `reopen` un-resolves a card and has no `CardResolution`, so it does NOT fire this — observe reopen via the underlying bubbling `kai-card` event.) | --- ### `kai-chain-of-thought` / `ChainOfThought` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `steps` | — | `{ label: string; content?: undefined \| string; id?: undefined \| string }[]` | The reasoning steps. Set as a JS property. Compound sub-parts collapse to this one data model (Route 1). Each `{ label, content?, id? }`. | | `type` | `type` | `undefined \| "single" \| "multiple"` | Open mode: `'multiple'` (default — any number of steps open at once) or `'single'` (at most one open; opening a step closes the others). | | `value` | — | `undefined \| string \| string[]` | Controlled open step key(s). When set, it WINS over user interaction (the consumer owns the open set). String in `single` mode, string[] in `multiple` mode. Set as a JS property. | | `defaultValue` | — | `undefined \| string \| string[]` | Uncontrolled INITIAL open step key(s) — seeds which steps render expanded. Ignored once `value` is provided. Set as a JS property. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-value-change` | `CustomEvent<{ value: string \| string[] }>` | The open set changed — by user click OR an expand()/collapse()/toggle() call. `value` is a string in `single` mode, a string[] in `multiple` mode. (Maps Radix Accordion's onValueChange.) | --- ### `kai-chat` / `Chat` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `messages` | — | `{ id: string; role: "user" \| "assistant"; content: string; reasoning?: undefined \| { text: string; label?: undefined \| string }; tools?: undefined \| { type: string; state: "input-streaming" \| "input-available" \| "output-available" \| "output-error"; input?: undefined \| Record<string, unknown>; output?: undefined \| Record<string, unknown>; toolCallId?: undefined \| string; errorText?: undefined \| string }[]; attachments?: undefined \| { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]; 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" }[]` | The full message thread to render, newest last. Each entry carries its role, content, and optional reasoning/tools/attachments/actions. Set as a JS property (`el.messages = [...]`). | | `value` | — | `undefined \| string \| ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> } })[]` | Value of the input. A **string** is controlled (the host owns the text and updates it on `kai-value-change`). A **ComposerDoc** is a one-time seed that pre-populates pills; the user then edits freely. Leave unset for uncontrolled. | | `placeholder` | `placeholder` | `undefined \| string` | Placeholder text shown in the empty input. | | `loading` | `loading` | `undefined \| false \| true` | When true, shows the loading/streaming state and disables submit (use while awaiting the assistant's reply). | | `suggestions` | — | `undefined \| string[]` | Starter prompts shown above the input when the thread is empty. Clicking one follows `suggestionMode`. Set as a JS property. | | `suggestionMode` | `suggestion-mode` | `undefined \| "submit" \| "fill"` | What clicking a suggestion does: `'submit'` (default) sends it immediately as if typed and submitted; `'fill'` just places it in the input. | | `persistSuggestions` | `persist-suggestions` | `undefined \| false \| true` | Keep suggestions visible after the conversation starts. By default suggestions are conversation starters and hide once `messages` is non-empty; set this to keep them always shown. Default false. | | `proseSize` | `prose-size` | `undefined \| "sm" \| "lg" \| "xs" \| "base"` | Body/prose font scale for rendered markdown (`'xs' \| 'sm' \| 'base' \| 'lg'`). Defaults to `'sm'`. | | `codeTheme` | `code-theme` | `undefined \| string` | Shiki theme name for syntax-highlighted code blocks (e.g. `'github-dark-dimmed'`). | | `codeHighlight` | `code-highlight` | `undefined \| false \| true` | Enable Shiki syntax highlighting in code blocks. Turn off to render plain `<pre>` blocks (lighter, no highlighter load). Default true. | | `chatTitle` | `chat-title` | `undefined \| string` | Optional header title shown on the left of the header. | | `models` | — | `undefined \| { id: string; name: string; provider?: undefined \| string; description?: undefined \| string; group?: undefined \| string }[]` | Optional model list. When set (>1 model) a ModelSwitcher is shown in the header and a `kai-model-change` event fires on selection. | | `currentModel` | `current-model` | `undefined \| string` | The currently selected model id (pairs with `models`). | | `context` | — | `undefined \| { usedTokens: number; maxTokens: number; inputTokens?: undefined \| number; outputTokens?: undefined \| number; estimatedCost?: undefined \| number }` | Optional context-window token usage. When set, a Context token meter is shown in the header. | | `scrollButton` | `scroll-button` | `undefined \| false \| true` | Show the scroll-to-bottom button inside the scroll area. Default true. | | `headerStart` | `header-start` | `undefined \| false \| true` | Whether the host has `slot="header-start"` content (left of the title) — set by the `<kai-chat>` facade so a custom control forces the header open. | | `headerEnd` | `header-end` | `undefined \| false \| true` | Whether the host has `slot="header-end"` content (right of the controls). | | `headerFull` | `header-full` | `undefined \| false \| true` | REPLACE — full custom header in place of the built-in title/model/context bar. | | `sidebar` | `sidebar` | `undefined \| false \| true` | INJECT — left sidebar column (e.g. a conversation list / your own nav). | | `empty` | `empty` | `undefined \| false \| true` | REPLACE — custom zero-state rendered in the message area while the thread is empty (replaces the empty message list only; the composer and its suggestions still render). | | `composer` | `composer` | `undefined \| false \| true` | REPLACE — full custom composer in place of the built-in prompt input. The projected content wires its own submit (the data-flow boundary). | | `composerActions` | `composer-actions` | `undefined \| false \| true` | INJECT — accessory row just above the composer (e.g. extra actions). | | `footer` | `footer` | `undefined \| false \| true` | INJECT — footer row below the composer (disclaimers, token meter, …). | | `search` | `search` | `undefined \| false \| true` | Show a Search (Globe) button in the input toolbar; fires a `search` event. | | `voice` | `voice` | `undefined \| false \| true` | Show a Voice (Mic) button in the input toolbar; fires a `voice` event. | | `triggers` | — | `undefined \| { char: string; kind: string; items?: undefined \| { id: string; label: string; icon?: undefined \| string; description?: undefined \| string; group?: undefined \| string; kind?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> }[] }[]` | Rich entity triggers — each `{ char, kind, items }` opens a caret-anchored menu that inserts an atomic pill (`/` skills, `@` agents/plugins). Set as a JS property; forwarded to the input. | | `kindIcons` | — | `undefined \| Record<string, string>` | Default icon per entity kind (kind → image src) for pills/menu items. | | `actionsReveal` | `actions-reveal` | `undefined \| "always" \| "hover"` | Whether each message's action bar is always visible (`'always'`, default) or only revealed on hover of that message row (`'hover'`). | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-attachments-change` | `CustomEvent<{ attachments: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[] }>` | The staged attachments changed (file added or removed). Carries the full current list so a consumer can react in real time. | | `kai-message-action` | `CustomEvent<{ messageId: string; action: string; state?: undefined \| "on" \| "off" }>` | 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. | | `kai-model-change` | `CustomEvent<{ modelId: string }>` | The header model switcher changed. | | `kai-search` | `CustomEvent<Record<string, never>>` | The Search button was clicked. | | `kai-submit` | `CustomEvent<{ value: string; attachments: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[] }>` | User submitted a message. | | `kai-suggestion-click` | `CustomEvent<{ value: string }>` | A suggestion chip was clicked (only in `suggestion-mode="fill"`). | | `kai-value-change` | `CustomEvent<{ value: string }>` | Fired on every input change. | | `kai-voice` | `CustomEvent<Record<string, never>>` | The Mic / voice button was clicked. | **Slots** (project your own markup via `slot="name"` on a light-DOM child): | Slot | Mode | Description | |---|---|---| | `header-start` | inject | Leading header controls, left of the title. | | `header-end` | inject | Trailing header controls. | | `header` | replace | Full custom header; replaces the built-in title/model/context bar. | | `sidebar` | inject | Left column (your nav / conversation list). Fixed width; use compose-your-own for resizable. | | `empty` | replace | Custom zero-state rendered in the message area while the thread is empty. Replaces the empty message list only — the composer and any suggestions still render. | | `composer` | replace | Full custom composer; you own submit + loading, drive the thread via messages. | | `composer-actions` | inject | Accessory row above the composer. | | `footer` | inject | Row below the composer (disclaimers, token meter). | **Styleable parts** (restyle from outside via `kai-chat::part(name)`): | Part | Description | |---|---| | `::part(header-bar)` | The built-in header bar (the title / model-switcher / context row that hosts the header-start/header-end inject slots). Restyle its height, padding, or gap from outside without replacing the whole header via the `header` slot. — `kai-chat::part(header-bar) { height: 3.5rem; padding-inline: 1rem; gap: 0.5rem }` | | `::part(header)` | Full custom header; replaces the built-in title/model/context bar. | | `::part(sidebar)` | Left column (your nav / conversation list). Fixed width; use compose-your-own for resizable. | | `::part(footer)` | Row below the composer (disclaimers, token meter). | --- ### `kai-checkpoint` / `Checkpoint` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `label` | `label` | `undefined \| string` | Optional text beside the icon. | | `tooltip` | `tooltip` | `undefined \| string` | Tooltip on hover. | | `variant` | `variant` | `undefined \| "default" \| "ghost" \| "outline"` | Visual button style. | | `size` | `size` | `undefined \| "sm" \| "md" \| "lg" \| "icon" \| "icon-sm"` | Button size (use an `icon*` size for an icon-only checkpoint). | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-select` | `CustomEvent` | The checkpoint was clicked. | --- ### `kai-choice` / `Choice` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `data` | — | `undefined \| Record<string, unknown>` | The choice definition (the CardEnvelope.data). Set as a JS PROPERTY: `el.data = { prompt, options:[…], allowOther?, submitLabel? }`. Import `ChoiceCardData` from `@kitn.ai/ui` for the full shape. | | `cardId` | `card-id` | `undefined \| string` | Stable card id correlating every emitted CardEvent. Attribute: `card-id`. | | `heading` | `heading` | `undefined \| string` | Heading rendered in the card chrome (= CardEnvelope.title). Attribute: `heading`. | | `resolution` | — | `undefined \| Record<string, unknown>` | Set when the user resolved this card; renders the read-only view. Property: `el.resolution = { kind:'action', action:'…' }`. | | `value` | `value` | `undefined \| string` | Controlled selection — the selected option id. When set, the consumer owns the current pick (RadioGroup `value`). Attribute: `value`. | | `defaultValue` | `default-value` | `undefined \| string` | Option id to pre-select on mount (uncontrolled seed). Attribute: `default-value`. | | `disabled` | `disabled` | `undefined \| false \| true` | Disable the whole radiogroup + Submit (e.g. while the agent is busy). Attribute: `disabled`. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-value-change` | `CustomEvent<{ value: string }>` | The selection changed BEFORE submit (a row click or the `select()` method). Distinct from the terminal `action` verb on the `kai-card` contract event. | --- ### `kai-coachmark` / `Coachmark` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `open` | `open` | `undefined \| false \| true` | Drive/observe open state (Shoelace-style: settable + reflected to the `open` attribute; the element still self-manages). Set `el.open = true`, or `<kai-coachmark open>`; listen for `kai-open-change`. | | `defaultOpen` | `default-open` | `undefined \| false \| true` | Initial open state on mount (uncontrolled seed). | | `headline` | `headline` | `undefined \| string` | The bold title. Named `headline` because `title` collides with the global `HTMLElement.title` attribute (it throws at registration). | | `badge` | `badge` | `undefined \| string` | A small badge pill beside the headline (e.g. "New"). | | `placement` | `placement` | `undefined \| string` | Floating placement relative to the anchor (default `bottom`). | | `tone` | `tone` | `undefined \| "error" \| "primary" \| "info" \| "success" \| "warning"` | Color tone: `primary` (default, theme accent), `info` (blue), `success` (green), `warning` (amber), or `error` (red) — reusing the kit's tool hues. | | `arrow` | `arrow` | `undefined \| false \| true` | Render the arrow that points at the anchor (default `true`). Set `arrow="false"` for a plain bubble with no pointer. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-dismiss` | `CustomEvent<Record<string, never>>` | The × dismiss button was pressed. The consumer records that this hint was seen so it won't show again. | | `kai-open-change` | `CustomEvent<{ open: false \| true }>` | The coachmark opened or closed (a method, the ×, or a driven `open`). | **Slots** (project your own markup via `slot="name"` on a light-DOM child): | Slot | Mode | Description | |---|---|---| | `content` | replace | The bubble body text shown under the headline. | **Styleable parts** (restyle from outside via `kai-coachmark::part(name)`): | Part | Description | |---|---| | `::part(bubble)` | The hint bubble panel. Restyle its background, radius, or padding from outside; the default is bg-primary. — `kai-coachmark::part(bubble) { border-radius: 1rem }` | | `::part(arrow)` | The arrow pointing at the anchor. Inherits the bubble color; recolor it alongside the bubble. — `kai-coachmark::part(arrow) { background: var(--color-accent) }` | | `::part(badge)` | The small badge pill beside the headline (e.g. "New"). — `kai-coachmark::part(badge) { text-transform: none }` | | `::part(title)` | The bold headline text. — `kai-coachmark::part(title) { font-size: 0.9375rem }` | | `::part(dismiss)` | The dismiss button. Recolor or reposition it from outside. — `kai-coachmark::part(dismiss) { color: var(--color-primary-foreground) }` | --- ### `kai-code-block` / `CodeBlock` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `code` | `code` | `string` | The source code to render. | | `language` | `language` | `undefined \| string` | Language grammar (e.g. `js`, `python`). Defaults to `tsx`. | | `codeTheme` | `code-theme` | `undefined \| string` | Shiki theme name. | | `codeHighlight` | `code-highlight` | `undefined \| false \| true` | Disable syntax highlighting (renders plain text, no Shiki). | | `proseSize` | `prose-size` | `undefined \| "sm" \| "lg" \| "xs" \| "base"` | Code text sizing. | _No events._ --- ### `kai-command` / `Command` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `items` | — | `undefined \| { id: string; label: string; icon?: undefined \| string; description?: undefined \| string; shortcut?: undefined \| string; group?: undefined \| string }[]` | Flat list of items. Set as a JS property — not an HTML attribute. | | `placeholder` | `placeholder` | `undefined \| string` | Placeholder text for the search input. | | `emptyLabel` | `empty-label` | `undefined \| string` | Label shown when no items match the current query. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-active-change` | `CustomEvent<{ id: undefined \| string }>` | Fired when the highlighted/active item changes — via Arrow keys or when filtering re-clamps the active row. `id` is the newly active item's id, or `undefined` when no item is active (e.g. the filtered list is empty). Lets a host preview the active item without committing a selection. | | `kai-query-change` | `CustomEvent<{ value: string }>` | Fired on every keystroke in the search input. | | `kai-select` | `CustomEvent<{ id: string }>` | Fired when the user selects an item (click or Enter). | **Styleable parts** (restyle from outside via `kai-command::part(name)`): | Part | Description | |---|---| | `::part(shortcut)` | The right-aligned per-row keyboard shortcut, rendered as kai-kbd key caps. Shown only when a row carries a `shortcut`. — `kai-command::part(shortcut) { opacity: 0.8 }` | --- ### `kai-compare` / `Compare` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `data` | — | `undefined \| Record<string, unknown>` | The compare definition (prompt + the two candidates). Set as a JS PROPERTY: `el.data = { prompt, candidates: [A, B], collapse? }`. Import `ResponseCompareData` from `@kitn.ai/ui` for the full shape. | | `compareId` | `compare-id` | `undefined \| string` | Stable id correlating every emitted event. Attribute: `compare-id`. | | `selection` | — | `undefined \| Record<string, unknown>` | Re-hydrate / control the user's pick. Set as a JS PROPERTY: `el.selection = { chosenId, rejectedIds }`. Renders the collapsed winner. | | `layout` | `layout` | `undefined \| "auto" \| "columns" \| "tabs"` | Layout: `'auto'` (default — columns when wide, tabs when narrow, by CONTAINER width) \| `'columns'` (side-by-side) \| `'tabs'` (pills to switch). Attribute: `layout`. | | `proseSize` | `prose-size` | `undefined \| "sm" \| "lg" \| "xs" \| "base"` | Prose/text size for the rendered candidates. Attribute: `prose-size`. | | `codeTheme` | `code-theme` | `undefined \| string` | Shiki theme for code blocks in the candidates. Attribute: `code-theme`. | | `codeHighlight` | `code-highlight` | `undefined \| false \| true` | Whether code blocks are syntax-highlighted. Attribute: `code-highlight`. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-compare-select` | `CustomEvent<{ chosenId: string; rejectedIds: string[]; at?: undefined \| number }>` | The user committed a pick. `detail` = `{ chosenId, rejectedIds, at }`. | | `kai-error` | `CustomEvent<{ compareId: string; message: string }>` | The definition was unusable. | | `kai-ready` | `CustomEvent<{ compareId: string }>` | Both candidates have settled and the pick is live. | --- ### `kai-composer` / `Composer` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `value` | — | `undefined \| string \| ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> } })[]` | Controlled value — string or a full ComposerDoc (set as JS property). | | `placeholder` | `placeholder` | `undefined \| string` | Placeholder text shown when the composer is empty. | | `disabled` | `disabled` | `undefined \| false \| true` | Disable the composer entirely (non-interactive). | | `loading` | `loading` | `undefined \| false \| true` | Show a loading/streaming state and block submit. | | `maxHeight` | `max-height` | `undefined \| string \| number` | Maximum height in px before the content scrolls. Default 240. | | `submitOnEnter` | `submit-on-enter` | `undefined \| false \| true` | Whether pressing Enter (without Shift) submits. Default true. | | `triggers` | — | `undefined \| { char: string; kind: string; items?: undefined \| { id: string; label: string; icon?: undefined \| string; description?: undefined \| string; group?: undefined \| string; kind?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> }[] }[]` | Trigger definitions — set as a JS property. | | `highlights` | — | `undefined \| (string \| { pattern: string; flags?: undefined \| string; class?: undefined \| string })[]` | Keyword highlight rules — set as a JS property. | | `kindIcons` | — | `undefined \| Record<string, string>` | Default icon per entity kind (kind → image URL/data-URI) for items without their own `icon`. Overrides the built-in agent/plugin glyphs. JS property. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-blur` | `CustomEvent<{ originalEvent: FocusEvent }>` | The composer lost focus. | | `kai-entity-add` | `CustomEvent<{ entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> } }>` | An entity pill was inserted into the composer. | | `kai-entity-remove` | `CustomEvent<{ entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> } }>` | An entity pill was deleted from the composer. | | `kai-focus` | `CustomEvent<{ originalEvent: FocusEvent }>` | The composer gained focus. `focus`/`blur` are NOT composed natively, so they don't escape the shadow root — these re-expose them on the host. (For `keydown`/`paste`/`focusin`/`focusout`, listen NATIVELY on `<kai-composer>`: they're composed and already cross the shadow boundary.) | | `kai-submit` | `CustomEvent<{ doc: ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> } })[]; text: string; entities: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> }[] }>` | The user submitted the composer (Enter or programmatic submit). | | `kai-trigger` | `CustomEvent<{ char: string; query: string; rect: DOMRect }>` | A trigger character was detected at the caret (e.g. `/` or `@`). | | `kai-trigger-close` | `CustomEvent<Record<string, never>>` | The active trigger was dismissed (Escape, space, or outside click). | | `kai-value-change` | `CustomEvent<{ doc: ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; pr