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/FRONTEND-JS-GUIDE.md
Jeffrey Smith ba4c9ca65c Feat v0.7.4 documentation + deferred surface work
Docs restructuring: category grouping (Getting Started, Platform,
Extension Development, Operations) with 14 ordered docs. Four new
guides: Permissions & Groups, Workflows, Starlark Reference, Frontend
JS Guide. Extension Guide updated with config_section docs.

Content refresh across 10 existing docs: rebrand volume names,
stale CSS vars, TLS_MODE env, self-hosted font notes, Architecture
frontend section.

Team Admin workflows.js (722 lines) split into 3 modules:
workflows.js (router+CRUD), workflow-editor.js (editor+stages),
workflow-monitor.js (assignments+monitor+signoff).

Fix: docs outline scroll no longer pushes topbar off-screen.
Fix: --bg-2 (undefined CSS var) replaced with --bg-secondary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:49:00 +00:00

7.6 KiB

Frontend JS Guide

Armature extensions run in the browser using Preact + htm — a 3 KB runtime with no build step. The kernel provides a rich SDK at window.sw that extensions use for API calls, auth, events, theming, and UI.

Getting started

Extension surfaces are ES modules loaded via <script type="module">. The SDK is available on window.sw after the sw:ready DOM event:

document.addEventListener('sw:ready', () => {
    const { html } = window;
    const mount = document.getElementById('my-mount');
    preact.render(html`<${App} />`, mount);
});

Or use the global hooks object for Preact hooks:

const { useState, useEffect } = hooks;

SDK modules

All modules live on the window.sw object. They are frozen after boot and available to every extension.

sw.api — REST client

Generic escape hatches for any endpoint:

sw.api.get('/api/v1/docs')
sw.api.post('/api/v1/teams', { name: 'Eng' })
sw.api.put(path, body)
sw.api.patch(path, body)
sw.api.del(path)
sw.api.upload(path, file)
sw.api.stream(path, body, signal)

All methods auto-inject auth tokens and return unwrapped data from { data: ... } response envelopes.

Domain namespaces provide typed CRUD methods:

Namespace Key methods
sw.api.auth login, register, refresh, logout
sw.api.teams list, get, create, members, workflows, assignments
sw.api.workflows list, get, stages, instances, advance, cancel
sw.api.channels list, get, create, update, del
sw.api.notifications list, unreadCount, markRead, markAllRead
sw.api.admin Sub-objects for users, teams, groups, packages, backup, etc.
sw.api.users search, resolve
sw.api.connections list, get, create, resolve
sw.api.ext(pkgId) Scoped client for extension API routes: get, post, put, del

sw.auth — Authentication state

sw.auth.isAuthenticated  // boolean
sw.auth.user             // { id, username, display_name, email, role, avatar }
sw.auth.permissions      // Set<string>
sw.auth.teams            // Array<{ id, name, role }>
sw.auth.groups           // Array<{ id, name, permissions }>

Lifecycle: sw.auth.login(login, pw), sw.auth.logout(), sw.auth.refresh().

sw.can — RBAC gates

sw.can('workflow.create')     // true if user has permission
sw.isAdmin                    // true if surface.admin.access granted
sw.isTeamAdmin(teamId)        // true if admin role in team

Use these to conditionally render UI elements.

sw.on / sw.off / sw.emit — Event bus

const unsub = sw.on('theme.changed', (payload) => { ... });
sw.once('auth.login', (user) => { ... });
sw.off('theme.changed');       // remove all listeners
sw.off('theme.changed', fn);   // remove specific listener
sw.emit('my.event', { data });

The event bus bridges to the WebSocket — server-emitted events (e.g. notification.created, workflow.sla_breach) arrive here.

sw.theme — Theme control

sw.theme.current   // 'dark' or 'light' (resolved)
sw.theme.mode      // 'dark', 'light', or 'system'
sw.theme.set('dark')
sw.theme.on('change', (theme) => { ... })
sw.theme.tokens    // live CSS variables as camelCase JS object

