# Surface Development Guide **Version:** 0.25.1 **Audience:** Anyone with admin access to a Chat Switchboard instance --- ## Overview A surface is a full-screen application mode in Chat Switchboard. The platform ships with core surfaces (Chat, Notes, Settings, Admin, Workflow) that cannot be uninstalled. Extension surfaces are uploaded as `.surface` archives via the Admin panel and can be enabled, disabled, and uninstalled at runtime. This guide covers everything needed to build, package, install, and manage surfaces using only a running instance β€” no access to the Go codebase required. --- ## Quick Start Create a minimal surface in under 5 minutes: ### 1. Create the files **manifest.json:** ```json { "id": "hello", "version": "1.0.0", "title": "Hello World", "description": "Minimal surface example", "route": "/hello", "auth": "authenticated", "scripts": ["js/hello.js"], "styles": ["css/hello.css"], "source": "extension" } ``` **js/hello.js:** ```javascript (function() { document.addEventListener('DOMContentLoaded', function() { if (window.__SURFACE__ !== 'hello') return; var root = document.getElementById('surfaceRoot'); if (!root) return; root.innerHTML = '
' + '
' + '← Back' + 'Hello World' + '
' + '
' + '
' + '
πŸ‘‹
' + '

Hello from a Surface

' + '

Waiting for app init…

