This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/EXTENSION-SURFACES.md
gobha be67feaa8e Changeset 0.37.19 (#232)
Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
2026-03-25 00:26:44 +00:00

455 lines
12 KiB
Markdown

# Extension Surfaces — Authoring Guide
**Version:** v0.30.2
**Audience:** Developers building custom surfaces for Chat Switchboard
**Prerequisite:** Admin access to install surfaces
**See also:** [PACKAGES.md](PACKAGES.md) for the `.pkg` archive format, [SDK.md](SDK.md) for the `sw.*` API
---
## 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.sw?.auth?.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.sw.auth.user` | Object | Authenticated user (via SDK): `{ username, display_name, role, id, email }`. Requires SDK boot. |
| `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>
<!-- User available via sw.auth.user after SDK boot -->
<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 `sw.auth.user`
- Using `UI.toast()` for notifications
- Using `API._get()` for authenticated requests
- Using CSS custom properties for theming
- Proper `esc()` function for XSS safety