# DESIGN: Shell Contract — v0.7.0

## Status: Proposed

## Problem

Extension surfaces render into `#extension-mount` with no shell chrome.
A full audit (v0.7.0 surface audit) found: 3/4 primary surfaces lack a
notification bell, 2/4 lack a user menu, 4 different topbar implementations,
user menu never updates on package/role changes, toast-and-forget error
handling, and empty states that explain nothing.

## Solution

### 1. Shell Topbar — Two-Slot Model

The kernel injects a topbar for all extension surfaces. Two named slots
(left + center) let surfaces customize without replacing the entire bar.

**Layout:**

```
┌──────────────────────────────────────────────────────────────┐
│  ←  │ [left]        │  [center: flex-1]         │ 🔔 │ 👤  │
└──────────────────────────────────────────────────────────────┘
```

- **← (home):** Always visible. Navigates to `__BASE__/`. Simple `<a>`.
- **Left slot:** Defaults to manifest title. Surfaces override with `setLeft()`.
- **Center slot:** Empty by default. Surfaces inject tabs, search, pickers.
  `flex: 1` — expands to fill available space.
- **Bell + User Menu:** Always visible. Kernel-managed.

**Template change** (`surfaces/extension.html`):

```html
{{define "surface-extension"}}
<div id="extension-surface" class="extension-surface"
     data-surface-id="{{.Surface}}">
    <div id="shell-topbar" class="sw-topbar sw-topbar--shell"></div>
    <div id="extension-mount" class="extension-mount" data-ext="{{.Surface}}"></div>
</div>
{{end}}
```

**SDK API:**

```js
sw.shell.topbar.setLeft(vnode)      // Override left slot (default: title)
sw.shell.topbar.setSlot(vnode)      // Set center slot content
sw.shell.topbar.setTitle(str)       // Shorthand: setLeft with plain text
sw.shell.topbar.hide()              // Remove topbar entirely
sw.shell.topbar.show()              // Restore after hiding
```

### 2. Three Navigation Patterns

The shell topbar provides the top bar. What happens below it is the
surface's business. Three patterns emerge naturally:

#### Pattern A — Default (simple extensions, Docs)

```
┌──────────────────────────────────────────┐
│  ←  │  Surface Title        │  🔔 │ 👤  │
├──────────────────────────────────────────┤
│                                          │
│           Content (full width)           │
│                                          │
└──────────────────────────────────────────┘
```

Title only, no tabs, no sidebar. Content gets everything.
Zero code required — kernel defaults handle it.

**Used by:** Docs, Notes, Chat, simple extensions.

#### Pattern B — Flat Tabs (no sidebar, full width)

```
┌──────────────────────────────────────────────────────────┐
│  ←  │ Title │ Tab1 │ Tab2 │ Tab3 │ Tab4 │    │  🔔 │ 👤 │
├──────────────────────────────────────────────────────────┤
│                                                          │
│              Content (full width, no sidebar)            │
│                                                          │
└──────────────────────────────────────────────────────────┘
```

Tabs in the topbar center slot. No sidebar — content fills the full
viewport width. For surfaces with 3–7 sections that don't have sub-items.
More real estate for content than a sidebar layout.

**Surface code:**

```js
sw.shell.topbar.setSlot(html`
    <div class="sw-topbar__tabs">
        ${sections.map(s => html`
            <a class="sw-topbar__tab ${active === s.key ? 'active' : ''}"
               href=${s.href} onClick=${navigate}>${s.label}</a>
        `)}
    </div>
`);
```

**Used by:** Team Admin (Members / Connections / Workflows / Settings / Activity),
Settings, Schedules.

#### Pattern C — Category Tabs + Sidebar (two-level)

```
┌─────────────────────────────────────────────────────────────┐
│  ←  │ Title │ People │ Workflows │ System │ Mon │ 🔔 │ 👤  │
├──────────┬──────────────────────────────────────────────────┤
│ Users    │                                                  │
│ Teams    │         Content                                  │
│ Groups   │                                                  │
│          │                                                  │
└──────────┴──────────────────────────────────────────────────┘
```

Major categories in the topbar (via center slot). Surface renders its own
sidebar inside the content area for sub-navigation within the active
category. The shell topbar doesn't know about the sidebar — it's a
surface-level div below `#extension-mount`.

**Surface code:**

```js
// Topbar: major categories
sw.shell.topbar.setLeft(html`
    <img src="${BASE}/favicon.svg" width="18" height="18" style="vertical-align:-3px" />
    <span style="margin-left:6px">Administration</span>
`);
sw.shell.topbar.setSlot(html`
    <div class="sw-topbar__tabs">
        ${categories.map(c => html`
            <a class="sw-topbar__tab ${activeCat === c.key ? 'active' : ''}"
               href=${c.href} onClick=${navigate}>
                <${CatIcon} paths=${c.icon} /> ${c.label}
            </a>
        `)}
    </div>
`);

// Content area: sidebar is surface-owned
return html`
    <div class="admin-body">
        <div class="admin-nav">
            ${sidebarSections.map(s => html`...`)}
        </div>
        <div class="admin-content">
            <${SectionComponent} />
        </div>
    </div>
`;
```

**Used by:** Admin.

### 3. Kernel-Provided Tab CSS

The kernel provides `.sw-topbar__tabs` and `.sw-topbar__tab` CSS so
surfaces get consistent tab styling. Not required — surfaces can style
their own slot content however they want.

```css
.sw-topbar__tabs {
    display: flex;
    align-items: center;
    gap: var(--sp-1);
    height: 100%;
}

.sw-topbar__tab {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    font-size: 13px;
    font-weight: 500;
    color: var(--text-2);
    text-decoration: none;
    border-radius: var(--radius-sm);
    transition: color var(--transition), background var(--transition);
    white-space: nowrap;
}

.sw-topbar__tab:hover {
    color: var(--text);
    background: var(--bg-hover);
}

.sw-topbar__tab.active {
    color: var(--text);
    background: var(--bg-2);
}
```

### 4. Primary Surface Migrations

#### Settings → Pattern B (flat tabs)

Currently: custom topbar (back + icon + "Settings"), sidebar nav.
After: shell topbar with flat tabs, no sidebar. Content full width.

Settings has 6 sections (General / Appearance / Profile / Teams /
Connections / Notifications) — perfect for flat tabs. The sidebar was
thin (~140px) and ate width from the content area for no good reason.

**Migration:**
- Delete `settings-topbar` div and CSS.
- Delete sidebar nav. Move section links into `sw.shell.topbar.setSlot()`.
- Remove `sb_settings_return` sessionStorage — shell home link handles it.
- Content area becomes full-width.
- Fix Teams section: add "Open Team Admin →" link, show role, add leave action.

Extension config sections (`__CONFIG_SECTIONS__`) render as additional
tabs after the divider. Same as today, just in the topbar instead of sidebar.

#### Admin → Pattern C (category tabs + sidebar)

Currently: custom topbar (favicon + "Administration" + category tabs + UserMenu).
After: shell topbar with category tabs in center slot, surface-owned sidebar.

**Migration:**
- Delete custom `admin-topbar` div and CSS.
- `sw.shell.topbar.setLeft()` with favicon + "Administration".
- `sw.shell.topbar.setSlot()` with category tabs (People / Workflows / System / Monitoring).
- Bell and user menu come from the shell — delete the explicit `<${UserMenu}>`.
- Admin sidebar and content area unchanged — they're below the topbar.
- Delete custom `CatIcon` renderer if category tab icons use standard SVG.
- Fix hardcoded `favicon.svg` — left slot can use theme-aware image.

**Result:** Admin looks identical to today but its topbar is kernel-managed.
Bell added for free. User menu reactive for free.

#### Team Admin → Pattern B (flat tabs)

Currently: custom topbar (back + "Team Admin: {name}"), sidebar nav.
After: shell topbar with flat tabs, no sidebar.

