docs(adr): browse-as-shell with preview-pane plugins (target architecture)

Document the agreed direction: browse becomes the single shell (header +
tree + preview pane), content tools become preview-pane plugins, and
server features (account menu, permissions) are progressive enhancement —
not a server-rendered header wrapping an iframed browse. Sketches the
plugin contract (handles/render/dispose + the ctx capability object that
abstracts server-vs-local read/write/verbs) and the incremental migration
path. Captures the model settled on with the user.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-06-05 19:44:39 -05:00
parent ef849ab3fa
commit 76087c861c

View file

@ -18,6 +18,55 @@ Every ZDDC tool compiles to a single self-contained `.html` file — no servers,
---
## ADR: Browse-as-shell with preview-pane plugins (target architecture)
**Status:** accepted; migrating incrementally (2026-06).
**Context.** The seven tools have started converging on browse: it already hosts classifier (grid iframe), tables (the table-leaf iframe), forms, and the md / yaml / `.zddc`-form editors in its preview pane, and the header chrome (profile menu + elevation) is shared across every tool. Rather than maintain seven parallel apps, the target is **one shell with a plugin content pane**.
**Decision.** Browse is the shell — header + tree + preview pane, one top-level document. Content tools render into the preview pane as **plugins**. Server-only behaviour (the account menu, permission-gated affordances) is **progressive enhancement**: it activates when zddc-server serves the page and `/.profile/access` answers, and is simply absent on `file://`. We do **not** iframe browse inside a server-rendered header — browse owns its header and the server enhances it in place. (So "browse opened locally is missing the server header" resolves to "the same header with its server-only items dormant," not a separate page.)
- **Server mode** is the security boundary: browse fetches ACL-gated listings + per-entry verbs; plugins act through a capability object and can't exceed what the server grants.
- **Local mode** (`file://`) is unrestricted: a picked FS-Access directory handle, no server, no account menu — by design.
**Plugin contract.** A plugin is a module on `window.app.modules`; the shell dispatches to the first whose `handles` returns true:
```
handles(node, ctx) -> bool // claim this node / selection?
render(node, container, ctx) // mount into the preview pane (or a host element)
dispose?() // tear down (called before switching away)
isDirty?() / currentNode?() // optional: unsaved-edit guard + re-render hooks
```
`ctx` is the capability object the shell supplies — the ONLY thing that differs between server and local mode, so a plugin is written once:
```
ctx = {
mode: 'server' | 'fs',
getArrayBuffer(node), getContentWithVersion(node), // read (etag/lastmod → optimistic concurrency)
saveFile(node, bytes, contentType, opts), // write: ACL-enforced (server) / FS-Access (local)
cap.has(node, verb), // 'rwcda' subset; '' or unknown offline
// server-only (undefined offline): access(path), elevation, history(node)
}
```
The md / yaml / `.zddc`-form editors already follow this shape (`handles` / `render` / `isDirty` / `currentNode` + a ctx with `getArrayBuffer` / `getContentWithVersion`); table-leaf and classifier-grid are the same idea via an iframe bridge. Formalising `ctx` makes the contract explicit and lets the heavy tools migrate from iframe to in-pane module — preferred, for shared selection / theme / permission state with no `postMessage`.
**Migration (incremental; standalone tools keep working throughout).**
1. ✓ Editors are in-pane modules; classifier / tables / forms embed in the pane; the shell header carries the profile menu + progressive-enhancement elevation.
2. Fold `archive` into the tree + a listing plugin.
3. Make `landing` the shell's root ("no project selected") view.
4. Move `transmittal` into a workflow plugin.
5. Flip `default_tool` routing to "browse + plugin X"; retire each standalone `<app>.html` only once its plugin lands.
**Consequences / tradeoffs.**
- Preserves the single-file + offline value: the shell still builds to one `browse.html` that runs from `file://`. Heavy plugins should lazy-load in server mode to keep the bundle reasonable.
- The server stays the only security boundary; local is unrestricted by definition.
- Seven lockstep release artifacts collapse toward one shell (plus optionally-separate plugins).
- Not every tool is a clean pane plugin — `transmittal` is workflow-heavy, `landing` is really the root view — called out above.
---
## Repository Structure
Every HTML tool follows the same directory layout: