# 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 ``. - **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"}}
{{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`
${sections.map(s => html` ${s.label} `)}
`); ``` **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` Administration `); sw.shell.topbar.setSlot(html`
${categories.map(c => html` <${CatIcon} paths=${c.icon} /> ${c.label} `)}
`); // Content area: sidebar is surface-owned return html`
${sidebarSections.map(s => html`...`)}
<${SectionComponent} />
`; ``` **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 `