- Date: 2026-06-09
- Status: Accepted
- Phase / Feature: Sorgente — instance-level structural edits
- Builds on: ADR 0052 (shared-DB), ADR 0053 (Foundation rules), ADR 0054 (Library surface), LayoutInspector module
- Scope: Sorgente only. Other surfaces follow if pattern proves out.
Context
Sorgente's token + brand surfaces let the maintainer change appearance. ADR 0053 covers colors, type, spacing, radii, effects. ADR 0054 wired the library showroom. None of that touches structure — if a particular <TokenChip> instance should disappear ("not relevant in this surface"), there's no override. Source code remains the only handle. That breaks the maintainer's promise ("one DB, edits propagate everywhere") for layout decisions.
This ADR adds layout overrides: per-selector CSS rules persisted in the shared sorgente schema, applied at runtime via a polled overlay (same shape as tokens). Hide is the first operation; the storage shape generalises to any structural CSS property.
A right-click context menu becomes the entry point. The native browser menu is suppressed across all ARNO chrome.
Decision
§1 — Storage
New table sorgente.layout_override:
| column | type | notes |
|---|---|---|
| id | text PK | nanoid |
| user_id | text FK → user.id (cascade) | per maintainer |
| selector | text NOT NULL | CSS selector to match (span.tchip[data-arno-component=...]) |
| property | text NOT NULL | CSS property (display, visibility, ...) |
| value | text NOT NULL | CSS value (none, hidden, ...) |
| note | text | optional, why this was applied (free-form) |
| created_at | timestamptz | |
| UNIQUE | (user_id, selector, property) | one override per selector+property |
Per-user scope (matches the rest of sorgente.*). Multi-maintainer evolution = same row per user. Cross-user "shared overrides" deferred — adding scope column when needed.
§2 — API
GET /api/v1/sorgente/state → … + layoutOverrides[]
POST /api/v1/sorgente/layout-override body: { selector, property, value, note? }
upsert by (selector, property)
DEL /api/v1/sorgente/layout-override/:id remove by row id
POST /api/v1/sorgente/layout-override-batch body: { ops: [{op, ...}] }
for undo group commitSame requireMaintainer gate as the rest of /api/v1/sorgente/*.
§3 — Property allowlist
Only structural CSS is acceptable in value. Validation in handler:
display, visibility, opacity,
grid-template-columns, grid-template-rows, grid-area, grid-column, grid-row,
flex, flex-direction, flex-wrap, order,
position, inset, top, right, bottom, left, z-index,
overflow, overflow-x, overflow-y,
pointer-eventscolor, background, font-*, padding, margin etc. — out. Those flow through sorgente.token_override. No conflict because the namespaces don't overlap on property name. (When they do — e.g. display: none overrides a token's padding — none wins by removing the element entirely, no visual conflict.)
§4 — Runtime application
SorgenteLayoutOverlay (sibling of SorgenteOverlay) — subscribes to the same useSorgenteState provider, renders <style id="arno-sorgente-layout-overlay"> containing each override as a rule:
span.tchip[data-arno-component="TokenChip"][data-arno-role="surface-tile"] { display: none !important; }!important — the override is explicit maintainer action, must beat any in-DOM specificity. Style tag updates on each poll cycle (2s) like the token overlay.
§5 — Right-click context menu
Document-level contextmenu listener mounted in /app/layout.tsx. event.preventDefault() suppresses the native browser menu across all ARNO chrome. Skip on <input>, <textarea>, <select>, <a href> so text-edit affordances stay native.
Menu UI = absolutely positioned overlay at cursor coords. Items:
TokenChip · surface-tile ← inferred component + role
3 similar instances on this page ← findSimilar(el).length
Hide this one ← creates override matching only this element
Hide group (3) ← creates override matching all in group ← default
Reset hides on this component ← removes all overrides under this subtree
Preview full (toggle hides off) ← temp local disable of overlay
Open in Layout Inspector ← if Inspector is closed, opens it focused on elESC / click-outside dismisses. Single menu instance at a time (re-render in place if right-click happens again).
§6 — Group detection (findSimilar(el))
Three signals, picked in priority order. First with >1 match wins.
-
Explicit code-side hints —
data-arno-component+data-arno-roleattributes on ARNO chrome components.
Selector:[data-arno-component="X"][data-arno-role="Y"]. Authoritative — code says "these are peers". -
DOM subtree signature — hash =
tag+ sortedclassList+ ordered list of children signatures, recursive. Compare against every other element in the page; matches with identical hash form the group.
Heuristic but predictable; distinguishes "chip with button" from "chip without button" because subtree differs. -
Parent + same selector — siblings with same
tag.classListunder the same parent. Weakest signal; final fallback.
Performance: signature computed lazily on right-click only (not every DOM mutation). Cached in a WeakMap<Element, string> until the next page navigation. Walk cost ≈ DOM node count, acceptable for the maintainer's own page.
§7 — Cmd+Z undo
Local in-memory stack per browser tab. Each menu operation pushes one entry:
{ kind: 'hide' | 'show' | 'reset', selector, property, value, prevValue }- Cmd+Z — pop, send inverse op to API, optimistic state update
- Cmd+Shift+Z — re-push, re-send original op
- Stack lifetime = tab session. Old overrides from previous sessions don't enter the stack — they're "historic state", visible in LayoutInspector "Hidden" tab and removable from there.
§8 — LayoutInspector "Hidden" tab
LayoutInspector panel gets a new tab listing all active layout overrides for this user:
selector | property | value | actions
─────────────────────────────────────────────────────────
span.tchip[…tile] | display | none | [show] [scroll-to]
.slc__hier-box… | visibility | hidden| [show] [scroll-to]show→ DELETE the row → element re-appears within 2sscroll-to→document.querySelector(selector).scrollIntoView()to find a matching instance
"Show all hidden" — bulk DELETE.
§9 — Preview full
Temporary local state — when toggled, the SorgenteLayoutOverlay sets its <style> content to empty. Affects only this browser tab; doesn't touch DB. Click "Bring overrides back" / dismiss → re-renders normally. Useful to verify what the unedited surface looks like before deciding to keep / reset.
§10 — ARNO components data-attrs
ARNO chrome components add explicit hints so findSimilar (§6.1) gets the authoritative signal first:
<span
className="tchip"
data-arno-component="TokenChip"
data-arno-role={role}
data-arno-key={tokenId} // unique identity within the role
>Phase 1 — TokenChip. Phase 2 — Button, sidebar items, navbar tabs, swatches. Phase 3 — rest of catalog UI. Not retroactively required everywhere — DOM signature fallback works.
§11 — Skip protections
- Never match selectors that would hide LayoutInspector / Context menu /
<html>/<body>/ app shell mount nodes - Confirmation prompt when selector matches > 50 elements
- Refuse to apply overrides with non-allowlisted property (validated server-side per §3)
§12 — Cross-environment propagation
Same as tokens: edits land in sorgente.layout_override on dev Neon (shared by all test+dev stands). Next poll cycle on any open editor — overlay refreshes — hidden elements disappear consistently. Prod doesn't carry SORGENTE_DATABASE_URL → endpoints return 503 → no overlay → prod renders unmodified ARNO chrome. Matches ADR 0052 §2.
§13 — Phasing
- DB migration + drizzle schema
- API endpoints + tests
- SorgenteLayoutOverlay component
- findSimilar() module
- ContextMenu component + mount
- TokenChip data-arno-* attrs
- Cmd+Z undo
- LayoutInspector "Hidden" tab + Preview
- Test deploy + visual verification
Anti-patterns explicit
- Do not ship layout overrides as localStorage. They MUST persist in Sorgente DB so test stands stay consistent across maintainer's open tabs.
- Do not apply overrides via React state — must be CSS via
<style>tag so the cascade catches every match (incl. dynamically inserted elements after override is applied). - Do not allow non-allowlisted CSS properties in
value. Token namespace owns appearance; layout namespace owns structure. No overlap. - Do not hide the LayoutInspector or context menu via the system itself (meta-tool can't disappear).