ADR-011 — Layout landmark grammar for /layout-generation
Date: 2026-06-28
Status: accepted
Context
The /layout-generation skill existed but produced inconsistent output across runs because it had no formal mapping from Figma’s structural hierarchy to HTML landmarks and layout utilities. Three specific problems forced this decision:
-
Improvised structure. The skill worked at primitive-composition level (Stack/Inline/Box/Card) but had no rule for which construct mapped to which Figma wrapper (Page, Section, Container, Column, Component). Structure was re-derived from the brief every run.
-
Wrong output target. The skill emitted Storybook stories into
packages/components/src/stories/. The actual goal is route pages inapps/showcase— a separate Vite + React app. Storybook is for component documentation, not full-page rendering. -
Contradictory constraints. The skill’s rulebook forbade all inline styles, yet the only existing full-page example (
Layout.stories.tsx) used.container,.grid,style={{ flex: 1 }}, andstyle={{ maxWidth }}. The rulebook and the codebase disagreed.
The Figma Course Overview page (node 96:5854) was used as the canonical design reference during planning and confirmed the hierarchy: Desktop → Header main → Section (×3) → Container → Columns → Components. It also revealed two distinct column patterns: CSS Grid (equal N-column cards) and wrapping flex (two-panel layouts that stack on mobile).
The building blocks were already present and unused by the skill:
Boxsupportsas?: ElementType, making it a polymorphic landmark renderer (<Box as="section">→<section>,<Box as="main">→<main>).containerand.gridglobal CSS utilities exist inpackages/components/src/styles/grid.css- Device tokens (
--ds-grid-columns,--ds-grid-margin,--ds-grid-gutter) handle responsive reflow automatically
Decision
Encode a fixed landmark grammar that maps one abstraction level per Figma wrapper. The grammar is the mandatory first pass of the skill; component selection and spacing follow only after the structure is locked.
| Figma level | Sanctioned code | Landmark / role | Rule |
|---|---|---|---|
| Page | <Box as="main"> |
main |
exactly one per route |
| Header | <Box as="header"> or <AppHeader> |
banner / navigation |
each <nav> inside gets a unique aria-label |
| Section | <Box as="section" aria-labelledby={headingId}> |
region |
must have accessible name; aria-labelledby → section Heading id preferred |
| Container | .container className |
presentational | max-width + grid margin; not a landmark |
| Column (N-column grid) | .grid className (CSS Grid) |
presentational | equal-width card grids (3–4+ items); column count reflows via --ds-grid-columns |
| Column (two-panel) | Inline wrap with style={{ flex: '1 0 0', minWidth }} |
presentational | two-column wrapping flex; minWidth sets the stack breakpoint |
| Component | fixed-set library component | per component | leaf |
| Footer | <Box as="footer"> |
contentinfo |
one per page |
The skill’s inline-style reconciliation replaces the prior blanket prohibition:
- Allowed:
.container/.gridclassNames;style={{ flex: '1 0 0' }}for column fill;style={{ minWidth }}for wrapping threshold;style={{ maxWidth }}for content measure. - Forbidden: raw color via inline style → use
<Text color=…>or<Heading>; raw token values outsidevar(); arbitrary CSS properties that belong in a component’s CSS Module.
The skill’s default output target changes from a Storybook story to a route page in apps/showcase/src/pages/<Name>.tsx. A --story mode retains the prior Storybook-story format for component-level review.
A scripts/validate-layout.js script (npm run layout:validate) enforces the grammar deterministically (one <main>, named sections, labelled navs, fixed-set component names only). It gates the skill: the validator must exit 0 before the output is declared done. Deterministic checks stay in scripts per the lite-agentic charter.
Consequences
Benefits:
- Layout generation is repeatable: the same brief produces the same structural skeleton across runs.
- Landmark rules are correct by construction (one
<main>, named regions) — no post-hoc a11y fix needed. - The two distinct column patterns (CSS Grid vs wrapping flex) are now explicit, matching the Figma design intent.
- The validator catches structural violations before they reach the developer’s browser, replacing the informal manual
accepts/containedBystep.
Trade-offs accepted:
- The grammar is opinionated; a brief that genuinely needs a non-standard structure requires explicit annotation to explain the deviation.
Inline wrapwithstyle={{ flex: '1 0 0', minWidth }}is the one place inline styles are accepted — this is a layout primitive concern, not a color/typography concern, so it does not violate the spirit of the typography rule.apps/showcasemust exist as a local Vite app before the skill can produce runnable output; see workstream D of the hardening plan.
Related ADRs: ADR-004 (layout token categories), ADR-005 (space vs size token split), ADR-009 (extend vs new vs internal component decision).