Responsive Architecture
Status: shipped (phases 1–5). A container-query-first responsive system — fluid type, per-container density, control sizing + touch floor, and
@containerlayout — generalised from the dp-toolingfeature/DOXP-11container-relative type-scale idea. This page is the reference; thetests/responsive.test.mjsguard keeps it honest.
Control Room is a dense, panel-composed dashboard system. Today the responsive story is thin and viewport-only:
- Type — a fixed px scale (
--text-2xs…--text-xl) plus a few half-finished role tokens (--type-display-*,--type-data-*,--type-label-*). Nothing scales with its container. - Spacing — one static 4px step scale (
--space-0…--space-16). No density mode; a card in a wide panel and the same card in a narrow rail get identical padding. - Sizing — control heights are ad-hoc; there is no pointer-aware tap floor (touch targets can fall below 44px on coarse pointers).
- Grid — layouts are hand-rolled per route; no shared density/column model.
- Zero container queries anywhere (verified). Everything keys off the viewport, which is the wrong axis for a system whose unit of composition is the panel.
The core thesis: the panel, not the viewport, is the responsive unit. A widget should size to the box it’s dropped into, so the same component reads correctly in a full-width board and a 240px rail without per-placement overrides.
Principles
Section titled “Principles”- Container-query-first. Components respond to their container
(
container-type: inline-size+cqiunits); the page shell is the only thing that responds to the viewport. - Three tiers, unchanged. Responsive values live as component tokens referencing new primitive scales — same primitive → semantic → component model already in place. No new mechanism, just new scales.
- Density is a mode, not a rewrite. One switch (
data-density="compact"+ault comfortable) remaps spacing/sizing tokens; components never branch on it. - Fixed where fixed matters. Borders, hairlines, the hard-shadow idiom, and radius (always 0) stay non-fluid — they’re chassis, not layout.
Proposed pieces
Section titled “Proposed pieces”1. Type scale (the concrete port)
Section titled “1. Type scale (the concrete port)”Eight roles, each carrying size + weight + leading + tracking:
| Tier | Roles | Unit | Rationale |
|---|---|---|---|
| Macro | display · h1 · h2 | clamp() on vw | page-level headings track the viewport |
| Dense | body · data · label · meta | clamp() on cqi | panel text tracks the panel |
| Micro | chrome | fixed (<11px) | sub-label chrome, never fluid |
- The
.cr-panelsurface is the query container: it setscontainer-type: inline-size— neversize(that also containers the block axis and collapsed intrinsic heights in dp-tooling; heed up front). - Folds the existing
--text-*px scale in as the fallback rung.
2. Spacing — dual-axis
Section titled “2. Spacing — dual-axis”- Keep the 4px
--space-*primitive as the base grid. - Add a density remap:
--gap,--pad,--pad-tight,--pad-loosesemantic spacing tokens that resolve to different--space-*steps under:root[data-density="compact"]vs comfortable. Components consume--pad, not--space-3, so density is one switch. - Optional fluid gaps for macro layout regions via
clamp()oncqi.
3. Sizing
Section titled “3. Sizing”- A
--control-hscale (sm/md/lg) so inputs/buttons/selects share heights. .cr-tappointer-aware floor:@media (pointer: coarse) { min 44px }— keyed to pointer type, not width (ported from dp-tooling; today control-room has no such floor).- Fluid component max-widths (
min(100%, …)) instead of fixed rems where sensible.
4. Grid / density model
Section titled “4. Grid / density model”- A
.cr-grid-autopanel grid (repeat(auto-fill, minmax(var(--col-min), 1fr))) that reflows by container width via@container, not media queries. - Named container breakpoints (
--bp-panel-sm/md/lg) expressed as@containerranges, so a panel restyles itself when narrow regardless of window size.
Interaction with what exists
Section titled “Interaction with what exists”- Tokens — all new scales land in
tokens/tokens.json(primitive + atypegroup + semantic spacing) and flow throughbuild:tokens→dist/control-room.cssanddist/tw-theme.css, guarded byverify:tokens. - Tailwind — the fluid roles become
text-<role>/p-<pad>utilities via the existing@themebridge (so both the classes-first.cr-*layer and utility consumers get them). - pt/dt contract — unaffected; density/type are token-level, so
dtoverrides still work per instance. - a11y — respect
prefers-reduced-motion(already) and add the pointer tap floor; fluid type must keep min sizes ≥ the AA-legible floor.
Agreed direction (decided)
Section titled “Agreed direction (decided)”- Density switch — per-container.
data-density="compact"may be set on any container; its subtree remaps--pad/--gapvia the CSS cascade (no per- component branching). - Spacing fluidity — stepped inside components, fluid at regions. Components
keep a fixed rhythm (
--pad/--gapon the--space-*grid); only macro layout regions useclamp()/cqigaps. - Breakpoints — 3 panel ranges (
--bp-panel-sm22rem ·-md34rem ·-lg52rem), room to add more. - Type roles — all 8 (
display/h1/h2display-register onvw;body/data/label/meta/chromedata-register, dense ones oncqi). - Migration — breaking big-bang. The new roles/scales become the system; the
fixed
--text-*scale is retired once every consumer is migrated. Executed in internally-gated phases so each step stays reviewable, converging on the break.
Directive: prefer the most modern responsive option at each choice.
What shipped (by phase)
Section titled “What shipped (by phase)”- Phase 1 — token foundation ✅: all 8 type roles +
--pad/--gapdensity aliases +--control-h-*+--bp-panel-*intokens.json→dist/control-room.css. - Phase 2 — fluid type ✅: the
--text-*rungs are now boundedcqiclamps (floor = legacy px → never shrinks / no new overflow); the.cr-panelsurface is acontainer-type: inline-sizequery container, so its dense text sizes to the panel. Opt-in.cr-type-*role utilities. (Hero/masthead are deliberately not containers: they carry dark-text-on-bright signal fills, andcontainer-typeblocks axe’sgetBackgroundColorfrom resolving a descendant’s background through the container — a false low-contrast reading — for no responsive gain, since the only@containerconsumer, the stacking.cr-dl, lives in panels.) - Phase 3 — per-container density ✅: component pad/gap tokens route through
--pad/--gap;[data-density="compact"]on any container tightens its subtree via the cascade. Comfortable = default = pixel-neutral. - Phase 4 — sizing + touch ✅: shared
--control-h-*heights on the form family; a@media (pointer: coarse)44px tap floor (WCAG 2.5.5) that leaves the fine-pointer desktop layout untouched;.cr-taputility. - Phase 5 — container-query layout ✅:
.cr-grid-autoreflows by container;@container (max-width: 22rem)stacks the data-list inside a narrow panel.
Note on the “breaking” migration: rather than renaming
--text-*away (which would churn 100+ call sites), the rung values were changed to fluid clamps — so every existing consumer became responsive at once. The names stay; the behaviour broke (intended), which is why the visual baselines were regenerated.
Guard: tests/responsive.test.mjs. Deeper per-component data-density adoption
and more @container layouts can land incrementally on this foundation.
Rollout (once direction is agreed)
Section titled “Rollout (once direction is agreed)”- Land primitives + type group +
.cr-typocontainer primitive +.cr-tap(token- CSS), no component changes → pixel-neutral. (As shipped,
.cr-panelbecame the query container and no.cr-typoclass exists.)
- CSS), no component changes → pixel-neutral. (As shipped,
- Add semantic spacing/sizing + density remap; migrate 2–3 exemplar components (Panel, a form control, a data row) as proof.
- Roll the density/container model across components in gated batches (like the pt/dt rollout), each pixel-diffed.
- Document + a
/reference/responsive/page; add a container-query visual test.