# Extension Surfaces — Authoring Guide

**Version:** v0.27.0+
**Audience:** Developers building custom surfaces for Chat Switchboard
**Prerequisite:** Admin access to install surfaces

---

## What Is an Extension Surface?

An extension surface is a custom full-page UI that runs inside the
Chat Switchboard shell. It gets its own URL (`/s/your-surface`), a
link in the sidebar, and full access to the platform's authenticated
API, theme system, and UI components.

Examples of what you can build:

- A monitoring dashboard that polls external APIs
- A custom form builder for workflow intake
- A kanban board backed by channel data
- A cost tracking view using usage data
- A project timeline visualization

Extension surfaces are installed as `.surface` archives via the admin
panel. No code changes to the platform are required.

---

## Quick Start

### 1. Create the Archive

A `.surface` file is a zip archive with this structure:

```
my-dashboard/
├── manifest.json       ← required: surface metadata
├── js/
│   └── main.js         ← required: entry point script
└── css/
    └── main.css        ← optional: styles
```

The top-level directory name must match the `id` in `manifest.json`.

### 2. Write the Manifest

```json
{
  "id": "my-dashboard",
  "title": "My Dashboard",
  "route": "/s/my-dashboard",
  "auth": "authenticated",
  "layout": "single",
  "version": "1.0.0",
  "description": "A custom dashboard surface."
}
```

| Field         | Required | Description |
|---------------|----------|-------------|
| `id`          | yes      | Unique identifier. Lowercase, alphanumeric + hyphens. Must match the archive's top-level directory name. |
| `title`       | yes      | Human-readable name shown in the sidebar nav. |
| `route`       | no       | URL path. Defaults to `/s/{id}` if omitted. Must start with `/s/`. |
| `auth`        | no       | Auth requirement. Only `"authenticated"` is supported currently. Default: `"authenticated"`. |
| `layout`      | no       | Layout preset. Only `"single"` is supported currently. Default: `"single"`. |
| `version`     | no       | Semver string for your own tracking. Not enforced. |
| `description` | no       | Shown in the admin surfaces panel. |

### 3. Write the Entry Point

`js/main.js` runs after all platform scripts have loaded. Your code
mounts into the `#extension-mount` container:

```js
(function() {
    'use strict';

    var mount = document.getElementById('extension-mount');
    if (!mount) return;

    var manifest = window.__MANIFEST__ || {};
    var user = window.__USER__ || {};

    mount.innerHTML = '<h1>Hello, ' + esc(user.display_name || user.username) + '!</h1>';

    function esc(s) {
        var el = document.createElement('span');
        el.textContent = s;
        return el.innerHTML;
    }
})();
```

### 4. Package and Install

```sh
zip -r my-dashboard.surface my-dashboard/
```

Then in the admin panel: **Admin → Surfaces → Upload** and select the
`.surface` file. The surface is immediately available — no restart
required. A link appears in the sidebar.

---

## Platform Globals

Your `main.js` runs in the full platform context. These globals are
available when your script executes:

### Window Injections

| Global | Type | Description |
|--------|------|-------------|
| `window.__MANIFEST__` | Object | Your surface's manifest with route, layout, etc. Keys are **lowercase** (JSON serialization from Go). Access as `manifest.id`, `manifest.route`, `manifest.title`. |
| `window.__USER__` | Object | Authenticated user: `{ username, display_name, role, id, email }`. |
| `window.__BASE__` | String | URL base path (e.g. `"/dev"` or `""`). Prepend to all internal links. |
| `window.__VERSION__` | String | Platform version string. |
| `window.__SURFACE__` | String | Your surface ID. |

### API Module

`API` provides authenticated fetch wrappers. Auth headers and token
refresh are handled automatically.

```js
// GET request — returns parsed JSON
var data = await API._get('/api/v1/channels');

// POST request — body is JSON-serialized automatically
var result = await API._post('/api/v1/channels', {
    name: 'my-channel',
    type: 'channel'
});

// PUT request
await API._put('/api/v1/channels/' + id, { name: 'renamed' });
```

