Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
8.7 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.
sw.forms — Typed Forms (v0.9.5)
Any extension can render and validate typed forms using the sw.forms module.
sw.forms.render(container, template, opts)
Renders a typed form into a DOM container. Supports flat forms and progressive multi-step forms (fieldsets). Returns a control handle.
const handle = sw.forms.render(document.getElementById('my-form'), template, {
values: { name: 'prefilled' },
onSubmit: (data) => { console.log('submitted', data); },
});
// Programmatic access
const data = handle.getData();
handle.setErrors([{ key: 'name', message: 'Name is taken' }]);
handle.destroy();
sw.forms.validate(template, data)
Client-side validation (no network call). Returns { valid, errors }.
const { valid, errors } = sw.forms.validate(template, { name: '' });
// valid === false, errors === [{ key: 'name', message: 'Name is required' }]
sw.forms.validateRemote(template, data)
Server-side validation via POST /api/v1/forms/validate. Returns a Promise.
const result = await sw.forms.validateRemote(template, data);
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.