# 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:

```javascript
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:

```javascript
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:

```javascript
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

```javascript
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

```javascript
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

```javascript
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

```javascript
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

```javascript
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

```javascript
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):

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

### sw.actions — Named action registry

```javascript
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:

```javascript
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:

```javascript
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

```javascript
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

```javascript
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:

```javascript
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:

```javascript
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

```javascript
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](EXTENSION-CSS)
doc for the full isolation rules, available primitives from
`sw-primitives.css`, and spacing tokens.