All methods return a Promise that resolves to parsed JSON. On 401,
tokens are refreshed automatically and the request is retried.

**Important:** The methods are `_get`, `_post`, `_put` — prefixed with
underscore. There is no `API.get()`.

### UI Module

`UI` provides platform UI primitives:

```js
// Toast notification — types: 'success', 'error', 'info', 'warning'
UI.toast('Operation completed', 'success');
UI.toast('Something went wrong', 'error');

// Navigate to settings
UI.openSettings('general');
```

### Theme Module

`Theme` manages dark/light mode:

```js
// Current resolved theme: 'dark' or 'light'
var mode = Theme.resolved();

// Check via DOM attribute
var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
```

Your CSS should use CSS custom properties (see below) rather than
querying Theme directly. Properties update automatically on theme change.

### Events Module

`Events` is the platform event bus (WebSocket-backed):

```js
// Subscribe to events
Events.on('chat.message.created', function(data) {
    console.log('New message:', data);
});

// Emit local-only events
Events.emit('my-surface.updated', { key: 'value' });
```

### ChatPane Component

If your surface needs an embedded chat, you can mount a ChatPane:

```js
var container = document.createElement('div');
container.style.height = '400px';
mount.appendChild(container);

ChatPane.create(container, { role: 'assist' });
```

---

## CSS Custom Properties

Your CSS should use the platform's CSS custom properties for consistent
theming. These update automatically when the user switches themes.

### Colors

| Property | Dark Default | Light Default | Usage |
|----------|-------------|---------------|-------|
| `--bg` | `#0e0e10` | `#f7f7fa` | Page background |
| `--bg-surface` | `#18181b` | `#ffffff` | Card/panel background |
| `--bg-raised` | `#222227` | `#eff0f3` | Elevated element background |
| `--bg-hover` | `#2a2a30` | `#e5e6eb` | Hover state background |
| `--border` | `#2e2e35` | `#d5d6dc` | Default borders |
| `--border-light` | `#3a3a42` | `#c2c3cb` | Subtle borders |
| `--text` | `#e8e8ed` | `#1a1a2e` | Primary text |
| `--text-2` | `#9898a8` | — | Secondary text |
| `--text-3` | `#6b6b7b` | — | Tertiary/muted text |
| `--accent` | `#6c9fff` | — | Accent color (links, highlights) |
| `--accent-hover` | `#84b0ff` | — | Accent hover state |
| `--accent-dim` | `rgba(108,159,255,0.12)` | — | Accent tinted background |
| `--danger` | `#ef4444` | — | Error/destructive actions |
| `--success` | `#22c55e` | — | Success indicators |
| `--warning` | `#eab308` | — | Warning indicators |

### Layout

| Property | Default | Usage |
|----------|---------|-------|
| `--radius` | `8px` | Default border radius |
| `--radius-lg` | `12px` | Large border radius |
| `--font` | `'DM Sans', ...` | Primary font stack |
| `--mono` | `'JetBrains Mono', ...` | Monospace font stack |
| `--transition` | `180ms ease` | Default transition timing |

### Example

```css
.my-card {
    background: var(--bg-surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 16px;
    color: var(--text);
}

.my-card-subtitle {
    color: var(--text-2);
    font-size: 13px;
}

.my-card:hover {
    background: var(--bg-hover);
}
```

**Do not use** `--bg-secondary`, `--text-secondary`, or
`--text-tertiary` — these do not exist. Use `--bg-surface`, `--text-2`,
and `--text-3` respectively.

---

## DOM Structure

When your surface loads, the page DOM looks like this:

```
<body data-surface="my-dashboard">
  <div class="surface">
    <div class="surface-inner">
      <div id="extension-surface" class="extension-surface">
        <div class="user-menu-wrap"> ... </div>   ← platform nav
        <div id="extension-mount" class="extension-mount">
          ← YOUR CONTENT GOES HERE
        </div>
      </div>
    </div>
  </div>
  <script>window.__MANIFEST__ = {...};</script>
  <script>window.__USER__ = {...};</script>
  <script src="/js/api.js"></script>
  <script src="/js/ui-core.js"></script>
  ... platform scripts ...
  <script src="/surfaces/my-dashboard/js/main.js"></script>  ← YOUR SCRIPT
</body>
```

