ADR-0009 — The ribbon renders in vanilla DOM; React wraps it

Context

ADR-0006 split at the rendering boundary: core packages stay framework-free, React is confined to packages/ui-react, and packages/ui-model holds the ribbon layout model so both hosts read one table. That decision rested on an assumption stated in its own consequences:

Two rendering implementations of the toolbar exist during the transition: massing's apps/web/src/viewer/toolbarView.ts and the React ribbon. packages/ui-model is the shared truth between them, and M9 removes the duplication.

Two requirements have since been stated explicitly, and the second one falsifies that assumption:

  1. MassingViewer must work standalone. People should be able to use just this and nothing else.
  2. Development continues in massing. massing is not being replaced by MassingViewer; both go on.

If massing keeps developing and consumes MassingViewer, the duplication is not transitional — it is permanent. Under ADR-0006 as written, that leaves three options, and all three are bad:

Decision

Move the rendering boundary down one level. The ribbon renders in vanilla DOM, in packages/ribbon, and React wraps it.

The direction of the dependency is the whole argument:

This is not a new pattern here — it is the pattern packages/viewport already uses, and for the same reason. The viewport is imperative three.js precisely so that any host can own a canvas, and ADR-0006 endorsed that while making the opposite choice one layer up. The ribbon has the same shape: long-lived, imperative, performance-sensitive, and needed by two hosts.

packages/ui-react remains in the plan and keeps its purpose — the panels, the property grid, the docking, the palette — but it becomes a consumer of the ribbon rather than its implementation.

What stays exactly as ADR-0006 decided

The principle is unchanged and the amendment is narrow. ui-model is still the single source of layout truth, and it is now more important rather than less: which tools exist, how they group, how groups collapse at a given width, and which are dimmed is pure data and pure math with no DOM. That is what makes the renderer thin enough to be worth having only one of.

scripts/check-architecture.mjs still confines react and react-dom to ui-react, and three to viewport. The core packages still omit the DOM lib so a stray document is a type error at home.

Consequences

What would change this decision

If massing adopts React for its own reasons, the argument evaporates: a React ribbon would then serve both hosts, and the vanilla renderer becomes the one with a single consumer. That is a decision for massing to make on its own merits, and this ADR should be revisited the week it does.