Three rhythms
gap rides the library's shared size axis — sm, md, lg — so the space between a form's rows is the same decision as the space between a page's sections, taken once.
A column of blocks standing one chosen step apart, the step taken from the spacing scale.
@pacit/components/stackpct-stackNo APG pattern applies, and none is invented — the reasoning is under Accessibility ↓
import { Component } from '@angular/core';
import { PctStack } from '@pacit/components/stack';
/** Vertical rhythm from the spacing scale — nobody hand-rolls margins between siblings. */
@Component({
selector: 'demo-stack',
imports: [PctStack],
// A column shrink-wrapped by the stage is three words wide; the host gives the rhythm a
// readable width and the stage centres it (lesson-146).
styles:
':host { display: block; inline-size: min(100%, 22rem); } div { border: 1px solid var(--pct-border); border-radius: 8px; padding: 8px 12px; }',
template: `
<pct-stack gap="lg">
<div>First</div>
<div>Second</div>
<div>Third</div>
</pct-stack>
`,
})
export class StackDemo {}stack.demo.ts — the same file runs here and is highlighted under Code
One import per component, and the shortest possible use — the package itself is installed once, on the start page.
import { PctStack } from '@pacit/components/stack';| Exported | What it is |
|---|---|
PctStackcomponentpct-stack | Vertical rhythm: a flex column whose blocks stand one chosen step apart, instead of each block bringing its own margin and the space between two of them being their sum. The second layout primitive (0057): no ARIA, no parts, one input. |
<pct-stack gap="lg">
<div>First</div>
<div>Second</div>
</pct-stack>Each example is the component itself, not a screenshot. Open the code under any of them.
gap rides the library's shared size axis — sm, md, lg — so the space between a form's rows is the same decision as the space between a page's sections, taken once.
Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.
Vertical rhythm: a flex column whose blocks stand one chosen step apart, instead of each block bringing its own margin and the space between two of them being their sum. The second layout primitive (0057): no ARIA, no parts, one input.
| Name | Type | What it does |
|---|---|---|
gapdefault 'md' | PctSize | The step of the spacing scale between blocks. |
(click), (focus), the form's submit — are the platform's, and two-way models above carry the rest. The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.
| Attribute | Bound to | Note |
|---|---|---|
class | pct-stack | |
data-pct-gap | gap() |
Two ways in, both stable. No class names to depend on.
In a test, PctStackHarness from @pacit/components/testing reads a part by the name below and a state by its attribute — the harness gate holds the names to the package.
No parts of its own — the element you write is the whole of it.
Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.
| Token | Styles |
|---|---|
--pct-stack-gap{pct.space.5}16px | Gap between the stack's children. |
--pct-stack-gap-lg{pct.space.6}24px | Gap between the stack's children at size lg. |
--pct-stack-gap-sm{pct.space.3}8px | Gap between the stack's children at size sm. |
A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.
[data-theme='brand'] {
--pct-stack-gap: 32px;
}Pattern: none — layout is presentational and this host carries no ARIA at all (0057). The unit suite pins the absence itself. Named in the class JSDoc.
| key | effect | test |
|---|
Empty by design: not interactive, nothing focusable added, no handler anywhere.
Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.
libs/components/stack/src/stack.tsstack.spec.ts › "carries no ARIA at all" plus layout.spec.ts › "none of the three carries a single ARIA attribute", three enginesapps/sandbox-e2e/src/a11y.spec.ts — /layout in SBX_ROUTESlayout.spec.ts measures 8, 16 and 24 px between real boxes AND the flush first edge, which pixels would only restateforced-colors: active — no state carried by colour alonenot applicable — stack.scss paints nothingprefers-reduced-motion — duration from a tokennot applicable — nothing moves--pct-control-height-*deliberately not — the gap axis reuses the scale's three words but measures distance, not height; the reasoning is the card's own paragraph above and 0057dir="rtl" screenshotstack.scss holds one flex-direction: column and three gaps — nothing directional exists to flip, and check-styles enforces the logical rule over the file anywayNG05xxapps/sandbox-e2e/src/hydration.spec.ts — /layout in SBX_ROUTESlibs/components/parts.snapshot.md — a component with no parts is itself the entrycontrast.policy.jsonlibs/tokens/tokens.snapshot.md — three --pct-stack-* names; no contrast entry, deliberately: no colour paintedPCT_TEXTSnot applicablelibs/components/size.snapshot.md — the ./stack row, on @angular/core alone — the PctSize import is a type, so even the shared axis costs no ./core at runtimedocs/acr/at/ — three readers, whole sandbox. As for the container and the grid: /layout produced no stop of its own in any of the three logs. A stack is a gap between children and a reader walks the children, which it did — every stop on that view is one of the scaffold controls, announced by its own groupapps/sandbox/src/app/views/layout/ (the sandbox view). The published site: /components/stack — prerendered, the demo's own source is the code tablibs/components/stack/src/stack.spec.ts — 4 cases; targeted Stryker run 2026-09-02: 1 mutant, 1 killed — 100.00 (the 'md' default swapped away dies on the default-gap case), and the day's shared full run wrote the same row into the snapshot: stack.ts 100.00 1(0) 0 0 0Read at build time from the tracked snapshots, not typed here.
The second layout primitive (0057): a flex column whose blocks stand one chosen step apart. The step comes from the spacing scale through the shared sm | md | lg axis, so rhythm is picked, not typed — and because it is a flex gap, it cannot collapse, spaces between blocks only, and leaves the first and last block flush with the host: three properties margins never manage together.
pct-cluster), refused in 0057 until something in the repository needs it twice — the extraction law of lesson-21.gap value; a consumer who truly needs 13px overrides --pct-stack-gap in a scope and owns the deviation.gap adds to them. Resetting other people's styles is a thing this library does not do anywhere, and starting here would be the surprise.