# Multi-Surface Packages

A multi-surface package serves multiple pages from a single package, each
with its own URL path, access level, and layout. Before v0.9.0, a package
got one route (`/s/{id}`), one auth posture, and one layout. Now a single
package can serve a public submission form alongside an authenticated
dashboard and an admin settings page.

## When to Use Multi-Surface

Use multi-surface when your package has logically related pages that share
the same backend (Starlark hooks, ext API, database tables) but need:

- **Different access levels** — public intake form + authenticated dashboard
- **Multiple views** — list view, detail view, edit view, monitor view
- **Sub-pages** — settings, admin, or debug pages within the package

If your pages don't share backend state, use separate packages instead.

## Manifest Setup

Add a `surfaces` array to your manifest. Each entry declares a page:

```json
{
  "id": "workflow-builder",
  "title": "Workflow Builder",
  "type": "full",
  "version": "0.1.0",
  "icon": "🔧",
  "auth": "authenticated",
  "layout": "single",

  "surfaces": [
    { "path": "/",                "title": "Workflows" },
    { "path": "/new",             "title": "New Workflow",    "nav": false },
    { "path": "/:id/edit",        "title": "Edit Workflow",   "nav": false },
    { "path": "/monitor",         "title": "Monitor",         "nav": true },
    { "path": "/monitor/:id",     "title": "Instance Detail", "nav": false }
  ],

  "hooks": ["surface"],
  "permissions": ["workflow.access"]
}
```

This generates five server routes, all under `/s/workflow-builder/`:

| Route | Access | Nav |
|-------|--------|-----|
| `/s/workflow-builder/` | authenticated | yes |
| `/s/workflow-builder/new` | authenticated | no |
| `/s/workflow-builder/:id/edit` | authenticated | no |
| `/s/workflow-builder/monitor` | authenticated | yes |
| `/s/workflow-builder/monitor/:id` | authenticated | no |

### Surface Entry Fields

| Field    | Default        | Description |
|----------|----------------|-------------|
| `path`   | **required**   | URL path relative to `/s/{id}`. Supports `:param` segments. |
| `access` | package `auth` | `public`, `authenticated`, `admin`, or `group:{name}`. |
| `title`  | package `title`| Label shown in nav and page title. |
| `layout` | package `layout`| `single` or `editor`. |
| `nav`    | `true` for `/`, `false` otherwise | Whether this surface appears in the sidebar. |

### Mixed Access Levels

A package can mix public and authenticated surfaces:

```json
{
  "id": "bug-tracker",
  "auth": "authenticated",
  "surfaces": [
    { "path": "/submit", "access": "public",  "title": "Report a Bug" },
    { "path": "/",       "title": "Dashboard" },
    { "path": "/:id",    "title": "Bug Detail", "nav": false },
    { "path": "/admin",  "access": "admin",   "title": "Settings", "nav": false }
  ]
}
```

The kernel enforces access per-surface. Unauthenticated visitors can reach
`/submit` but are redirected to login if they try `/` or `/:id`.

## Frontend: Routing Within Your Package

### Reading the Current Surface

When your package JS loads, two globals tell you which surface was matched:

```js
const path   = window.__SURFACE_PATH__   || '/';
const params = window.__SURFACE_PARAMS__ || {};
```

Use these to decide which view to render:

```js
function render() {
    const path   = window.__SURFACE_PATH__ || '/';
    const params = window.__SURFACE_PARAMS__ || {};

    const root = document.getElementById('surface-root');

    switch (path) {
        case '/':           return renderList(root);
        case '/new':        return renderEditor(root, null);
        case '/:id/edit':   return renderEditor(root, params.id);
        case '/monitor':    return renderMonitor(root);
        case '/monitor/:id':return renderDetail(root, params.id);
        default:            return render404(root);
    }
}

render();
```

### SPA Navigation with `sw.navigate()`

Navigate between surfaces without a full page reload:

```js
// Navigate to a static path
sw.navigate('/new');

// Navigate with params
sw.navigate('/:id/edit', { id: 'wf-42' });

// Navigate to monitor sub-page
sw.navigate('/monitor/:id', { id: 'inst-7' });
```

`sw.navigate()` does three things:
1. Updates `window.__SURFACE_PATH__` and `window.__SURFACE_PARAMS__`
2. Calls `history.pushState()` to update the URL
3. Emits a `surface.navigate` event

### Listening for Navigation Events

Re-render when the user navigates (including browser back/forward):

```js
sw.on('surface.navigate', ({ path, params }) => {
    window.__SURFACE_PATH__   = path;
    window.__SURFACE_PARAMS__ = params;
    render();
});
```

### Links Between Surfaces

For simple `<a>` links that do full page loads:

```html
<a href="/s/workflow-builder/monitor">Monitor</a>
```

For SPA-style navigation:

```js
button.onclick = () => sw.navigate('/monitor');
```

## API Routes

All surfaces in a package share the same ext API. API calls go through
`/s/{id}/api/*` regardless of which surface is active:

```js
// These work from any surface in the package
const items = await sw.api.get('/items');
const item  = await sw.api.get(`/items/${id}`);
await sw.api.post('/items', { title: 'New item' });
```

The ext API handler checks `package.status == "active"` — if the package
is in `pending_review`, all API calls return 403.

## Backward Compatibility

Packages without a `surfaces` array continue to work. The kernel
synthesizes a single-entry array from the legacy `auth` and `layout`
fields:

```
auth: "authenticated" + layout: "single"
  →  surfaces: [{ "path": "/", "access": "authenticated", "layout": "single" }]
```

No migration is required for existing packages.

## Constraints

- **No cross-package routing.** Surface paths are relative to the package
  mount point. A package cannot claim arbitrary top-level routes.
- **No per-surface Starlark hooks.** All surfaces share the same backend
  hooks. Use the surface path in your hook logic to differentiate.
- **No SSR.** Surface rendering is client-side. The kernel serves the
  shell template; your JS renders the content.
