Mosaic one model, many lenses

ADR 0032 — Six-position shell slots + split/tabs page templates

This ADR closes the remaining app-shell parity gap flagged by ADR 0031 ("the six-position SlotShell itself is still not adopted") and adopts EE's page templates (split_page, tabs_page) as a DSL choice. Where Mosaic's existing surfaces already do more, they are kept and extended (the switchable shell layout, projection charts on the split page) instead of replaced.

1. The shell is a six-position slot system

Context

EE's SlotShell (blocks/ui/ee-theme) is a frame with named injectable positions — header_left, header_right, sidebar_top, header_tools, tenant_selector, sidebar_scenes, footer, events_panel, help_panel — and the app frame composes them. Mosaic's shell had a fixed content model (brand, nav, actions, footer) with no declarative way to inject content into positions.

Decision

The generated web shell is now a six-position slot system, with the switchable layout (topbar/sidebar/bottom/floating, ADR 0022-era data-layout) kept on top of it — the layout moves the nav between positions, the slots fill them:

  • header-left — the brand (declared app.ui.brand or the app name).
  • header-center — a filter navigation search box (client-side JS filters the scene links and hides empty scene groups) plus any declared app.ui.slots entries for this position.
  • header-right — the nav actions (SSO sign-in/out, the P31 realm bar, theme toggle, layout toggle).
  • menu-top / menu-bottom — declared app.ui.slots links placed at the top/bottom of the nav.
  • footer — the P31-era footer (copyright + links).
  • events/timeline slot — when the app has aggregates, a live ticker over the CQRS event log (SSR paints from the store; spawn_client refreshes via GET /api/events/list) with a timeline link into the /cqrs replay scrubber — the full timeline-toolbar parity (EE's sticky scrubber island is that page itself).

New DSL: app.ui.slots —

app:
  ui:
    slots:
      - position: header-center   # header-center | menu-top | menu-bottom
        label: knowledge
        path: /knowledge          # internal path or external URL (new tab)

Positions are validated at plan time (bad-app-ui-slot). EE's tenant_selector maps to the P31 realm bar, sidebar_scenes to the scene nav groups, and help_panel to /docs + the nav search — no separate positions are emitted for them (their content already lives in a slot).

2. split and tabs page templates

Context

EE ships page templates in blocks/ui/ee-ui/src/components/pages/: list_page (toolbar + list), detail_page (back nav + optional sidebar), dashboard_page (KPI grid), split_page (master-detail: 360px master column

  • detail pane), tabs_page (header + tab panels). Mosaic already generates the list/detail/dashboard shapes from ui: hints; the split and tabs shapes were missing.

Decision

ui.template on a projection selects the list page shape:

projections:
  - name: Tickets
    fields: [ … ]
    on: [ … ]
    ui:
      template: split   # split | tabs — absent → the classic list page
  • split — master-detail: the rows card (filters + sortable headers + reactive table, unchanged) becomes the left master column (380px grid track, 1fr detail track; stacks below 768px); each row is clickable (row-selectable) and selecting one renders the row inline in the right pane as a label/value card (title = the row key), so the detail view exists even without ui.detail_page. Charts stay above the split — EE's split_page has no charts; Mosaic keeps them.
  • tabs — rows / charts as tab panels (a tab bar only when charts exist; each panel is a Show over a tab signal). This is EE's tabs_page shape with Mosaic's reactive table and SVG charts as panels.

template is validated at plan time (split | tabs); both compile in every hydration mode (verified for wasm32-unknown-unknown and native SSR).

Incidental fix

A projection page whose list is off (ui.list_page.enabled: false) with no charts and no admin commands rendered an empty <Layout> — a required-children component, so the generated app failed to compile. Such pages are now suppressed, and declared charts render even when the list is off (charts are a list-page hint, not the list). No example hit this before P32 (no example declared list_page.enabled: false).

Consequences

  • The app-shell feature-map row (codegen.web in sync/ee.md) moves from "partial (slot shell not adopted)" to landed-plus-more: the six-position slot system with the events/timeline slot, tenant scoping (P31), and the four-position switchable layout on top.
  • split/tabs are opt-in per projection; the classic list page stays the default, so existing apps render unchanged.
  • help_panel remains a /docs + search combination rather than a dedicated shell position — revisit if an app needs in-shell help content.
  • The EE transport.rs ClientTransport seam (InProcess/Browser/LocalStore/ Replay) stays mapped to Mosaic's cfg-aware api/api_put/api_post fetch helpers (P30) — a single send() entry point is not worth a rename while every call site is generated code.