kai-cards
Turns server-sent interactions into live cards and routes what the user does with them back to your app.
- Shadow DOM
- Property-driven API
- Built-in card types
- Extensible type registry
- CardFallback for unknown types
Preview
Section titled “Preview”Set all three in JavaScript — they’re arrays and objects, so none work as HTML attributes:
<kai-cards id="cards"></kai-cards>
<script type="module"> import '@kitn.ai/ui/web-components';
await customElements.whenDefined('kai-cards');
const el = document.getElementById('cards');
el.cards = [ { type: 'confirm', id: 'deploy', title: 'Deploy to production?', data: { body: 'Apply 3 migrations?', tone: 'warning', actions: [{ id: 'go', label: 'Deploy', style: 'primary', default: true }, { id: 'no', label: 'Cancel' }] } }, ];
el.policy = { onAction: (cardId, action, payload) => console.log('action', cardId, action, payload), onSubmit: (cardId, data) => console.log('submit', cardId, data), onOpen: (url, target) => window.open(url, target === 'tab' ? '_blank' : '_self'), onError: (cardId, message) => console.warn('card error', cardId, message), };</script>cards— array ofCardEnvelopeobjects:{ type, id, data, title?, resolution? }. Thetypestring selects which built-in child element renders the envelope.policy:{ onAction, onSubmit, onOpen, onDismiss, … }. Provide only the handlers you need; swap it after mount, since it is read at event time.types—{ 'my-type': 'my-card-element' }merges over the built-in registry; override a built-in by reusing its key.- Resolved cards: set
resolutionon an envelope to render it read-only after the user has acted: terminal{ kind: 'action', action, payload? }and{ kind: 'submit', data }, deferred{ kind: 'dismissed' }, and{ kind: 'expired', reason? }.
Dismiss and recover
Section titled “Dismiss and recover”Add dismissible: true to a confirm, choice, form, or tasks card’s data to show a × on it. Dismissing a card defers it rather than deleting it: the card collapses to a compact, re-openable stub — “Proposed: Deploy to production? — dismissed · Reopen” — and onDismiss(cardId) fires.
Defer, don’t delete: a user who waves a card away may want it back, and only your app knows when the moment has passed.
cards.policy = { onDismiss: (cardId) => console.log('dismissed', cardId), onReopen: (cardId) => console.log('reopen requested', cardId),};When the user taps Reopen on the stub, onReopen(cardId) fires and your handler decides: bring the card back live, or mark it expired to end it there.
dismissRecovery() — the wiring, done for you
Section titled “dismissRecovery() — the wiring, done for you”dismissRecovery() builds the onDismiss / onReopen pair against your card store. It writes the dismissed resolution, offers an Undo toast, and applies a default reopen rule (live unless the card is terminal or stale):
import { dismissRecovery, toast } from '@kitn.ai/ui';
const recovery = dismissRecovery({ get: () => cards.cards, set: (next) => { cards.cards = next; }, // Inject a toast adapter for the "Dismissed · Undo" affordance. toast: { show: ({ message, action, durationMs }) => { const t = toast(message, { duration: durationMs, action: action && { label: action.label, onAction: action.onClick }, }); return { dismiss: t.dismiss }; }, }, staleAfterMs: 5 * 60_000, // a card dismissed over 5 minutes ago can't reopen});
cards.policy = { ...recovery, onAction, onSubmit };onDismissstamps{ kind: 'dismissed', at }on the card (a new array reference, so a Solid/React host re-renders) and shows the Undo toast, which restores whatever resolution the card had before.onReopenclears the resolution when the card is re-openable, otherwise stamps{ kind: 'expired' }. Override the rule withisReopenable, or cap it withstaleAfterMs.
The toast is injected, never imported, so cards stay decoupled from the toast module: pass any adapter with a show() method, or omit toast to skip the Undo affordance.
Examples
Section titled “Examples”Mixed Card Stream
Section titled “Mixed Card Stream”Single Confirm Card
Section titled “Single Confirm Card”Resolved Card
Section titled “Resolved Card”Pass resolution to lock an envelope in its read-only view, which is what rehydrating conversation history needs.
Unknown Type Fallback
Section titled “Unknown Type Fallback”| Property | Type | Default | Notes |
|---|---|---|---|
| theme | "light" | "dark" | "auto" | 'auto' | Color mode (`auto` follows prefers-color-scheme). |
| cards | — | The stream of card envelopes to render. Set as a JS PROPERTY: `el.cards = [...]`. | |
| types | Record<string, string> | — | Card type→element tag overrides/additions, merged over the built-ins. JS property: `el.types`. |
| schemas | Record<string, object> | — | Card-type JSON Schemas keyed by envelope type; validates each card's `data`. JS property: `el.schemas`. |
| policy | — | Optional CardPolicy handling child events. Property: `el.policy`. | |
| validateCards | boolean | true | Validate each card's `data` against its schema before rendering. Default `true`; opt out with `validate-cards="false"`. |
Methods
Section titled “Methods”| Method | Signature | Notes |
|---|---|---|
| resolve | (cardId: string, resolution: CardResolution): void | Programmatically resolve a child card by id: set that envelope's `resolution` so the child re-renders into its read-only/resolved view. The imperative twin of the consumer mutating the cards array. No-op for an unknown id. |
| dismiss | (cardId: string): void | Collapse a card to its re-openable stub from the host side. Convenience for `resolve(cardId, { kind: 'dismissed' })`. |
| getCard | (cardId: string): HTMLElement | null | Return the live child element node for a card id (or null) so consumers can call that card's own methods (focus/expand/…) without a shadow-DOM query. |
Composed from
Section titled “Composed from”This element wraps these SolidJS components — reach for them directly when you need finer control than the props expose.