# Cards

A dispatcher that renders a stream of generative-UI card envelopes — confirm, choice, tasks, link previews, and more — routing user actions through an optional policy.

<p class="kai-tag-sub">kai-cards</p>

Drop in `<kai-cards>` to render a live stream of AI-generated card envelopes — confirmations, task lists, choice pickers, link previews, and custom types — while a single `policy` object routes every user action back to your app.

## Preview

> **tip:** 
Reach for `<kai-cards>` whenever your AI response includes structured interactions — a deployment confirmation, a multi-step plan, a list of options. Pass server envelopes into `el.cards`, wire `el.policy` to handle results, and the element takes care of rendering, routing, and resolved (read-only) states.

## Usage

Set all three in JavaScript — they're arrays and objects, so none work as HTML attributes:

```html
<kai-cards id="cards"></kai-cards>

<script type="module">

  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 your app needs; the rest are silently ignored. Swap it after mount — 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 in its read-only state after the user has acted. The kinds: terminal `{ kind: 'action', action, payload? }` and `{ kind: 'submit', data }`, the deferred `{ kind: 'dismissed' }`, and the terminal `{ kind: 'expired', reason? }`.

## 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; keep the option open until your app decides the card is truly done.

```js
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. Your handler decides what happens — bring the card back live, or mark it `expired` if the moment has passed (the agent already proceeded, or too much time elapsed). "Already proceeded" is a fact only your app knows, so the decision is yours.

### `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):

```js

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. Undo restores whatever resolution the card had before.
- **`onReopen`** clears the resolution (card live again) 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** — cards stay decoupled from the toast module. Pass any adapter with a `show()` method, or omit `toast` to skip the Undo affordance entirely.

## Examples

### Mixed Card Stream

### Single Confirm Card

### Resolved Card

Pass `resolution` on an envelope to lock it in its read-only view — useful when rehydrating conversation history.

### Unknown Type Fallback

## Props

## Methods

## Composed from
