ADR-0006 — React only in the shell; the core stays framework-agnostic

Context

massing's web app is plain TypeScript against the DOM — no React, no Vue, no Svelte, no state library. Module-level closures and window custom events. That is a deliberate and defensible choice for an app of its shape, and it means there is no component tree to port.

MassingViewer needs a materially better UI than the one it inherits: an Office-style ribbon with contextual tabs and responsive group collapsing, a command palette, an AutoCAD-style command line, a dockable panel system, and drag-and-drop as a headline interaction. Hand-building responsive ribbon collapse, KeyTips and docking against raw DOM is where a large share of the UI budget would go, for no differentiating benefit.

But MassingViewer must also be pulled back into massing (milestone M9), and massing is vanilla. If the whole thing is React, React enters massing's bundle and its existing panels get rewritten.

Decision

Split at the rendering boundary.

Two structural consequences make this hold rather than merely being intended.

packages/ui-model exists because of this decision. The ribbon and palette layout model — which tools exist, how they group, how groups collapse — is pure data and pure math, and it lives one layer below React. Both hosts read one table. Had it lived inside the React package, massing would have grown a second copy and the two would have diverged; that is the drift shape this repo is trying not to repeat.

The boundary is a build failure, not a convention. scripts/check-architecture.mjs confines react and react-dom to ui-react, and three and @thatopen/* to viewport. It confines three for a concrete, measured reason: massing's apps/web/vite.config.ts documents resolve.dedupe: ["three"] because "Multiple instances of Three.js" was an observed failure there — two copies in one bundle produce objects that fail each other's instanceof checks, and the symptom is geometry that silently refuses to render. One importing package is what makes deduping tractable.

The core packages' tsconfig.json files also omit the DOM lib entirely, so a stray document reference is a type error at home rather than a runtime failure in someone else's environment.

Consequences