sw.storage — Namespaced localStorage

const store = sw.storage.local('my-extension');
store.set('key', { complex: 'value' });
store.get('key')     // parsed object
store.remove('key')
store.keys()         // ['key', ...]
store.clear()

sw.realtime — WebSocket pub/sub

const unsub = sw.realtime.subscribe('my-channel', 'item.updated', (data) => { ... });
// Or subscribe to all events on a channel:
const unsub = sw.realtime.subscribe('my-channel', (event, data) => { ... });

sw.slots — UI slot registry

Register components into named shell slots (e.g. toolbar areas):

const unreg = sw.slots.register('topbar-actions', {
    id: 'my-button',
    component: MyButton,
    priority: 10,
});

sw.actions — Named action registry

sw.actions.register('copy-link', {
    handler: async (url) => navigator.clipboard.writeText(url),
    label: 'Copy Link',
    icon: 'M12 2...',
});
await sw.actions.run('copy-link', someUrl);

sw.pipe — Filter pipeline

Three-stage pipeline for message processing:

sw.pipe.pre(10, async (ctx) => { /* pre-process */ return ctx; });
sw.pipe.stream(10, async (ctx) => { /* streaming */ return ctx; });
sw.pipe.render(10, async (ctx) => { /* post-render */ return ctx; });

sw.renderers — Block and post renderers

Register custom renderers for fenced code blocks or post-processing:

sw.renderers.register('mermaid', {
    type: 'block',
    pattern: /^mermaid$/,
    render: (code, container) => { /* render diagram */ },
});

sw.renderers.register('linkify', {
    type: 'post',
    render: (container) => { /* post-process rendered HTML */ },
});

sw.markdown — Unified rendering

const html = sw.markdown.renderSync(markdownString, { sanitize: false });
await sw.markdown.render(markdownString); // async variant
sw.markdown.ready // boolean — true after preload

Uses marked + DOMPurify + registered sw.renderers.

sw.users — Identity resolution

const user = await sw.users.resolve(userId);
const map = await sw.users.resolveMany([id1, id2]);
sw.users.displayName(userObj)  // display_name || username || 'Unknown'

Results are cached for 60 seconds. Batch fetches use the server's bulk resolve endpoint.

sw.testing — Test framework

For writing package runner tests:

sw.testing.suite('CRUD', async (s) => {
    s.before(async () => { /* setup */ });
    s.after(async () => { /* cleanup */ });

    s.test('creates item', async (t) => {
        const resp = await sw.api.post('/api/v1/items', { name: 'test' });
        t.assert.status(resp, 200);
        t.assert.ok(resp.id);
        s.track('item', resp.id); // auto-cleanup in afterAll
    });
});
await sw.testing.run();

sw.shell — Topbar API

The kernel injects a two-slot topbar into every extension surface. Extensions customize it via:

sw.shell.topbar.setTitle('My Surface');          // text in left slot
sw.shell.topbar.setSlot(html`<${TabBar} />`);   // center slot content
sw.shell.topbar.hide();                          // full-bleed mode
sw.shell.topbar.show();                          // restore topbar

sw.toast / sw.confirm / sw.prompt — UI primitives

sw.toast('Saved!', 'success');               // success | error | info
sw.toast('Something broke', 'error', 5000);  // custom duration

const ok = await sw.confirm('Delete this item?');
const name = await sw.prompt('Enter name', 'default value');

Shell topbar patterns

Every surface uses one of three patterns:

Pattern Description Example
A — Default Shell title only. No center slot content. Docs
B — Flat tabs setTitle() + tabs in center slot via setSlot(). Full-width content. Settings, Team Admin
C — Category tabs + sidebar setTitle() + category tabs in center slot. Surface-owned sidebar below. Admin

The shell provides the home link, notification bell, and user menu on every surface for free.

Extension CSS contract

Extensions must prefix all CSS classes with .ext-{slug}- to avoid conflicts with kernel styles. See the Extension CSS doc for the full isolation rules, available primitives from sw-primitives.css, and spacing tokens.