# UI kit

> zidane-ui: the desktop app's design system and chat components as one React package.

`zidane-ui` is the design system behind the desktop app and this site, plus the app's chat components: session list, transcript and composer. The chat components read a `ChatHost`, an object of readable stores and optional actions. A fixture, an in-page agent, your server or zidane-server can be the host.

| Layer | Import | Contents |
| - | - | - |
| Design system | `zidane-ui` | Tokens, surfaces, palette engine, buttons, menus, popovers, dialogs, form controls, motion helpers |
| Chat | `zidane-ui/chat` | `ChatProvider`, `SessionList`, `Transcript` / `HostTranscript`, `Composer` / `HostComposer`, `HostInteractions` |
| Hosts | `zidane-ui/chat/headless`, `zidane-ui/chat/testing`, `zidane-ui/adapters` | React-free model (`ChatHost`, stores, transcript pipeline, `createFixtureHost`) and hosts backed by a real agent |

The design system works without the chat layer. The chat layer works without `zidane-ui/adapters`. Every import is listed in [Entry points](#entry-points).

## Install

```bash
npm install zidane-ui react react-dom motion
npm install zidane   # for every area except appearance, plus the adapters and the server helpers
```

| Peer | Version | Needed for |
| - | - | - |
| `react`, `react-dom` | ^19.3 | Everything |
| `motion` | ^13.5 | The design system and the chat layer. `Overlay`, `FloatingSurface`, `Lightbox` and the transcript's `Grow` animate with it |
| `zidane` | ^8 | Optional: the adapters, the server helpers and every area except appearance: chat, approvals, app, files, git, keybindings, machines, onboarding (the setup wizard), palette, session, settings, terminal, workspace. They import `zidane/chat/pure`, a browser-safe slice with no agent runtime |
| `unocss` | >=66 | Optional, for `zidane-ui/uno` only |

Without `zidane` you can use the design system (`zidane-ui`, `zidane-ui/code/editor`, the highlight and patch-parse workers, `zidane-ui/labels`, `zidane-ui/uno`), the appearance area, and the headless and testing entries of files and terminal. Markdown (`react-markdown`, `remark-gfm`) and the composer editor (Tiptap) are regular dependencies.

## Styling

Pick one of the two routes below. Do not use both.

### Prebuilt CSS

Use this route when your app does not run UnoCSS. Import one stylesheet once: `zidane-ui/core.css` for the design system alone, `zidane-ui/styles.css` for everything (chat, setup wizard, editor, terminal). It holds every utility class and icon that those components use, then the kit styles. If your page has no reset, import the reset first. Without one, native button and list styles show through.

```ts
import 'zidane-ui/reset.css'
import 'zidane-ui/core.css' // or 'zidane-ui/styles.css'
```

The prebuilt files sit in the cascade layers `zidane-ui.reset`, `zidane-ui.utilities` and `zidane-ui.kit`, so your own unlayered CSS overrides the kit without a specificity fight. They only carry the utilities the kit uses: to put token classes in your own markup, take the UnoCSS route or use the `--zg-*` variables.

### UnoCSS

Add the preset, scan the kit's files and import the kit stylesheet after the Uno utilities. Your markup can then use token classes (`bg-zg-raised`, `text-zg-muted`). `crispPhosphor` draws regular Phosphor icons with their bold outline, so they stay crisp at the kit's 10 to 14px.

```ts
// uno.config.ts
import { crispPhosphor, presetZidane } from 'zidane-ui/uno'
import { defineConfig, presetIcons, presetUno } from 'unocss'

export default defineConfig({
  content: { filesystem: ['src/**/*.{ts,tsx}', 'node_modules/zidane-ui/dist/**/*.js'] },
  presets: [presetIcons({ collections: { ph: () => import('@iconify-json/ph/icons.json').then(i => crispPhosphor(i.default)) } }), presetUno({ dark: { dark: '.dark' } }), presetZidane()],
})
```

```css
/* your stylesheet, after the Uno utilities */
@import 'zidane-ui/styles/index.css';
/* or, to put the kit in a layer of its own: */
@import 'zidane-ui/styles/index.css' layer(zidane-ui);
```

Components paint from `--zg-*` CSS variables with dark defaults, so they render before you set a [theme](https://zidane.si/docs/ui-theming.md).

### Extension icons

Extensions name their icons as data (`icon: { gui: 'i-ph-rocket' }`), so no stylesheet built ahead of time has them all. Call `watchRuntimeIcons` once at startup, on either route. It adds the rule of each `i-ph-*` class the page shows without one, from the same Phosphor set. The set loads the first time an icon is missing, in a chunk of its own.

```ts
import { watchRuntimeIcons } from 'zidane-ui'
import { crispPhosphor } from 'zidane-ui/uno'

watchRuntimeIcons({ loadPhosphor: () => import('@iconify-json/ph/icons.json').then(m => crispPhosphor(m.default)) })
```

## Build your own agent UI

The example below puts a sidebar, a transcript, question cards and a composer on one host. This fixture host echoes every prompt. You can swap in a real host without a change to the components.

```tsx
import 'zidane-ui/styles.css'
import { ChatProvider, HostComposer, HostInteractions, HostTranscript, SessionList } from 'zidane-ui/chat'
import { createFixtureHost } from 'zidane-ui/chat/testing'
import { useState } from 'react'

const host = createFixtureHost(
  { sessions: [{ id: 's1', title: 'First session', status: 'idle', createdAt: Date.now(), updatedAt: Date.now() }] },
  { reply: prompt => [{ kind: 'markdown', text: `You said: ${prompt}` }] },
)

export function App() {
  const [id, setId] = useState('s1')
  return (
    <ChatProvider host={host}>
      <div style={{ display: 'grid', gridTemplateColumns: '260px 1fr', height: '100vh' }}>
        <SessionList activeId={id} onSelect={setId} search />
        <main style={{ display: 'flex', flexDirection: 'column', gap: 12, padding: 16, minWidth: 0 }}>
          <div style={{ flex: 1, overflowY: 'auto' }}><HostTranscript sessionId={id} /></div>
          <HostInteractions sessionId={id} />
          <HostComposer sessionId={id} attachments />
        </main>
      </div>
    </ChatProvider>
  )
}
```

| Component | Role |
| - | - |
| `ChatProvider` | Puts the host (and label overrides) in context |
| [`SessionList`](https://zidane.si/docs/ui-sessions.md) | Lists `host.sessions`. The row menu shows only the actions that the host implements |
| [`HostTranscript`](https://zidane.si/docs/ui-transcript.md) | Renders `host.transcript(id)`. `Transcript` takes a plain event array instead. |
| `HostInteractions` | Questions and plans the agent is waiting on |
| [`HostComposer`](https://zidane.si/docs/ui-composer.md) | Sends through `host.send`, steers or queues during a run, shows model pickers when the host offers `models` |

The inline styles are there because the prebuilt stylesheet holds only the kit's own classes. With UnoCSS, use utility classes.

## Pick a host

A host runs the agent behind the components. Every host returns a `ChatHost`, so switching hosts leaves the components unchanged.

| Host | Import | Agent runs | Use for |
| - | - | - | - |
| `createFixtureHost` | `zidane-ui/chat/testing` | Nowhere (static data, scripted reply) | Demos, tests, docs, design |
| `createRunHost` | `zidane-ui/adapters` | Your `run` function, in the page | Code that already streams zidane events |
| `createWorkerHost` + `exposeRunHost` | `zidane-ui/adapters` | Same, in a Web Worker | Keeping the main thread free |
| `createSseHost` + `createSseHandler` | `zidane-ui/adapters`, `zidane-ui/server` | A zidane chat engine on your server, over HTTP and SSE | A product with its own users, auth and session store |
| `createServerHost` | `zidane-ui/adapters` | A running [zidane-server](https://zidane.si/docs/server.md), over WebSocket | Sessions shared with the TUI and desktop clients of that server |

[Connecting a host](https://zidane.si/docs/ui-adapters.md) sets up each one and lists their [limits](https://zidane.si/docs/ui-adapters.md#limits). [Server clients](https://zidane.si/docs/server-clients.md#browser) compares a browser with other zidane-server clients.

### What a host offers

To write your own host, implement `ChatHost`. Only `transcript(id)` is required. Each optional member that you leave out hides its control:

| Member | Without it |
| - | - |
| `transcript(sessionId)` | Required: the transcript store of a session |
| `sessions.list` | No session list |
| `sessions.create`, `rename`, `setArchived`, `setFavorite`, `remove` | No New, Rename, Archive, Favorite or Delete control |
| `send` | The composer cannot send, so it does not render |
| `interrupt` | No Stop button, and Escape does nothing |
| `steer`, `followUp` | Enter does nothing during a run. The draft stays until the run ends |
| `interactions.pending`, `answer` | No question or plan cards |
| `interactions.cancel` | No Dismiss on a card |
| `models` | No model picker |
| `toolOutput` | No "Load full output" on a clipped tool result |
| `cancelTool` | No Stop on a running `!` command |

## Entry points

| Import | Contents |
| - | - |
| [`zidane-ui`](https://zidane.si/docs/reference/ui.md) | Design system: components, motion helpers, hooks, code highlighting (`@pierre/highlights`, loaded on first use), theme engine, brand marks, labels |
| [`zidane-ui/code/editor`](https://zidane.si/docs/reference/ui-code-editor.md) | Code editor, file view, file tree, find and diagnostics (`@pierre/diffs`) |
| `zidane-ui/code/highlight.worker` | The highlighting worker module, for `?worker` or `new URL` |
| [`zidane-ui/terminal`](https://zidane.si/docs/reference/ui-terminal.md) | Terminal surface (`@tahul/ghostty-web`, loaded on first use), and the terminal area: `TerminalProvider`, `TerminalPanel`, `TerminalDrawer`, `TerminalToggle`, `TerminalPanelTab` ([guide](https://zidane.si/docs/ui-terminal.md)) |
| `zidane-ui/terminal/headless` | `TerminalHost`, `createTerminalStore`, scope keys, `DEFAULT_TERMINAL_LABELS` |
| `zidane-ui/terminal/testing` | `createFixtureTerminalHost` and demo terminals |
| [`zidane-ui/labels`](https://zidane.si/docs/reference/ui-labels.md) | `DEFAULT_LABELS`, `LabelsProvider`, `useLabels` |
| [`zidane-ui/uno`](https://zidane.si/docs/reference/ui-uno.md) | `presetZidane()`: tokens as a UnoCSS preset |
| [`zidane-ui/chat`](https://zidane.si/docs/reference/ui-chat.md) | Styled chat layer, including the three entries below |
| [`zidane-ui/chat/transcript`](https://zidane.si/docs/reference/ui-chat-transcript.md) | Transcript rows, renderer slots (`TranscriptRenderersProvider`), actions (`TranscriptActionsProvider`) |
| [`zidane-ui/chat/composer`](https://zidane.si/docs/reference/ui-chat-composer.md) | Composer, model and reasoning pickers, mention suggestions, attachment helpers |
| [`zidane-ui/chat/interactions`](https://zidane.si/docs/reference/ui-chat-interactions.md) | Question and plan cards |
| [`zidane-ui/chat/headless`](https://zidane.si/docs/reference/ui-chat-headless.md) | React-free model: `ChatHost`, stores, session-list model, transcript pipeline, labels, diff tree |
| [`zidane-ui/chat/testing`](https://zidane.si/docs/reference/ui-chat-testing.md) | `createFixtureHost`, `DEMO_TRANSCRIPT`, `DEMO_TRANSCRIPT_ALL_KINDS` |
| [`zidane-ui/approvals`](https://zidane.si/docs/reference/ui-approvals.md) | Tool approvals: `ApprovalsProvider`, `ApprovalBar`, `ApprovalCard`, `FileEditApproval`, `SafelistPanel` |
| [`zidane-ui/approvals/headless`](https://zidane.si/docs/reference/ui-approvals-headless.md) | `ApprovalsHost`, the pending queue, the safelist store, `DEFAULT_APPROVALS_LABELS` |
| [`zidane-ui/approvals/testing`](https://zidane.si/docs/reference/ui-approvals-testing.md) | `createFixtureApprovalsHost` and demo approvals |
| [`zidane-ui/onboarding`](https://zidane.si/docs/reference/ui-onboarding.md) | The setup wizard: `OnboardingProvider`, `OnboardingFlow`, the eleven steps, `useOnboarding`, `OnboardingStepFrame` |
| [`zidane-ui/onboarding/headless`](https://zidane.si/docs/reference/ui-onboarding-headless.md) | `OnboardingHost`, `createOnboardingController`, `DEFAULT_ONBOARDING_LABELS` |
| [`zidane-ui/onboarding/testing`](https://zidane.si/docs/reference/ui-onboarding-testing.md) | `createFixtureOnboardingHost` and demo data |
| [`zidane-ui/app`](https://zidane.si/docs/reference/ui-app.md) | `AppHostProvider` (provides each area's host, `ChatProvider` included, in one wrap), `useAppHost` |
| [`zidane-ui/app/headless`](https://zidane.si/docs/reference/ui-app-headless.md) | `AppHost`: one optional host per area (`chat`, `onboarding`) |
| [`zidane-ui/app/testing`](https://zidane.si/docs/reference/ui-app-testing.md) | `createFixtureAppHost`: an `AppHost` of the areas' fixture hosts |
| [`zidane-ui/adapters`](https://zidane.si/docs/reference/ui-adapters.md) | Browser-safe hosts: `createRunHost`, `createWorkerHost` / `exposeRunHost`, `createSseHost`, `createServerHost` |
| [`zidane-ui/server`](https://zidane.si/docs/reference/ui-server.md) | Node and Bun only (browser builds fail to resolve it): `createSseHandler`, `createLocalEnvironment` |
| [`zidane-ui/server/testing`](https://zidane.si/docs/reference/ui-server-testing.md) | Node and Bun only: `createScriptedProvider`, `demoTools` |
| `zidane-ui/core.css` | Prebuilt, the design system alone: its utilities and icons, then its styles |
| `zidane-ui/styles.css` | Prebuilt, everything: utilities, icons, kit stylesheet |
| `zidane-ui/styles/index.css` | Kit stylesheet alone, for UnoCSS apps (`styles/<part>.css` for one part) |
| `zidane-ui/reset.css` | Optional reset (Tailwind preflight from `@unocss/reset`) |

`dist/` has one module per source file. Only the CSS has side effects, so bundlers keep only what you import. No module has `'use client'`. If you use React Server Components, import the kit from your own client component.

## Labels

Every rendered string comes from one dictionary. Override chat strings with `ChatProvider`'s `labels` prop and design-system strings (Copy, Confirm, Cancel, Close) with `LabelsProvider`. Function labels take their values as arguments, so translations control word order. A prop passed to a component wins over the dictionary.

```tsx
import type { ChatHost } from 'zidane-ui/chat/headless'
import type { ReactNode } from 'react'
import { ChatProvider } from 'zidane-ui/chat'
import { LabelsProvider } from 'zidane-ui/labels'

export function French({ host, children }: { host: ChatHost, children: ReactNode }) {
  return (
    <LabelsProvider labels={{ confirm: 'Confirmer', cancel: 'Annuler', close: 'Fermer' }}>
      <ChatProvider
        host={host}
        labels={{
          newSession: 'Nouvelle session',
          searchSessions: 'Rechercher',
          noMatches: query => `Aucune session ne correspond à « ${query} »`,
          composerPlaceholder: 'Écrivez à l’agent…',
        }}
      >
        {children}
      </ChatProvider>
    </LabelsProvider>
  )
}
```

Full lists: `DEFAULT_LABELS` (`zidane-ui/labels`) and `DEFAULT_CHAT_LABELS` (`zidane-ui/chat/headless`).

## Next

- [Session list](https://zidane.si/docs/ui-sessions.md): Grouping, sorting, search, row additions.
- [Transcript](https://zidane.si/docs/ui-transcript.md): Options, renderer slots, actions.
- [Composer](https://zidane.si/docs/ui-composer.md): Pickers, attachments, mentions, interaction cards.
- [Components](https://zidane.si/docs/ui-components.md): The design system, component by component.
- [Theming](https://zidane.si/docs/ui-theming.md): Tokens, surfaces, palettes, motion.
- [Connecting a host](https://zidane.si/docs/ui-adapters.md): A real agent behind the components.
