Context
The template builder needs to scale to 100+ module types and page settings without hand-writing a form per type. The wire contract (templateGet → data.moduleList[]) is consumed by live OTT apps and cannot change. Editors expect one inspector that switches context the way Figma does — page settings when the canvas is selected, module settings when a module is selected.
Decision
Adopt a schema-driven Template Builder with three pillars (see guidelines/template-builder-architecture.md):
- Sanity-shaped definitions — pages and modules are authored with
defineType+defineField(name/title/type/fields[]).defineBlock/definePage+f.*remain supported for existing call sites. - One settings renderer —
SettingsRendererrenders any schema-backed panel (page or module). The dual-mode inspector mounts the same engine for both. - Contract adapters —
core/adapters/moduleContract.adapter.tsandpageContract.adapter.tsare the only places that map rich attributes to the wire shapes. Unknown modules round-trip via the raw passthrough block.
Working-copy state, selection, and DnD stay in useSingleTemplate / inspector hooks per the existing one-way data flow.
Consequences
Enables
- Add a page or module by writing one definition file — no inspector UI changes.
- Same settings UX for pages and modules.
- Incremental migration of legacy blocks without breaking existing templates.
Restricts
- Wire
moduleList/ page field shapes stay frozen. - No hand-written per-type settings forms; UI comes from the schema.
Trade-offs
- Field kinds must cover real cases; bespoke controls are added as new kinds, not one-off forms.
- Persistence mutations are still being wired — local apply works first.
Notes
- Extends the registry-driven precedent of ADR-008
- Invariant: persisted output must stay compatible with the existing wire contract.