---
name: shell-editor
load-when: authoring an editor-shell — a design tool / canvas with resizable side panes
load-size: ~1.2k tokens
required-for: [shell-selection — editor path]
---

# editor-shell — the canvas + panes

Design-tool / code-editor chrome from `@adia-ai/web-modules`. Register: `import '@adia-ai/web-modules/editor'`. (Claims re-verified against the kit's live source 2026-08-26; re-verify on a MINOR cut.)

## Cluster roster

`<editor-shell>` (host; reflects `[focus-mode]`) · `<editor-toolbar>` (top bar; icon/heading/action slots) · `<editor-sidebar slot="leading"|"trailing">` (collapsible, holds `<pane-ui resizable>`) · `<editor-canvas>` (center work surface; optional `<editor-canvas-toolbar>` + `<editor-canvas-empty>`) · `<editor-statusbar>` (footer).

## Canonical skeleton

```html
<editor-shell>
  <editor-toolbar>
    <span slot="icon"><icon-ui name="brackets-curly"></icon-ui></span>
    <span slot="heading">Editor</span>
    <span slot="action"><button-ui icon="fullscreen" variant="ghost" size="sm"></button-ui></span>
  </editor-toolbar>
  <editor-sidebar slot="leading" collapsible><pane-ui resizable><tree-ui></tree-ui></pane-ui></editor-sidebar>
  <editor-canvas>
    <editor-canvas-toolbar><tabs-ui></tabs-ui></editor-canvas-toolbar>
    <div id="surface"></div>
  </editor-canvas>
  <editor-sidebar slot="trailing" collapsible><pane-ui resizable><!-- inspector --></pane-ui></editor-sidebar>
  <editor-statusbar>Status…</editor-statusbar>
</editor-shell>
```

## Props · events · methods

- **Prop:** `focus-mode` (reflected) — distraction-free; propagates `[full-screen]` to the toolbar, `[focused]` to the canvas (and calls `canvas.focus()`).
- **Event:** `editor-mode-change {focusMode}`.
- **Method:** `.toggleFocusMode()`.
- Panes resize via `<pane-ui resizable>`; sidebars collapse via `collapsible`.

## `<editor-canvas>` JS API (host wiring)

Source of truth: `packages/web-modules/editor/editor-canvas/editor-canvas.js`.

| API | Type | Contract |
| --- | --- | --- |
| `.zoom` | number get/set (default `1.0`) | Setting writes `--editor-canvas-zoom` on the canvas; shell CSS already scales every direct `<editor-canvas>` child (`editor-canvas > * { transform: scale(var(--editor-canvas-zoom, 1)); transform-origin: top left }`) — don't add your own `scale()` on top of it, or nested content double-scales |
| `.resetView()` | method | Restores zoom `1.0`; call it rather than re-implementing "zoom = 1" inline |
| `.focus()` / `.blur()` | methods | Claim/release focus; reflect `[focused]`. `toggleFocusMode()` calls `.focus()` for you |
| `[empty]` | reflected boolean | **Auto-managed** by a `childList` MutationObserver: true iff zero non-`<editor-canvas-empty>` children |
| `[focused]` | reflected boolean | Auto-set by `.focus()`/`.blur()` |

The shell binds **no keyboard shortcuts** — the host owns the chords (focus-mode toggle, zoom in/out/reset) and calls the API; toolbar buttons reach the host via the bubbled `toolbar-action` event.

**Empty state is declarative:** put an `<editor-canvas-empty>` child (with `<empty-state-ui>`) beside your content; appending/removing content children flips `[empty]` automatically and CSS shows/hides the empty state. Async content gets a free "empty until first mount" state. A *loading* state distinct from empty is your own sibling element behind your own attribute.

## Gotchas

- Import the **editor barrel**.
- Never toggle `[focus-mode]`, `[full-screen]`, `[focused]`, or `[empty]` by hand — the first three desync the shell's propagation cascade; `[empty]`'s MutationObserver overrides your toggle on the next child mutation. Use `.toggleFocusMode()` / the canvas API and child mutations.
- `<editor-canvas-empty>` is the empty-state slot, not a conditional wrapper — canvas content goes *beside* it, never inside it.
- Legacy shapes (`[data-editor-body]`, `[data-canvas]`, `data-pane-side/grow`) retired v0.4.0 — use the bespoke tags (`adia-lint` `LEGACY-SHELL`).
- SSR: swap the canvas content via the framework outlet, not `<router-ui>`.
- **A persistent `<drawer-ui data-mobile-nav-drawer>` is always in the DOM** (relocated mobile-nav mechanism, gh#1984/ADR-0090), not conditionally created — only its visibility responds to a container query at the shell's mobile-nav breakpoint. A selector assuming exactly one `drawer-ui`/`dialog` on the page (e.g. an E2E test) must scope past it: `drawer-ui:not([data-mobile-nav-drawer])`.

Real usage: `apps/genui/app/a2ui-editor/`.