' + '
' + '
' + '
'; document.addEventListener('sb:ready', function() { var el = document.getElementById('helloStatus'); if (el) { el.textContent = 'Logged in as ' + (API.user?.username || 'unknown') + ' | ' + (App.models?.length || 0) + ' models loaded'; } }, { once: true }); }); })(); ``` **css/hello.css:** ```css /* Empty for minimal example. Real surfaces use CSS classes with var(--*) tokens. */ ``` ### 2. Package ```bash zip -r hello.surface manifest.json js/ css/ ``` ### 3. Install **Admin β†’ System β†’ Surfaces β†’ + Install Surface**. Upload `hello.surface`. ### 4. Visit Navigate to `https://your-instance/hello`. --- ## How Surface Loading Works ``` Browser requests /hello β†’ Router matches route from surface_registry β†’ Auth middleware runs (based on manifest "auth" field) β†’ base.html renders with: - Common CSS (variables, layout, components) - Common JS (api.js, events.js, ui-core.js, all component factories) - Your surface CSS and JS from manifest - app.js (boot script) β†’ DOMContentLoaded fires: 1. Your surface JS runs β€” build DOM, create components 2. app.js init() runs β€” auth, settings, models, WebSocket 3. 'sb:ready' CustomEvent fires when init is complete ``` ### Critical: Boot Timing Your JS runs BEFORE app init completes. | Available at DOMContentLoaded | Available at sb:ready | |---|---| | `window.__SURFACE__`, `window.__BASE__` | `App.models`, `App.settings` | | All component factories | `API.user`, `API.isAdmin` | | DOM ready | `Events` WebSocket connected | **Rule:** Anything needing models, user info, or settings goes in `sb:ready`: ```javascript document.addEventListener('sb:ready', function() { // Safe to use App.models, API.user, App.settings }, { once: true }); ``` --- ## manifest.json Reference | Field | Required | Description | |---|---|---| | `id` | Yes | Unique identifier. Lowercase, no spaces. | | `title` | Yes | Display name in admin panel and nav. | | `version` | No | Semver for display. | | `description` | No | Short description for admin. | | `route` | No | URL pattern. Gin params: `/thing/:id`. Defaults to `/{id}`. | | `alt_routes` | No | Additional URL patterns. | | `auth` | No | `"authenticated"` (default), `"admin"`, `"session"`, `"public"`. | | `scripts` | No | JS files relative to archive root. | | `styles` | No | CSS files relative to archive root. | | `components` | No | Component IDs used (documentation only). | | `source` | No | Always `"extension"` for uploaded surfaces. | --- ## .surface Archive Format ``` my-surface.surface (ZIP file) β”œβ”€β”€ manifest.json ← Required β”œβ”€β”€ js/ β”‚ └── my-surface.js ← Boot script(s) β”œβ”€β”€ css/ β”‚ └── my-surface.css ← Styles └── assets/ ← Optional: images, fonts ``` Standard ZIP. `.surface` extension is conventional; `.zip` also accepted. --- ## CSS Theme Tokens Use these instead of hardcoded colors: ```css /* Backgrounds */ var(--bg) var(--bg-secondary) var(--bg-tertiary) var(--bg-raised) var(--bg-hover) /* Text */ var(--text) var(--text-2) var(--text-3) /* Accent */ var(--accent) var(--accent-dim) var(--accent-hover) /* Borders */ var(--border) var(--border-light) /* Semantic */ var(--success) var(--warning) var(--danger) var(--purple) var(--success-dim) var(--warning-dim) var(--danger-dim) var(--purple-dim) /* Type & Layout */ var(--mono) var(--msg-font) var(--radius) var(--radius-lg) var(--transition) ``` --- ## Reusable Components All loaded by `base.html` on every surface. No imports needed. ### UserMenu ```javascript // Build DOM into a container container.innerHTML = '
' + '' + '
' + '' + '' + '' + '
' + '' + '
' + '
'; // Wire after auth (sb:ready) var menu = UserMenu.create({ id: '' }); menu.setUser(API.user); menu.bind({ onSettings: function() { location.href = (window.__BASE__ || '') + '/settings'; }, onAdmin: function() { location.href = (window.__BASE__ || '') + '/admin'; }, onSignout: function() { if (typeof handleLogout === 'function') handleLogout(); }, }); menu.showAdmin(API.isAdmin); ``` ### ChatPane ```javascript // Build DOM slot.innerHTML = '
' + '
' + '
' + '
' + '
' + '' + '
' + '
' + '
'; // Create instance var pane = ChatPane.create({ id: 'x', messagesEl: document.getElementById('xChatMessages'), inputEl: document.getElementById('xChatInput'), sendBtnEl: document.getElementById('xSendBtn'), standalone: true, }); pane.showWelcome(); ``` ### PaneContainer ```javascript PaneContainer.registerPreset('my-layout', { type: 'split', direction: 'horizontal', sizes: [250, null, 350], children: [ { type: 'leaf', id: 'nav', size: 250, minSize: 150 }, { type: 'leaf', id: 'main' }, // flex pane (no size) { type: 'tabbed', id: 'assist', size: 350, minSize: 200, tabs: [{ id: 'chat', label: 'Chat' }, { id: 'tools', label: 'Tools' }] }, ], }); var layout = PaneContainer.mount(document.getElementById('body'), 'my-layout'); var mainEl = layout._panes.get('main').el; var chatTab = layout._panes.get('assist').getTabPanel('chat'); ``` **Rules:** One child per split must omit `size` (flex pane). Drag handles cap at 60%. Sizes persist in localStorage. --- ## Making API Calls ```javascript // CRUD var data = await API._get('/api/v1/channels'); var resp = await API._post('/api/v1/channels', { title: 'New', type: 'direct' }); await API._put('/api/v1/channels/' + id, { title: 'Updated' }); await API._del('/api/v1/channels/' + id); // Streaming (SSE) var resp = await API.streamCompletion(channelId, content, model, abortSignal); var reader = resp.body.getReader(); ``` Auth tokens and base path are handled automatically. --- ## Admin: Surface Management **Location:** Admin β†’ System β†’ Surfaces | Action | Description | |---|---| | **+ Install Surface** | Upload `.surface` or `.zip` archive | | **Toggle** | Enable/disable. Disabled surfaces redirect to Chat. | | **Uninstall** | Remove extension surface (files + DB entry). | **Protected:** Chat and Admin toggles are locked β€” cannot be disabled. ### API ```bash # List curl -H "$AUTH" "$BASE/api/v1/admin/surfaces" # Install curl -H "$AUTH" -F "file=@hello.surface" "$BASE/api/v1/admin/surfaces/install" # Disable / Enable curl -X PUT -H "$AUTH" "$BASE/api/v1/admin/surfaces/editor/disable" curl -X PUT -H "$AUTH" "$BASE/api/v1/admin/surfaces/editor/enable" # Uninstall curl -X DELETE -H "$AUTH" "$BASE/api/v1/admin/surfaces/hello" # Public: list enabled (for nav) curl -H "$AUTH" "$BASE/api/v1/surfaces" ``` --- ## Checklist - [ ] `manifest.json` has `id` and `title` - [ ] Boot script checks `window.__SURFACE__` guard - [ ] DOM built in JS (no Go template dependency) - [ ] Deferred init via `sb:ready` - [ ] Topbar: `position: relative; z-index: 20` - [ ] All colors use `var(--*)` tokens - [ ] Back link: `window.__BASE__ + '/'` - [ ] Graceful with 0 models - [ ] Archive: only `manifest.json`, `js/`, `css/`, `assets/` --- ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | Surface doesn't appear after install | Routes registered at startup | Restart server or toggle disable/enable | | "Cannot overwrite core surface" | ID conflicts with chat/notes/etc. | Choose different ID | | `API is undefined` | Code runs before app init | Move to `sb:ready` listener | | No styles applied | CSS path mismatch | Check manifest `styles` matches archive structure | | Flyout hidden behind content | Missing stacking context | Add `z-index: 20` to topbar | | Drag handle snaps | v0.25.0 known issue | Sub-pixel rounding; partially mitigated |