Skip to content
kitn AI/UI

Cards

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

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 of CardEnvelope objects: { type, id, data, title?, resolution? }. The type string 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 resolution on 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? }.

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 };
  • onDismiss stamps { 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.
  • onReopen clears the resolution when the card is re-openable, otherwise stamps { kind: 'expired' }. Override the rule with isReopenable, or cap it with staleAfterMs.

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.

Pass resolution to lock an envelope in its read-only view, which is what rehydrating conversation history needs.

PropertyTypeDefaultNotes
theme'auto'Color mode (`auto` follows prefers-color-scheme).
cards—The stream of card envelopes to render. Set as a JS PROPERTY: `el.cards = [...]`.
types—Card type→element tag overrides/additions, merged over the built-ins. JS property: `el.types`.
schemas—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`.
validateCardstrueValidate each card's `data` against its schema before rendering. Default `true`; opt out with `validate-cards="false"`.
MethodSignatureNotes
(cardId: string, resolution: CardResolution): voidProgrammatically 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.
(cardId: string): voidCollapse a card to its re-openable stub from the host side. Convenience for `resolve(cardId, { kind: 'dismissed' })`.
(cardId: string): HTMLElement | nullReturn 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.

This element wraps these SolidJS components — reach for them directly when you need finer control than the props expose.

CardFallback