With Groups removed, Team Admin has 5 sections: Members / Connections /
Workflows / Settings / Activity. Perfect for flat tabs.

**Migration:**
- Delete `team-admin-topbar` div and CSS.
- `sw.shell.topbar.setTitle('Team Admin: ' + team.name)` after team fetch.
- Section tabs into `sw.shell.topbar.setSlot()`.
- Delete sidebar nav. Content full-width.
- Remove `sb_team_admin_return` sessionStorage.
- Remove Groups tab entirely (37-line dead-end).
- Fix signoff display: `user_id` → `sw.users.displayName()`.

#### Docs → Pattern A (default)

Currently: imports `shell/topbar.js` and renders it explicitly.
After: shell topbar auto-renders. No surface code needed.

**Migration:**
- Delete `import { Topbar }` and `<${Topbar}>` render.
- Shell topbar provides title + bell + user menu automatically.
- Docs sidebar (document list) is in the content area, unaffected.

### 5. User Menu Reactivity

**The single most impactful fix.**

**Backend — new WS events:**

```go
// After package install/uninstall/enable/disable:
h.hub.BroadcastToUser(userID, "package.changed", map[string]string{
    "action": "installed", "id": packageID,
})

// After team role/membership change:
h.hub.BroadcastToUser(userID, "auth.changed", map[string]string{
    "reason": "team_role",
})
```

**Frontend** (`user-menu.js`):

```js
useEffect(() => {
    if (!sw?.api?.surfaces?.list) return;

    function fetchSurfaces() {
        sw.api.surfaces.list().then(data => {
            const raw = Array.isArray(data) ? data : data?.data || [];
            setAllSurfaces(raw);
        }).catch(() => {});
    }

    fetchSurfaces();

    const off1 = sw.on?.('package.changed', fetchSurfaces);
    const off2 = sw.on?.('auth.changed', fetchSurfaces);

    return () => {
        if (typeof off1 === 'function') off1();
        if (typeof off2 === 'function') off2();
    };
}, [authenticated]);
```

**Event inventory:**

| Event | Trigger | Payload |
|-------|---------|---------|
| `package.changed` | Install, uninstall, enable, disable | `{ action, id }` |
| `auth.changed` | Team role change, group membership change | `{ reason }` |
| `notification.read` | Mark notification read | `{ id }` |
| `notification.all_read` | Mark all read | `{}` |

### 6. Notification Read Broadcast

**Backend** — after `MarkRead()` / `MarkAllRead()`:

```go
h.hub.BroadcastToUser(userID, "notification.read", map[string]string{"id": id})
h.hub.BroadcastToUser(userID, "notification.all_read", nil)
```

**Frontend** — `NotificationBell` listens for `.created`, `.read`, `.all_read`:

```js
const onRead = (e) => {
    setNotifications(prev => prev.map(n =>
        n.id === e.id ? { ...n, read_at: new Date().toISOString() } : n
    ));
};
const onAllRead = () => {
    setNotifications(prev => prev.map(n => ({
        ...n, read_at: n.read_at || new Date().toISOString()
    })));
};
```

### 7. Shell Announcement Global Dismiss

On dismiss, write: `localStorage.setItem('armature_dismissed_' + hash(text), '1')`.
On mount, check. New announcements (changed text) show again.

Implementation: inline `<script>` in `base.html` — checks localStorage on
DOMContentLoaded. Dismiss button writes the key. Works on every surface
including login.

### 8. Error Handling Pass

**Pattern** — inline error + retry replaces toast-and-forget:

```js
const [error, setError] = useState(null);

async function load() {
    setError(null);
    try {
        const data = await sw.api.whatever.list();
        setItems(data || []);
    } catch (e) { setError(e.message); }
}

// In render:
${error && html`
    <div class="sw-inline-error">
        <span>${error}</span>
        <button class="sw-btn sw-btn--secondary sw-btn--sm"
                onClick=${load}>Retry</button>
    </div>
`}
```

**New CSS class** (`sw-primitives.css`):

```css
.sw-inline-error {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    background: var(--bg-2); border: 1px solid var(--danger);
    border-radius: var(--radius); font-size: 13px; color: var(--danger);
}
```

**Sections requiring this pass:**

| Surface | Section | Current | After |
|---------|---------|---------|-------|
| Admin | Workflows | Toast + empty | Inline error + retry |
| Admin | Packages | Toast + empty | Inline error + retry |
| Admin | Groups | Toast + empty | Inline error + retry |
| Team Admin | Workflows (adopt) | Toast + "No global workflows" | Inline error + retry |
| Team Admin | Members | Toast + empty | Inline error + retry |
| Settings | General | Console warn | Inline error + retry |
| Settings | Teams | Toast + empty | Inline error + retry |
| Workflow Demo (pkg) | Main | Silent swallow | Inline error + retry |

### 9. Empty State Guidance

Every "No X" empty state gets: one-line explanation, primary action or doc link.

| Surface | Section | Current | After |
|---------|---------|---------|-------|
| Admin | Groups | "No groups" | "Groups control access to surfaces and features via permissions." + Create button |
| Admin | Workflows | "No workflows" | "Workflows define multi-stage approval processes with team roles and SLA tracking." + Create button |
| Admin | Teams | "No teams" | "Teams group users for shared connections, workflows, and access control." + Create button |
| Team Admin | Workflows | "No workflows — create one or adopt" | Add: "Adopt copies a global workflow for this team to customize." |
| Settings | Notifications | "No notification preferences" | "Preferences appear when notification types are configured by an administrator." |

## Rebrand Asset Inventory

### Current → Target

| File | Current | Target |
|------|---------|--------|
| `favicon.svg` | Dark icon ✅ | Unchanged |
| `favicon-light.svg` | **MISNAMED** (520×80 wordmark) | Square icon, light mode **(NEW)** |
| `favicon-32.png` | Dark raster ✅ | Unchanged |
| `favicon-256.png` | Dark raster ✅ | Unchanged |
| `favicon-light-32.png` | Missing | Light raster **(NEW)** |
| `favicon-light-256.png` | Missing | Light raster **(NEW)** |
| `favicon.ico` | Dark ✅ | Unchanged |
| `wordmark.svg` | Missing | **RENAMED** from current `favicon-light.svg` |
| `wordmark-dark.svg` | Missing | Light text #E5E5E5 on transparent **(NEW)** |
| `manifest.json` | "Self-hosted AI chat..." | "Self-hosted extension platform..." |

## Bug Fixes (bundled)

- **evil-chat:** `finally` cleanup + tighten `409` assertion.
- **Workflow demo:** Silent `catch` → inline error + retry.
- **Hello dashboard:** Delete `packages/hello-dashboard/`.

## Changeset Plan

| CS | Scope | Description |
|----|-------|-------------|
| CS-1 | Backend (Go) | WS events: `notification.read`, `notification.all_read`, `package.changed`, `auth.changed`. Tests. |
| CS-2 | Frontend (JS + HTML) | Shell topbar component, SDK API (`setLeft`/`setSlot`/`setTitle`/`hide`), extension.html template, SDK boot auto-mount. |
| CS-3 | Frontend (JS) | User menu reactivity: listen for `package.changed` + `auth.changed`. Notification bell: listen for `.read` + `.all_read`. |
| CS-4 | Frontend (JS + HTML) | Surface migrations: Settings → Pattern B, Admin → Pattern C, Team Admin → Pattern B, Docs → Pattern A. Delete custom topbars. |
| CS-5 | Frontend (JS + CSS) | Error handling pass + empty state guidance. `sw-inline-error` CSS. All sections from inventory. |
| CS-6 | Frontend (JS) | Announcement global dismiss (localStorage). |
| CS-7 | Static + docs | Rebrand assets, manifest.json, REBRAND-SPEC.md. |
| CS-8 | Frontend (JS) | Bug fixes: evil-chat cleanup, workflow demo error, hello-dashboard deletion. |

Each changeset independently CI-green.