The `#extension-mount` div uses `flex: 1` and `overflow: auto`, so it
fills all available vertical space. You can set its content to anything.

---

## Admin API Reference

Surfaces are managed via the admin API. All endpoints require admin
authentication.

### Install a Surface

```
POST /api/v1/admin/surfaces/install
Content-Type: multipart/form-data

file: <.surface or .zip file>
```

The archive must contain a `manifest.json` with `id` and `title`.
Static assets (`js/`, `css/`, `assets/`) are extracted to the server's
storage directory. The surface is registered in the database and
enabled immediately.

**Response:**
```json
{
  "id": "my-dashboard",
  "title": "My Dashboard",
  "source": "extension",
  "enabled": true
}
```

### List All Surfaces

```
GET /api/v1/admin/surfaces
```

Returns all surfaces (core + extension) with their enabled state.

### Enable / Disable

```
PUT /api/v1/admin/surfaces/:id/enable
PUT /api/v1/admin/surfaces/:id/disable
```

Disabled surfaces redirect to the chat surface. The sidebar link is
hidden. Core surfaces (`chat`, `admin`) cannot be disabled.

### Uninstall

```
DELETE /api/v1/admin/surfaces/:id
```

Removes the surface registration and cleans up extracted static assets.
Core surfaces cannot be deleted.

### List Enabled (Non-Admin)

```
GET /api/v1/surfaces
```

Returns enabled surfaces with minimal info (id, title, route). Used by
the sidebar to render nav links. Available to all authenticated users.

---

## Platform CSS Classes

Your extension can use these CSS classes from the platform's
`primitives.css` without importing anything:

### Buttons

```html
<button class="btn-primary">Primary Action</button>
<button class="btn-secondary">Secondary</button>
<button class="btn-danger">Destructive</button>
<button class="btn-ghost">Ghost</button>
<button class="btn-small">Small</button>
```

### Badges

```html
<span class="badge">Default</span>
<span class="badge badge-success">Success</span>
<span class="badge badge-danger">Error</span>
<span class="badge badge-warning">Warning</span>
```

### Toast (via JS)

```js
UI.toast('Message here', 'success');   // green
UI.toast('Message here', 'error');     // red
UI.toast('Message here', 'info');      // blue
UI.toast('Message here', 'warning');   // yellow
```

---

## Tips

**Always wrap in an IIFE.** Your script shares the global scope with
platform scripts. Use `(function() { ... })();` to avoid collisions.

**Use `esc()` for user-generated content.** The platform does not
provide a global escaping function. Define your own:

```js
function esc(s) {
    var el = document.createElement('span');
    el.textContent = s;
    return el.innerHTML;
}
```

**Prepend `__BASE__` to internal links.** The platform may be deployed
under a path prefix (e.g. `/dev`):

```js
var base = window.__BASE__ || '';
link.href = base + '/settings/general';
```

**Check for globals before using them.** If your surface might be
previewed outside the platform:

```js
if (typeof API !== 'undefined' && API._get) {
    // safe to make API calls
}
if (typeof UI !== 'undefined' && UI.toast) {
    UI.toast('Works!', 'success');
}
```

**Size limit:** Archive upload is capped at 50 MB. Only `js/`, `css/`,
and `assets/` directories are extracted from the archive.

**Cache busting:** Static assets are served with `?v={platform_version}`
query parameter. When the platform is upgraded, caches are invalidated
automatically.

---

## Full Example

See the included `hello-dashboard.surface` archive for a complete
working example that demonstrates:

- Manifest structure
- Mounting into `#extension-mount`
- Reading `__MANIFEST__` and `__USER__`
- Using `UI.toast()` for notifications
- Using `API._get()` for authenticated requests
- Using CSS custom properties for theming
- Proper `esc()` function for XSS safety
