Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
455 lines
12 KiB
Markdown
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
|