# AI Kit Viewers

Self-contained HTML viewers for AI Kit skills. Each viewer is built from React 19 source, bundled with Vite, flattened into a single HTML file with `vite-plugin-singlefile`, and then embedded into scaffold skill definitions.

This folder is a standalone package. It is not part of the parent pnpm workspace. Install and build it from this directory with `--ignore-workspace`.

## Overview

Use this package when a skill needs a rich interactive artifact that can ship as one HTML asset.

Current viewers:

- `c4-viewer.html` -> C4 architecture viewer powered by ReactFlow and ELK layout
- `tour-viewer.html` -> interactive code tour viewer powered by ReactFlow and a BFS tree layout

Why single-file HTML:

- AI Kit skill definitions embed assets as template literals in `.mjs` files
- A self-contained HTML file avoids external JS, CSS, font, and image dependencies
- Embedding one file is simpler and more reliable than coordinating asset directories
- Browser rendering is predictable inside skill-driven workflows

## Quick Start

Run these commands from `packages/viewers`.

```bash
pnpm install --ignore-workspace
pnpm build
pnpm embed
```

Available scripts:

```bash
pnpm dev:c4
pnpm dev:tour
pnpm build
pnpm embed
```

What each command does:

- `pnpm install --ignore-workspace` installs this package independently from the monorepo root
- `pnpm dev:c4` serves the C4 entry HTML with `vite.c4.config.ts`
- `pnpm dev:tour` serves the Tour entry HTML with `vite.tour.config.ts`
- `pnpm build` writes single-file artifacts into `dist/`
- `pnpm embed` injects built HTML into scaffold skill definitions

After `pnpm embed`, rebuild the parent scaffold output from the `aikit-mcp` root if you need refreshed generated artifacts. `embed.mjs` prints that reminder explicitly.

## Package Layout

```text
viewers/
├── README.md
├── package.json
├── tsconfig.json
├── pnpm-lock.yaml
├── .gitignore
├── c4-viewer.html
├── tour-viewer.html
├── vite.c4.config.ts
├── vite.tour.config.ts
├── embed.mjs
├── src/
│   ├── env.d.ts
│   ├── shared/
│   │   ├── theme.css
│   │   ├── types.ts
│   │   ├── ThemeProvider.tsx
│   │   ├── Layout.tsx
│   │   ├── Header.tsx
│   │   ├── Footer.tsx
│   │   ├── Sidebar.tsx
│   │   ├── Card.tsx
│   │   ├── Badge.tsx
│   │   ├── simple-template.css
│   │   └── simple-template.html
│   ├── c4/
│   │   ├── C4Viewer.tsx
│   │   └── main.tsx
│   └── tour/
│       ├── TourViewer.tsx
│       └── main.tsx
└── dist/
    ├── c4-viewer.html
    └── tour-viewer.html
```

Important distinction:

- Root `*-viewer.html` files are Vite entry templates
- `dist/*.html` files are the final self-contained artifacts
- `embed.mjs` reads from `dist/`, not from the root entry templates
- `package.json` `exports` points at the built viewer HTML assets, which keeps workspace health satisfied without changing the standalone build flow

## Architecture

## Build Stack

- React 19 for the viewer UI
- `@xyflow/react` v12 for graph rendering and interaction
- `elkjs` for auto-layout in the C4 viewer
- Vite 8 for dev/build
- `vite-plugin-singlefile` to inline JS and CSS into one HTML document

## Runtime Shape

Each full viewer follows the same structure:

1. Root HTML entry provides a `<div id="root"></div>` and optional JSON payload script tag
2. `src/<viewer>/main.tsx` reads `window.__VIEWER_CONFIG__` first, then falls back to legacy inline JSON
3. The viewer component wraps its content in `ThemeProvider`
4. `Layout`, `Header`, and `Footer` provide the shared shell
5. ReactFlow renders the interactive canvas
6. Vite emits a single HTML file into `dist/`
7. `embed.mjs` injects that HTML into the relevant skill definition file

## Shared Template System

`src/shared/` is the design system and shell contract for full viewers.

Treat it as the source of truth for:

- theme tokens
- layout variants
- header and footer behavior
- shared TypeScript interfaces
- reusable UI primitives

Two template tiers live here:

- Full template: React + ReactFlow viewers such as C4 and Tour
- Simple template: `simple-template.html` + `simple-template.css` for pure HTML/CSS rendering patterns

Use the full template for interactive graph viewers. Use the simple template only when a viewer does not need React or graph interaction.

## Shared Components

Every new full viewer should use the shared shell components unless there is a strong reason not to.

## `ThemeProvider`

Purpose:

- owns the light/dark/system theme mode state
- resolves system preference with `matchMedia('(prefers-color-scheme: dark)')`
- persists mode to `localStorage` under `aikit-theme-mode`
- writes the active theme to `document.documentElement.dataset.theme`

API:

```tsx
<ThemeProvider defaultMode="system">...</ThemeProvider>
```

Context values:

```ts
{
  mode: 'light' | 'dark' | 'system';
  resolvedMode: 'light' | 'dark';
  setMode: (mode: ThemeMode) => void;
  toggleMode: () => void;
}
```

Rules:

- Full viewers must be wrapped in `ThemeProvider`
- `defaultMode` should stay `system` unless you have a documented override
- Theme toggle behavior should remain centralized here, not reimplemented in individual viewers

## `Layout`

Purpose:

- applies the top-level CSS Grid layout class

Props:

```ts
interface LayoutProps {
  children: React.ReactNode;
  variant?: 'full' | 'sidebar-left' | 'sidebar-right';
  className?: string;
  style?: React.CSSProperties;
}
```

Usage:

```tsx
<Layout variant="full">
  <Header />
  <main />
  <Footer />
</Layout>
```

## `Header`

Purpose:

- renders the sticky top bar
- shows the viewer title and optional subtitle
- exposes the shared theme toggle button

Props:

```ts
interface HeaderConfig {
  title: string;
  subtitle?: string;
  showThemeToggle?: boolean;
}
```

Defaults:

- `title` defaults to `AI KIT`
- `showThemeToggle` defaults to `true`

Usage:

```tsx
<Header title="My Viewer" subtitle="Context for this artifact" />
```

Rule:

- Pass a viewer-specific title instead of changing the shared default

## `Footer`

Purpose:

- renders attribution
- optionally shows a timestamp
- optionally shows a legend

Props:

```ts
interface FooterConfig {
  showTimestamp?: boolean;
  showAttribution?: boolean;
  legend?: Array<{ label: string; color: string; icon?: string }>;
}
```

Usage:

```tsx
<Footer
  legend={[
    { label: 'system', color: 'var(--c4-system)' },
    { label: 'container', color: 'var(--c4-container)' },
  ]}
/>
```

Rule:

- Leave attribution enabled unless the host explicitly requires a different footer contract

## `Sidebar`

Purpose:

- provides a collapsible inspector panel
- supports left or right placement
- becomes an overlay on mobile

Props:

```ts
interface SidebarProps {
  children: React.ReactNode;
  position?: 'left' | 'right';
  collapsible?: boolean;
  defaultCollapsed?: boolean;
  width?: number;
  className?: string;
  title?: string;
}
```

Use this when a viewer needs metadata, filters, or a drill-down panel. Do not build a custom sidebar first.

## `Card`

Purpose:

- reusable surface for detail panes, metrics, or inspector content
- supports a gradient orb effect and clickable states

Props:

```ts
{
  children: React.ReactNode;
  className?: string;
  gradient?: boolean;
  onClick?: () => void;
  variant?: 'default' | 'outlined' | 'elevated';
}
```

Rule:

- Use `gradient` for featured blocks only. Do not turn every card into a visual accent.

## `Badge`

Purpose:

- compact status and category labels

Props:

```ts
{
  children: React.ReactNode;
  className?: string;
  variant?: 'default' | 'primary' | 'success' | 'warning' | 'error' | 'info';
  size?: 'sm' | 'md';
}
```

Rule:

- Prefer semantic variants over custom inline styling

## `types.ts`

Purpose:

- shared viewer contract for theme, layout, header, footer, C4 data, tour data, and the top-level `ViewerConfig`

Use existing types first. Extend them only when a new viewer needs a real shared contract.

## Theme System

Theme state is split across React state and CSS custom properties.

Mechanics:

- `ThemeProvider` tracks `mode: 'light' | 'dark' | 'system'`
- `resolvedMode` is always `'light'` or `'dark'`
- the provider writes `data-theme="light"` or `data-theme="dark"` on `<html>`
- `theme.css` defines the token sets for `:root` and `[data-theme="dark"]`
- if no explicit mode is active, `@media (prefers-color-scheme: dark)` provides the system fallback

### Key Theme Tokens

Core surface tokens:

- `--background`
- `--foreground`
- `--card`
- `--card-foreground`
- `--primary`
- `--primary-foreground`
- `--muted`
- `--muted-foreground`
- `--accent`
- `--accent-foreground`
- `--border`

Supplementary tokens used by shared components:

- `--sidebar`
- `--sidebar-foreground`
- `--color-success`
- `--color-warning`
- `--color-error`
- `--color-info`
- spacing tokens such as `--space-1` through `--space-24`
- shape and motion tokens such as `--radius`, `--shadow-*`, and `--transition-*`

C4-specific tokens:

- `--c4-person`
- `--c4-system`
- `--c4-container`
- `--c4-component`
- `--c4-database`
- `--c4-queue`
- `--c4-external`
- `--c4-boundary-border`

Conventions:

- Do not hardcode colors in new viewer UI code
- Read from CSS variables with `var(--token)`
- If you need a new semantic token, add it to `theme.css` instead of scattering literals
- Treat any remaining literal-color spots in older code as legacy, not as a pattern to copy

Note on palette language:

- The system is designed around shared CSS custom-property tokens and a Nord-inspired dark palette
- The current implementation stores concrete color values directly in `theme.css`
- If you migrate tokens to `oklch()`, keep the same semantic token names so viewer code stays unchanged

## Data Format

New viewers should prefer `window.__VIEWER_CONFIG__`.

Top-level shape:

```ts
interface ViewerConfig {
  type: 'c4' | 'tour' | 'custom';
  theme?: Partial<ThemeConfig>;
  layout?: Partial<LayoutConfig>;
  header?: Partial<HeaderConfig>;
  footer?: Partial<FooterConfig>;
  data: unknown;
}
```

## Preferred Injection Pattern

Use this in `src/<name>/main.tsx`:

```tsx
const config = (window as any).__VIEWER_CONFIG__;
const dataEl = document.getElementById('<viewer>-data');
const data = config?.data || (dataEl ? JSON.parse(dataEl.textContent || '{}') : {});
```

Why this order matters:

- `window.__VIEWER_CONFIG__` is the current contract for AI Kit-driven rendering
- inline script tags keep local preview and legacy flows working
- the fallback keeps the entry HTML usable as a manual demo and as a development fixture

## Current Viewers

### C4 Viewer

Source files:

- `src/c4/C4Viewer.tsx`
- `src/c4/main.tsx`
- `c4-viewer.html`
- `vite.c4.config.ts`

Behavior:

- reads `window.__VIEWER_CONFIG__` when `type === 'c4'`
- falls back to legacy `window.__C4_DATA__` and then `#diagram-data`
- normalizes legacy `elements` -> `nodes` and `relationships` -> `edges`
- uses ELK layered layout for automatic node positioning
- builds a legend for the footer from the node types in the payload

ReactFlow conventions already used here:

- `animated: true` on edges
- `proOptions: { hideAttribution: true }`
- includes `<Background />`, `<Controls />`, and `<MiniMap />`
- wraps the canvas in `ReactFlowProvider`

Data shape:

```ts
interface C4ViewerConfig {
  title?: string;
  description?: string;
  layout?: {
    direction?: string;
    spacing?: number;
    layerSpacing?: number;
  };
  nodes: Array<{
    id: string;
    type: 'person' | 'system' | 'container' | 'component' | 'database' | 'queue' | 'external' | 'boundary';
    label: string;
    technology?: string;
    description?: string;
  }>;
  edges: Array<{
    source: string;
    target: string;
    label?: string;
    style?: 'sync' | 'async' | 'dashed' | 'dotted' | 'solid';
  }>;
}
```

Example payload:

```html
<script type="application/json" id="diagram-data">
{
  "title": "Sample System",
  "description": "Replace this JSON with your C4 diagram data",
  "layout": { "direction": "DOWN", "spacing": 80, "layerSpacing": 120 },
  "nodes": [
    { "id": "user", "type": "person", "label": "User", "description": "End user" },
    { "id": "web", "type": "system", "label": "Web App", "technology": "React", "description": "Frontend" }
  ],
  "edges": [
    { "source": "user", "target": "web", "label": "Uses" }
  ]
}
</script>
```

### Tour Viewer

Source files:

- `src/tour/TourViewer.tsx`
- `src/tour/main.tsx`
- `tour-viewer.html`
- `vite.tour.config.ts`

Behavior:

- reads `window.__VIEWER_CONFIG__` first
- falls back to `#tour-data`
- computes a BFS layout from `steps` and `dependencies`
- renders a graph above and a detail panel below
- supports active-step selection and previous/next navigation

ReactFlow conventions already used here:

- `animated: true` on edges
- `proOptions: { hideAttribution: true }`
- includes `<Background />`, `<Controls />`, and `<MiniMap />`
- wraps the canvas in `ReactFlowProvider`

Current data shape:

```ts
{
  title?: string;
  description?: string;
  estimatedTime?: string;
  steps: Array<{
    stepNumber: number;
    id: string;
    title: string;
    file: string;
    explanation: string;
    learnsConcept: string;
    duration: string;
  }>;
  dependencies: Array<{
    source: string;
    target: string;
  }>;
}
```

## How to Create a New Viewer

Use this process for every new full viewer.

### 1. Create the source directory

```text
src/<name>/
  <Name>Viewer.tsx
  main.tsx
```

### 2. Create `<Name>Viewer.tsx`

Use the shared shell.

```tsx
import { ReactFlowProvider, ReactFlow, Background, Controls, MiniMap } from '@xyflow/react';
import '@xyflow/react/dist/style.css';

import { ThemeProvider } from '../shared/ThemeProvider';
import { Layout } from '../shared/Layout';
import { Header } from '../shared/Header';
import { Footer } from '../shared/Footer';
import '../shared/theme.css';

interface MyViewerData {
  title?: string;
  description?: string;
  nodes: Array<{ id: string; label: string }>;
  edges: Array<{ source: string; target: string }>;
}

function MyViewerInner({ data }: { data: MyViewerData }) {
  return (
    <Layout variant="full">
      <Header title={data.title || 'My Viewer'} subtitle={data.description} showThemeToggle={true} />
      <div style={{ minHeight: 0, height: '100%', overflow: 'hidden' }}>
        <ReactFlow
          nodes={[]}
          edges={[]}
          proOptions={{ hideAttribution: true }}
        >
          <Background color="var(--muted)" gap={24} size={1.5} />
          <Controls />
          <MiniMap />
        </ReactFlow>
      </div>
      <Footer />
    </Layout>
  );
}

export function MyViewer({ data }: { data: MyViewerData }) {
  return (
    <ThemeProvider>
      <ReactFlowProvider>
        <MyViewerInner data={data} />
      </ReactFlowProvider>
    </ThemeProvider>
  );
}
```

### 3. Create `main.tsx`

Use the standard data loader.

```tsx
import { createRoot } from 'react-dom/client';
import { MyViewer } from './MyViewer';

const config = (window as any).__VIEWER_CONFIG__;
const dataEl = document.getElementById('my-viewer-data');
const data = config?.data || (dataEl ? JSON.parse(dataEl.textContent || '{}') : {});

createRoot(document.getElementById('root')!).render(<MyViewer data={data} />);
```

### 4. Create the root entry HTML template

Name it `<name>-viewer.html` and keep it minimal.

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>AI KIT - My Viewer</title>
</head>
<body>
  <script type="application/json" id="my-viewer-data">
  {
    "title": "Sample Viewer",
    "description": "Replace this JSON with sample viewer data"
  }
  </script>
  <div id="root"></div>
  <script type="module" src="./src/my-viewer/main.tsx"></script>
</body>
</html>
```

### 5. Create `vite.<name>.config.ts`

Copy an existing Vite config and change the input file.

```ts
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import { viteSingleFile } from 'vite-plugin-singlefile';

export default defineConfig({
  plugins: [react(), viteSingleFile()],
  root: '.',
  build: {
    outDir: 'dist',
    emptyOutDir: false,
    rollupOptions: {
      input: 'my-viewer.html',
    },
  },
});
```

### 6. Update `package.json`

Add a dev script and include the new Vite config in the build flow.

Example:

```json
{
  "scripts": {
    "dev:my-viewer": "vite --config vite.my-viewer.config.ts",
    "build": "vite build --config vite.c4.config.ts && vite build --config vite.tour.config.ts && vite build --config vite.my-viewer.config.ts"
  }
}
```

### 7. Build locally

```bash
pnpm build
```

Confirm that `dist/my-viewer.html` exists and opens correctly.

### 8. Embed if the viewer belongs in a skill asset

Update `embed.mjs` if the new viewer should be injected into a skill file.

Current embed targets:

- `dist/c4-viewer.html` -> `../../definitions/skills/c4-architecture.mjs`
- `dist/tour-viewer.html` -> `../../definitions/skills/docs.mjs`

Pattern:

```js
await updateSkillAsset('my-skill.mjs', 'my-viewer.html', myViewerHtml);
```

Then run:

```bash
pnpm embed
```

## Build and Deploy Flow

The end-to-end path is:

```text
src/<viewer>/*
  -> root <name>-viewer.html entry template
  -> vite.<name>.config.ts
  -> dist/<name>-viewer.html
  -> embed.mjs
  -> scaffold/definitions/skills/*.mjs asset arrays
```

`embed.mjs` behavior matters:

- it reads the built HTML from `dist/`
- it escapes backslashes, backticks, and `${` for safe template-literal embedding
- it replaces an existing asset entry if the same file marker is already present
- otherwise it appends a new asset object before the closing `];`

That replacement strategy is designed to recover cleanly from previous partial embeds.

## Conventions for LLM Agents

Follow these rules when modifying or creating viewers.

1. Full viewers must use `ThemeProvider`, `Layout`, `Header`, and `Footer`.
2. Use shared components before creating new shell primitives.
3. Do not hardcode colors in new code. Use `var(--token)` from `theme.css`.
4. Theme state belongs on `<html data-theme="light|dark">` and is resolved by `ThemeProvider`.
5. Keep `ThemeMode` as `'light' | 'dark' | 'system'` unless a cross-viewer contract change is intentional.
6. `Header` defaults to `AI KIT`. Pass the viewer-specific title through props.
7. Root `*-viewer.html` files are entry templates, not build artifacts.
8. Each viewer gets its own `vite.<name>.config.ts` file.
9. ReactFlow viewers must include `<Background />`, `<Controls />`, and `<MiniMap />`.
10. ReactFlow edges should be animated unless the viewer has a documented reason not to animate them.
11. Set `proOptions: { hideAttribution: true }` on ReactFlow instances.
12. Keep the package standalone. Use `pnpm install --ignore-workspace` in this folder.
13. Prefer `window.__VIEWER_CONFIG__` and retain an inline JSON fallback for previewability.
14. Reuse shared types from `src/shared/types.ts` when possible.
15. If you add a shared token or component, document it here in the same change.
16. If you add a viewer intended for a skill, wire it into `embed.mjs` and verify the target skill file.

## Common Mistakes

- Editing `dist/*.html` directly instead of source files
- Treating root `*-viewer.html` as final output
- Running `pnpm install` without `--ignore-workspace`
- Recreating theme logic inside a viewer instead of using `ThemeProvider`
- Adding bespoke layout wrappers instead of using `Layout`
- Copying literal colors from older code instead of using theme tokens
- Forgetting to add a new Vite config to the build script
- Forgetting to update `embed.mjs` for a new skill-bound viewer

## Minimal Checklist for a New Viewer

- Source lives under `src/<name>/`
- Root entry HTML exists as `<name>-viewer.html`
- Vite config exists as `vite.<name>.config.ts`
- Viewer uses `ThemeProvider`, `Layout`, `Header`, and `Footer`
- ReactFlow config includes `Background`, `Controls`, `MiniMap`, animated edges, and hidden attribution
- Data loader prefers `window.__VIEWER_CONFIG__`
- `package.json` scripts are updated
- `pnpm build` succeeds
- `pnpm embed` updated and run if needed
- This README updated if the shared contract changed

## Source of Truth

When this README and the code disagree, trust the code in this folder. Then update the README in the same change.