# 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 = '

Hello, ' + esc(user.display_name || user.username) + '!

'; 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: ```
...
← platform nav
← YOUR CONTENT GOES HERE
... platform scripts ... ← YOUR SCRIPT ``` 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 ``` ### Badges ```html Default Success Error Warning ``` ### 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