- Date: 2026-07-12
- Status: Accepted
- Builds on: ADR 0059 (naming chord), ADR 0061 (
layerTypeOfshared dispatch), ADR 0062 (carrier contract)
Context
The four Studio tools — dito (bus-driven hover panel), tela (viewport-preset strip), layers (layers sidebar), punta (property / spacing inspector) — currently live inside the web app at apps/web/src/dev-tools/studio/<tool>/. They are mounted from exactly one place: the /app/library route (apps/web/src/app/app/library/page.tsx).
We want to reuse these tools in other parts of ARNO, and — critically — we want a single source of truth: an edit to a tool's code must propagate to every place it is used at once, with no copies to keep in sync. As the tools evolve, every consumer inherits the change on the next build.
The tools are not four independent islands. They share a runtime kernel that every one of them binds to:
studio-bus.ts— module-level singleton event bus (hover / pick state, tree mutators, clipboard, capability flags, active-tree handle)useStudioTree.ts— the tree store (undo history, debounced save,registerActiveTree)studio-events.ts— batched telemetry POSTtypes.ts(LayoutNode,isContainer,findNode,genId) andtree-ops.ts(moveNode,groupNodes,insertFromPalette)adaptive/primitives/primitive-meta.tsx— primitive glyphs / labels (shared by layers and dito)
And they cross-import each other's internals:
| imports → | dito | tela | layers | punta |
|---|---|---|---|---|
| dito | — | — | single-child | layer-type, PuntaIcons |
| tela | — | — | — | — |
| layers | — | — | — | node-bridge |
| punta | — | — | single-child | — |
Plus runtime (non-import) coupling: dito is driven by hover events layers emits with source:"layers" and positions itself against layers' [data-layer-id] DOM; tela's size store is applied by the canvas component apps/web/src/components/sorgente-library-catalog.tsx, not by tela itself.
Consequence: no single tool detaches cleanly on its own. The shared kernel must come out first, and the tools carry genuine inter-tool dependencies.
Decision
Each Studio tool becomes its own workspace package (packages/<tool>, name @arno/<tool>), sitting on top of one shared kernel package. Consumers everywhere in ARNO import the tool from its package (import { Dito } from "@arno/dito") via workspace:*. An edit to packages/dito/src/** propagates to every importer on the next build / HMR pass — one source of truth, zero copies. This is the reuse mechanism the request asks for.
Package set
@arno/ponte— the shared kernel:studio-bus,useStudioTree+ tree store,studio-events,types,tree-ops,adaptive/primitives. Every tool depends on it. Name per ADR 0059: ponte = bridge (Ponte Vecchio spans the Arno) — the substrate that bridges all tools into one live tree. Rejectedstudio-core/shared(generic, forbidden by ADR 0059 §"No generic English replacements").@arno/tela— self-contained; no other-tool imports.@arno/punta— its internals (layer-type,node-bridge,PuntaIcons) are imported by dito and layers.@arno/layers— see chord-debt note below.@arno/dito— depends on punta + layers at import and layers at runtime.
Inter-tool imports become explicit inter-package workspace:* dependencies (dito → @arno/punta + @arno/layers; layers → @arno/punta; punta → @arno/layers). "Per-tool" (поштучно) holds at the package boundary; it does not mean zero dependencies between them.
Extraction order (forced by the dependency graph)
@arno/ponte— first; all four bind to it.@arno/tela— autonomous, validates the package template on the simplest tool. Its size applier insorgente-library-catalog.tsxtakes a@arno/teladependency.@arno/punta— before its consumers (dito, layers).@arno/layers— before dito (dito runtime-depends on it).@arno/dito— last.
Package template
Follow packages/foundation (the React-component package pattern): name @arno/<tool>, version 0.0.0, private, type module, peerDependencies react/react-dom ^18, exports barrel ./src/index.ts (none of the tools has a barrel today — each gets one created as its entry), deps @arno/ponte: workspace:*, devDeps @arno/eslint-config / @arno/tsconfig / vitest. pnpm-workspace.yaml already globs packages/* — no glob change needed.
Implementation refinement — shared cross-tool utils live in the kernel
Two couplings surfaced during extraction that the naive per-tool split did not anticipate, resolved by pulling shared pure code into @arno/ponte rather than duplicating or creating circular tool deps:
useStudioTreestays app-side. It is imported only by the canvas and binds persistence (sorgente-draft,api-client); it is the app's kernel binding, not part of the reusable kernel. It reaches@arno/pontelike any consumer.single-child(mergedLeafChild) promoted to@arno/ponte. It is a pure tree util used by punta, dito, and layers. Left inlayers, it forcedpunta → layerswhilelayers → punta(node-bridge) already existed — a circular package dependency. Promoting it to the kernel severspunta → layers, yielding an acyclic DAG:ponte ← punta ← {layers, dito}.- General rule: a pure, side-effect-free utility imported by two or more tools belongs in
@arno/ponte, not in whichever tool happened to define it. This keeps tool packages depending only downward (on the kernel) or on a single acyclic chain. studio-eventstransport is injected (configureStudioEvents) so the kernel carries no app coupling; the app wires it once viastudio/configure-ponte.ts.
Naming chord-debt (deferred, by decision)
@arno/layers is a generic English name and breaks the ADR 0059 chord (dito / tela / punta are Italian). Keeping layers is a deliberate deferral to avoid entangling the extraction refactor with a rename: the rename lands in a separate ADR + PR. Candidates on record: ordito (weaving warp — the structural threads, pairs with tela/cloth) or strati (literally "layers"). Tracked as chord-debt; do not treat the current @arno/layers name as endorsed by ADR 0059.
Consequences
Positive
- Single source of truth: one edit in
packages/<tool>reaches every consumer across ARNO on rebuild. No copy drift. - Tools reusable anywhere via a plain import; each evolves centrally.
- Package boundaries make the (previously implicit) coupling explicit and lintable.
Costs / risks
- The kernel (
@arno/ponte) must be extracted before any tool — no tool ships independently first. - Tools are not independent islands: dito, layers, punta carry inter-package deps; only tela is standalone.
sorgente-library-catalog.tsx(canvas size applier) must depend on@arno/tela.@arno/layersis a known chord-violation until the rename ADR.studio-busis a module-level singleton; a package must preserve single-instance semantics (one bus per app, not per import) — bundler dedupe of@arno/ponteis load-bearing.
Regression gate: after every extraction step, /app/library (Sorgente tokens view) must render and drive all four tools exactly as before — same pick / hover / edit behavior.
Alternatives rejected
- Copy tool code into each new site — no propagation, defeats the entire request.
- One mega
@arno/studiopackage — contradicts the per-tool decision, couples release cadence of unrelated tools, and forces consumers to pull all four to use one. - Figma Code Connect — solves design↔code mapping, not runtime component reuse across the app. Different problem.