Skip to content

feat(theme): add semantic/layouts — the container system - #884

Open
gabriel-lisboa-azion wants to merge 3 commits into
mainfrom
feat/theme-layout-tokens
Open

feat(theme): add semantic/layouts — the container system#884
gabriel-lisboa-azion wants to merge 3 commits into
mainfrom
feat/theme-layout-tokens

Conversation

@gabriel-lisboa-azion

Copy link
Copy Markdown
Collaborator

What

The layout system every console page is built on, as a theme token group: ten tokens and nine @utility classes, covering three decisions expressed once — how far content sits from the app chrome (boundary), how far things sit from each other (rhythm), and how wide a reading column may get (measure).

Four container types, picked by what the page is:

Class Type Today For
.layout-column Data 1620px Lists, detail dashboards
.layout-column-focused Focused 1024px Home, single-task multi-column pages
.layout-column-form Form 1024px Settings, in-page edit forms
.layout-form-create Create 1192px Create pages (also retunes --layout-measure-control)

Plus .layout-boundary / .layout-boundary-inline, the two rhythm steps (--layout-section-gap, --layout-group-gap) with their .layout-section-start / .layout-group-start margin forms, and .layout-field-control for the control side of a settings row.

A derived group

Every token is a var() reference to --spacing-* / --container-*, never a literal length, and that is why none of them carries a breakpoint map. The spacing scale is already fluid (--spacing-lg is 1rem, then 1.5rem from sm), and var() is substituted at use time on the element — so a layout token follows the breakpoint override for free. Giving it its own map would duplicate the spacing scale into layout and let the two drift.

Why @utility and not @layer components

Same reason the typography utilities are emitted that way: @layer components classes are opaque to v4's variant resolver, and their modifiers silently drop. A layout class is exactly the kind you want behind a variant — md:layout-column.

Two things worth a reviewer's eye

.layout-field-control carries a :not(#\#) specificity pin. It is applied to Item.Actions, whose own root already declares shrink-0, and Vue merges both class lists onto one element. Custom @utility blocks sort before the core utilities, so without the pin shrink-0 wins and the control side can no longer yield to a long field name. Measured, not assumed — .layout-field-control at byte 19079 vs .shrink-0 at 31336 in a built bundle. #\# is an id no element can carry, so the selector always matches while reading (1,1,0); it is Tailwind's own important-strategy idiom and stays variant-safe. Only this one of the nine has it, because only this one has a live conflict.

The boundary widening nests on the column, not the boundary. Specificity is (0,2,0) either way, so correctness does not decide it. Nesting on the column keeps both declarations that can set max-width in one block, keeps .layout-boundary to the three padding declarations its name promises, and keeps "a fifth column class costs one line" true — they are generated from a COLUMN_MEASURE map and the nested rule comes along.

Also in the diff

  • emitIllustrationUtilities-style emission generalized into emitUtilities(map) with a thin emitLayoutUtilities caller.
  • Regenerating dist picks up one line main was stale on: an emitted comment still reading bg-[var(--x)] where the source has said bg-(--x) since ENG-47001.
  • Docs: a new Foundations → Layout Storybook page (built from the token source, so it cannot drift), the Max width section of DESIGN.md, a Layout row in Get Started, and the derived-group note in the theme's token README. DESIGN.md ships here rather than separately because that section is the catalog for these tokens — documenting them before they exist would be the drift the rule guards against.

Verification

  • build:tokens clean, assertNoZeroWithUnit passing, theme suite 12/12.
  • git diff dist/v4/globals.css is additions only apart from the stale comment: 10 --layout-* lines in :root, no new @media lines (every token is a bare var()), 9 new @utility blocks.
  • Storybook builds; validate-story-source --all reports 91/91 compliant.

Ten tokens and nine `@utility` classes: how far content sits from the app
chrome (BOUNDARY), how far things sit from each other (RHYTHM), and how wide
a reading column may get (MEASURE).

A DERIVED group: every token is a var() reference to --spacing-* /
--container-*, never a literal length. That is why none carries a breakpoint
map — the spacing scale is already fluid and var() is substituted at use time
on the element, so a layout token follows the override for free. Duplicating
the scale into layout would only let the two drift.

Emitting them as `@utility` rather than `@layer components` is what gives
them variants (`md:layout-column`), the same reason the typography utilities
are emitted that way.

Generalizes the utility emitter into emitUtilities(map) with a thin
emitLayoutUtilities caller, so the shape is reusable.

Regenerating dist also picks up one line main was stale on: an emitted
comment that still read `bg-[var(--x)]` where the source has said `bg-(--x)`
since ENG-47001.
gabriel-lisboa-azion and others added 2 commits August 11, 2026 16:07
The catalog's token allowlists are scraped from the built theme, so adding
semantic/layouts adds ten --layout-* entries. Regenerated rather than
hand-edited, which is what catalog:check verifies.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants