- Date: 2026-05-26
- Status: Accepted
- Feature: Phase 3 Layout Grid (ADR 0020)
- Affects:
packages/shared/src/library.ts— Breakpoint type, resolveProps, writePropAtBreakpoint, clearPropOverride, hasOverridepackages/shared/src/composition.ts— updateProp + updatePropInZones extended с breakpoint param, clearOverrideInZones helperpackages/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 + ViewportSimulatorapps/web/src/components/layout-grid/{Content,Container}.tsx—resolveProps(inst, breakpoint)вместоinst.propsapps/web/src/components/props-panel.tsx— breakpoint badge, override dot, reset buttonapps/web/src/lib/tokens-store.ts(new) — draft + published state +document.documentElement.style.setPropertyapps/web/src/components/tokens-editor.tsx(new) — Tokens editor UIapps/web/src/app/app/tokens/page.tsx(new) —/app/tokensroute
- 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"→ returninst.propsbreakpoint === "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:
- Edit draft через
tokensStore.setDraft(name, value)→ only state change, no DOM mutation - Click ↗ Publish →
tokensStore.publish()копирует draft → published + applies viastyle.setProperty - Browser repaints — no React rerender, killer feature (ADR 0020 §D3)
- 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
| Alternative | Reason |
|---|---|
| Mobile-first cascade | Tailwind 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 mechanism | Browser 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 compat —
updateInstancePropбез breakpoint param = desktop baseline (existing call sites не сломались) - Reversible —
clearOverriderevert к 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 (
useMemofor 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/tokensroute - 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.jsonparsing
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.