ADRs
ADR 0025 — Adaptive overrides + Tokens publish flow
  • Date: 2026-05-26
  • Status: Accepted
  • Feature: Phase 3 Layout Grid (ADR 0020)
  • Affects:
    • packages/shared/src/library.ts — Breakpoint type, resolveProps, writePropAtBreakpoint, clearPropOverride, hasOverride
    • packages/shared/src/composition.ts — updateProp + updatePropInZones extended с breakpoint param, clearOverrideInZones helper
    • packages/editor/src/composition-store.ts — updateInstanceProp(breakpoint), clearOverride()
    • apps/web/src/components/layout-grid/BreakpointContext.tsx (new)
    • apps/web/src/components/layout-grid/BreakpointSwitcher.tsx (new)
    • apps/web/src/components/composition-canvas.tsx — toolbar + ViewportSimulator
    • apps/web/src/components/layout-grid/{Content,Container}.tsxresolveProps(inst, breakpoint) вместо inst.props
    • apps/web/src/components/props-panel.tsx — breakpoint badge, override dot, reset button
    • apps/web/src/lib/tokens-store.ts (new) — draft + published state + document.documentElement.style.setProperty
    • apps/web/src/components/tokens-editor.tsx (new) — Tokens editor UI
    • apps/web/src/app/app/tokens/page.tsx (new) — /app/tokens route
  • References: ADR 0020 (Layout Grid §D3 tokens, §D5 cascade), ADR 0021 (30K MAU)

Context

ADR 0020 Phase 3 acceptance:

  • Per-breakpoint overrides на каждом Container/Content
  • Adaptive breakpoint switcher в editor UI
  • DS Publish button → CSS variables hot-swap
  • Customizable breakpoints в layout-grid.config.json (defer)
  • Acceptance: token change → click Publish → все open editors update without reload

Decision

D1. Per-breakpoint overrides на ComponentInstance

type ComponentInstance = {
  ...
  props: Record<string, string>;  // baseline = desktop
  adaptiveOverrides?: {
    tablet?: Record<string, string>;
    mobile?: Record<string, string>;
  };
  ...
}

Desktop-first cascade (per ADR 0020 §D5):

  • breakpoint === "desktop" → return inst.props
  • breakpoint === "tablet"{ ...inst.props, ...overrides.tablet }
  • breakpoint === "mobile"{ ...inst.props, ...overrides.tablet, ...overrides.mobile }

Helper resolveProps(inst, breakpoint) в shared/library.

D2. Edit-time breakpoint context

BreakpointContext (React Context) хранит current viewport: desktop | tablet | mobile. Все editing UI читают этот context:

  • Click на BreakpointSwitcher button → setCurrent(bp) → re-render canvas с resolved props
  • Edit prop → compositionStore.updateInstanceProp(..., breakpoint) пишет в props (desktop) или adaptiveOverrides[bp] (tablet/mobile)
  • Inline edit text → same routing

D3. Viewport simulation (max-width wrap)

Canvas wrapped в <ViewportSimulator> который применяет max-width per current breakpoint:

  • desktop → 100% (no constraint)
  • tablet → 1200px
  • mobile → 768px

Это позволяет дизайнеру увидеть как layout adapts без resizing browser window.

D4. Tokens draft + published state

tokens-store.ts keeps two snapshots:

  • published — applied к document.documentElement.style.setProperty('--token-name', value)
  • draft — local edits, не applied к DOM

Editor показывает draft + diff indicator (dirty dot, blue border) для tokens с unpublished changes.

Publish flow:

  1. Edit draft через tokensStore.setDraft(name, value) → only state change, no DOM mutation
  2. Click ↗ PublishtokensStore.publish() копирует draft → published + applies via style.setProperty
  3. Browser repaints — no React rerender, killer feature (ADR 0020 §D3)
  4. Discard → tokensStore.revert() копирует published обратно в draft

D5. Override UI affordances в PropsPanel

При current breakpoint !== desktop:

  • Header badge color-coded ("tablet" — yellow, "mobile" — pink)
  • Каждый prop с override показывает blue dot + "reset" button
  • Hint в footer: «Editing at tablet → changes stored as overrides»

hasOverride(inst, bp, propName) → boolean check для UI indicator. compositionStore.clearOverride(screenId, instanceId, bp, propName) → revert prop to inherited cascade.

Rejected alternatives

AlternativeReason
Mobile-first cascadeTailwind pattern, но ARNO = desktop product (ADR 0020 §D5). Designers начинают с desktop, потом adapt down.
Resolved values stored (precomputed props.desktop/tablet/mobile)Triples storage. Cascade resolution at render time — cheap (O(props count)).
Per-instance breakpoint preferenceЮзер всегда работает в одном viewport контексте, не per-element.
CSS @media queries как primary mechanismBrowser handles только viewport-driven changes. ARNO нуждается в editing-time preview без actual resize → JS-driven resolution.
Resolved styles inlined в style attr (instead of CSS variables)Bypasses Publish hot-swap. CSS vars = single repaint, inlining = full React tree rebuild.
Live token update без PublishЮзер accidentally меняет palette → ломает все pages → потеря работы. Publish — explicit commit step (ADR 0020 §D3).

Consequences

Positive

  • Designers work как в Figma — switch viewport, edit overrides, see live
  • Publish hot-swap = killer feature — token change visible везде instantly без rebuild
  • No prop drilling — BreakpointContext available throughout layout-grid components
  • Backward compatupdateInstanceProp без breakpoint param = desktop baseline (existing call sites не сломались)
  • ReversibleclearOverride revert к inherited cascade

Negative

  • +1 helper file (BreakpointContext, BreakpointSwitcher) — modest
  • PropsPanel complexity ↑ (override indicators, reset buttons)
  • Tokens stored в localStorage — пока. Phase 4 (GitHub serialization) перенесёт в design-tokens.json в connected repo.
  • Customizable breakpoints в layout-grid.config.json — defer ADR 0020 §D5, hardcoded mobile=0-767/tablet=768-1199/desktop=1200+

Performance

  • resolveProps — O(props), called per Content/Container render. Cheap.
  • Memo opportunity если bottleneck appears (useMemo for resolved props per instance).
  • CSS variables hot-swap — single repaint event, no React reconciliation.

Persistence

  • ComponentInstance.adaptiveOverrides → JSONB column через existing screen_composition pipeline (no schema migration: optional field на JSONB)
  • Tokens → localStorage (Phase 3) → connected repo design-tokens.json (Phase 4 future)

Implementation status

  • Breakpoint type + resolveProps + writePropAtBreakpoint + clearPropOverride + hasOverride в shared/library
  • updateProp + updatePropInZones extended (backward-compat optional breakpoint param)
  • clearOverrideInZones helper
  • CompositionStore.updateInstanceProp(breakpoint) + clearOverride()
  • BreakpointContext + BreakpointProvider
  • BreakpointSwitcher toolbar (Desktop/Tablet/Mobile buttons)
  • ViewportSimulator wrapper (max-width per bp)
  • Content/Container use resolveProps(inst, bp)
  • InlineEditableText commits to current breakpoint
  • PropsPanel breakpoint badge + override dot + reset button
  • tokens-store.ts draft + published state
  • tokens-editor.tsx UI
  • /app/tokens route
  • Unit tests resolveProps cascade (deferred to Phase 3-LG.5 follow-up)
  • Integration test: switch breakpoint → resolved props change (deferred)
  • Customizable breakpoints через layout-grid.config.json (ADR 0020 §D5 deferred)
  • Per-breakpoint tokens (e.g. font-size-mobile) — current model assumes single value (ADR 0020 §D5 future)

Re-evaluation triggers

  • Token usage > 100 — switch storage to Style Dictionary tokens pipeline
  • Need Figma sync — adopt Tokens Studio + Style Dictionary
  • Performance bottleneck в resolveProps (1000+ instances) — add memoization layer
  • Mobile usage > 40% sessions — review desktop-first cascade default
  • Customizable breakpoints requested — implement layout-grid.config.json parsing

References

  • ADR 0020 §D3 — Tokens via CSS variables + Publish
  • ADR 0020 §D5 — Adaptive cascade desktop-first
  • ADR 0021 — 30K MAU ceiling (justifies in-memory tokens, no Redis)
  • ADR 0024 — Storybook catalog (showcase tokens in catalog when applicable)
  • Master spec §I.5 — Reactive Vision

Changelog

  • 2026-05-26 v1.0: Initial ADR. Implementation done. Tests + customizable breakpoints deferred.