ADRs
ADR 0070 — Per-project Tela default viewport
  • Date: 2026-07-14
  • Status: Accepted
  • Feature: Tela viewport / responsive primitives
  • Builds on: ADR 0065 (per-width primitive overrides) + its 2026-07-14 amendment (Adaptive removed)

Context

Adaptive was removed from Tela (see ADR 0065 amendment). Adaptive had played two roles: the frameless full-canvas view, and the base-editing context — the mode in which editing a primitive's gap/columns wrote the base value (applied at every width) rather than a per-width propsAt override.

Removing it needed a replacement base-editing context. We chose a default device: exactly one Tela preset is marked Default; the Studio opens on it and editing at it writes the base. The open question was where the default lives. Session-only (module-load, resets to 1440×900 each reload) was rejected by product: the default is an intentional per-project choice — "create a project, set it, live with it" — so it must persist with the project, not the session.

Decision

The Tela default viewport is a persisted per-project setting.

  • Storage. New column project.default_viewport jsonb NOT NULL DEFAULT '{"width":1440,"height":900}' (packages/db/src/schema.ts, migration 20260714140000_project_default_viewport). The DB default backfills existing rows with the 1440×900 desktop base — zero-impact on current projects.
  • Create. POST /api/v1/projects assigns DEFAULT_VIEWPORT (1440×900); the owner reassigns it later.
  • Read. GET /api/v1/me/projects returns defaultViewport; it rides ProjectMeta to the client and warms a viewportCache (useProjectViewport).
  • Write. PATCH /api/v1/projects/:projectId accepts defaultViewport (validated to integer 200–4000, mirroring Tela's MIN_DIM/MAX_DIM), alongside the existing name. Both fields are optional; at least one required.
  • Wiring. The workflow page seeds Tela (setDefaultSize + setTelaSize) from the project's value on open, and persists the Default radio change back via updateProjectViewport. The seed's own subscribeDefaultSize echo is guarded so opening a project does not issue a redundant PATCH.
  • Tela package stays generic. @arno/tela owns only the in-memory defaultSize + isDefaultSize; it never imports project persistence. The app is the sole persistence wirer (seed in, subscribe out) — same seam Canvas uses for the active size.

Sorgente (prj-arno-sorgente-seed) is a synthetic seed project with no project row, so its default is not persisted — it stays 1440×900 in-memory.

Consequences

  • The base-editing context is now a durable, user-owned per-project choice.
  • Validation duplicates Tela's dimension bounds (200–4000) in apps/api/viewport.ts rather than importing @arno/tela (a client "use client" package) into the worker bundle. Two constants, unit-tested (viewport.test.ts).
  • One more project column + a one-line additive migration; no backfill script needed (DB default handles existing rows).

Alternatives rejected

  • Session-only default. Simplest (no DB), but the default reset on every reload — rejected by product: it's a per-project decision, not a session one.
  • Stash in project.prop_overrides JSONB (no migration). Avoids a column but overloads a semantically-scoped blob (captured-component prop overrides). Rejected: a dedicated column is one trivial ALTER and keeps the meaning clean.
  • A separate project_viewport row (workflow_artboard-style). Overkill for a single {width,height} scalar — a column on project is lighter and loads with the existing /me/projects fetch, no extra round-trip.