Skip to content

Review 3938

Cindy Zhang edited this page Aug 26, 2026 · 7 revisions

Review — PR #3938, feat(tablist): add isFullBleed to bleed a tab bar to container edges

LOOP VERSION: 1.4.0 AUDIT RUBRIC: 1.12 AUTHOR CAN PROCEED: yes — see round 3b

Author: jiunshinn · branch feat/2622-tablist-bleed · issue #2622

Read round 3b. Rounds 2a and 3 asked the author to keep the template's negative margins; round 3a corrected that criterion and proved the LayoutHeader arm. Cindy has now ratified the ownership split: TabList owns inline full bleed; LayoutHeader owns the block-end dock through paddingBlockEnd={0}. No design choice remains. The current ask is two implementation changes.

Round Head Date Outcome
1 b765a250fe1 2026-08-24 Changes requested — block-axis gating
2 24893c04872 2026-08-26 Changes requested — edge stops do not compensate
2a 24893c04872 2026-08-26 Correction — asked for the wrong shape in round 2; see below
3 8a18544aaed 2026-08-26 Changes requested — geometry guard is not CI-enforced superseded by 3a
3a 8a18544aaed 2026-08-26 Correction — round 3 graded against round 2a's obsolete criterion; see below
3b 8a18544aaed 2026-08-26 Ruling ratified — ownership settled; two implementation changes remain

Round 2

Superseded in part by round 3a: item 4 of this round's ask ("leave marginBottom: -16 / marginTop: 12 … exactly as they are now") is withdrawn.

What changed since round 1

Round 1 blocked on the block-axis gating: :first-child/:last-child matched the strip's own parent rather than the padded container, so the rule meant to prevent the strip being pulled up was what pulled it up 16px into the metadata row.

The author fixed it in 24893c04872 and fixed it correctly. isFullBleed is now inline-only, the docblock says so and says why, and the template keeps its block-axis dock with an honest comment explaining that no component prop can reach the header's padding edge from inside StackItem > HStack. That blocker is resolved and is not re-raised.

What remains

The strip's box bleeds, but the first and last stops do not pad back in, so their labels land 4px short of the container's content inset. Everything else on the row — the metadata line above, the section heading below — sits on that inset, so the tab labels are the only thing off it.

This is the second half of the prop's own promise. The docblock says the strip extends "out to the container's content edges"; the box does, the content does not.

Measurement

Real detail-page template rendered by apps/template-viewer (which globs packages/cli/assets/templates/** and renders page.tsx itself, with @astryxdesign/core aliased to source). Not a repro story.

arm build first tab label vs content inset ghost centre / tab centre
A — main today 58f95421362 0.00px 133.98 / 133.48 (Δ 0.50)
B — this branch edbc7d8c8ce −4.00px 133.98 / 133.48 (Δ 0.50)
D — + block-end rule as well b9e95feb00b 0.00px 131.98 / 139.48 (Δ −7.50)
D2 — + edge-stop compensation only b747a345b03 0.00px 133.98 / 133.48 (Δ 0.50)

B vs D2

D2 is the shape to ask for. D is recorded because it is the trap: adding a block-end cancel as well double-docks against the template's own marginBottom: -16, pushing the strip 9px below the divider and the tab centre 6px down. The inline-only version leaves the block axis bit-identical to main.

An earlier arm that deleted the whole tabsRow object and flipped vAlign="center" to "end" lifted the header's ghost panel-toggle 14px out of the row (ghost centre 119.98 vs tab centre 133.48). That regression is attributable to the block-axis deletion, not to isFullBleed; it is the reason the template's block margins should stay until a LayoutHeader docking slot exists.

Receipts

All four frames carry <shot>.sensors.json. Identical on every sensor except Build:

route=/packages/cli/assets/templates/pages/detail-page
theme=neutral · mode=light dark · dir=ltr · 1440x1000@3
media: forcedColors=false reducedMotion=false coarsePointer=false hover=true
targets=1 · state={tabs:4, active:"Details", ghost:"present"}
fonts=loaded · runningAnimations=0 · pageErrors=none

Builds: A 58f95421362 · B edbc7d8c8ce · D b9e95feb00b · D2 b747a345b03. Surface luminance reads unknown on all four — the header paints no background, so expected.surfaceClass was omitted rather than loosened.

Frames and raw receipts: assets/pr-3938/armd/.

Arm isolation. A receipt proves a frame is of the surface and conditions it claims; it does not prove the build is the change it claims. Arms D and D2 were hand-built, so the exact diffs are recorded beside the frames: A-to-B.diff, B-to-D2.diff, D2-to-D.diff.

B→D2 is functionally three things and nothing else: TabList publishes --_tab-edge-inset-{start,end} from the container padding, clamped max(…, --spacing-2); Tab and TabMenu consume them at :first-child / :last-child. The rest of that diff is comment churn from the two arms having been cut from different bases — inert, and inspected to confirm it.

D2→D adds exactly one rule, fullBleedBlockEnd. That is the whole cost of the 6px push in arm D. Round-1 evidence set (33 frames, isolated repros and full pages): assets/pr-3938/.

RTL measured identically to LTR in every arm.

Tests

The three added tests assert className contains the string fullBleed. StyleX emits that class from the object's key, so all three pass against an empty style object — they cannot fail for the reason they exist. The third is weaker still: it asserts only that the class count grew.

This is inherited, not invented: Divider's isFullBleed test says so in its own comment (// we verify component renders without error). Worth not copying forward.

jsdom has no layout, so the geometry belongs in a browser test: set --container-padding-inline-start on a wrapper, assert the strip's box escapes it and the first stop's label lands back on it.

Also noted

  • Dropping marginInline: -12 moves the ghost panel-toggle inline as well: ghost_box_end_inset −12 → 0. It now lands on the content inset like everything else. An improvement, but a visible change to that button, and it should be called out rather than discovered.
  • The template moved on main: packages/cli/templates/packages/cli/assets/templates/. The branch is ~904 commits behind, so the rebase will not apply cleanly here.
  • Unrelated formatting churn in the template diff (Icon, MetadataListItem, Collapsible re-wrapped). Harmless, but it is noise in a review.
  • Not this PR's: TabList renders no tablist/tab roles at all — plain nav plus buttons. Separate issue.

Ask

  1. Keep the prop and the name. Both are right.
  2. Add the edge-stop compensation, inline only — the first and last stops pad back in by the amount the box bled out, clamped so a stop never insets less than its normal padding. This is three files, not one: TabList publishes the amount, and both Tab and TabMenu consume it. In this template the last stop is the TabMenu ("More ⌄"), so the end edge does not move without it.
  3. Do not add a block-end rule. Arm D shows what it costs.
  4. Leave marginBottom: -16 / marginTop: 12 and vAlign="center" in the template exactly as they are now.
  5. Replace the three class-name tests with browser geometry assertions.
  6. Rebase.

Round 2a — correction

Itself corrected by round 3a: arm E stands, but item 4 of this round's revised ask — "Template keeps marginBottom: -16, marginTop: 12, vAlign="center"" — was wrong and is withdrawn.

Round 2 asked for the wrong shape. The diagnosis holds (the branch is a 4px regression against main); the remedy does not. Arm E supersedes D2 as the ask.

Arm E

One rule, one file. TabList keeps the negative margins on the nav and pads the strip back in by whatever the bleed exceeds a stop's own inline padding:

const STOP_INLINE_PADDING = spacingVars['--spacing-3'];
const EDGE_PAD_START = `max(var(--container-padding-inline-start, 0px) - ${STOP_INLINE_PADDING}, 0px)`;
const EDGE_PAD_END   = `max(var(--container-padding-inline-end,   0px) - ${STOP_INLINE_PADDING}, 0px)`;

Tab and TabMenu are untouched. No private var protocol, no child selectors. Built as 0584db591b2 on vis-3938-e; diffs main-to-E-core.diff, D2-to-E.diff.

Why D2 was wrong

Measured across 13 rigs per arm (matrix-D2.json, matrix-E.json):

  1. D2's TabMenu rule never fires. TabMenu returns a fragment — the trigger <button> plus the popover's <template> and a zero-size <div>, all strip siblings — so the trigger is never :last-child. In layout="fill" D2's chevron lands 4px outside the inset: the exact regression this PR exists to fix, at the other edge. E lands 0.00 and needs no TabMenu change.
  2. Widens stops that are not at an edge. :last-child fires on the last Tab even in hug, where it is nowhere near the end — a 4px-wider hover pill on one tab for no reason. Single-tab strips get both rules and grow 8px.
  3. Regresses padding={0}. The property is published as 0px, so max(0px, --spacing-2) resolves to 8px and the first stop's padding shrinks 12 → 8, in a container with no padding to bleed. E is a no-op there.

At rest on this template E and D2 are pixel-identical (0 differing pixels); the only on-screen difference is a 13px band at the first hover pill, where E matches main's −12 and D2 sits at −16.

What E costs

  • Container padding below 12px (padding={2} → 8px): E's label lands 4px inside the inset; D2 is exact. Structural — the strip can add padding, never remove it. Main has the same flaw today (its hardcoded -12 over-bleeds an 8px container), so E is no worse than the status quo, and D2 is strictly better on this one axis.
  • A restated constant. E duplicates --spacing-3 in TabList to subtract the stop's own padding; the import would be circular. If a stop's inline padding changes, edge alignment drifts silently. Wants a unit test.
  • layout="fill" distributable area is 8px narrower (~1.3px per stop). Not visible.

Everything else measured the same in both: RTL, unpadded containers, scrolling strips, overflow="none", and the entire block axis.

Full write-up: arm-E-report.md.

Revised ask

  1. Keep the prop and the name.
  2. Add the strip-padding compensation (arm E). One fileTab and TabMenu stay untouched.
  3. No block-end rule.
  4. Template keeps marginBottom: -16, marginTop: 12, vAlign="center".
  5. Browser geometry tests, not class-name assertions. Include a stop-padding coupling test.
  6. Rebase.

Round 3

Withdrawn in part by round 3a. This round graded the head against round 2a's criterion, which ruling 4 had already superseded. Its finding about the unenforced geometry assertion holds; its judgement that "the implementation and intended layout are correct" does not, and neither does its AUTHOR CAN PROCEED reasoning.

LOOP VERSION: 1.4.0 AUDIT RUBRIC: 1.12 HEAD REVIEWED: 8a18544aaed20fe7760f16fb96eda7e87b6ffcf9 AUTHOR CAN PROCEED: yes

Reconciliation

Every code/layout finding from rounds 1, 2, and 2a is resolved at this head. The block-axis cancel is gone; arm E's one-file strip compensation is present; Tab and TabMenu remain untouched; the template removes only its inline margin and keeps the measured block dock. This round extends 2a with one finding about whether the replacement browser assertion is actually enforced.

Problem

A builder placing TabList at the bottom of a padded header otherwise needs a fixed negative margin to make the strip meet the header edge. That workaround does not follow container padding and drags the adjacent panel-toggle button outside the content inset.

Solution — 2 decisions, about 18 runtime lines

  1. isFullBleed expands TabList through the logical container padding and pads the strip back only by the amount a tab stop does not already provide.
  2. detail-page adopts the prop and removes marginInline; its measured marginTop, marginBottom, and vAlign="center" block dock stay intact.

Architecture

TabList owns the opt-in inline behavior; Layout publishes the padding values. The implementation uses the existing logical container vars and spacing token, with no child selector or per-stop protocol.

The ruling's Toolbar alternative was also driven on the real template and is not a smaller replacement: without isFullBleed it shrank the strip 968→316px, left the first label 4px inside, put the ghost action 8px outside, and left the rail 10px above the header edge; adding the prop over-pulled the label 8px. Current Toolbar would need new slot-fill and docking behavior.

Impact

Existing callers do not move because the prop defaults false. On detail-page, the label remains aligned, the strip reaches the header box edge, the panel toggle returns from 12px outside to the content inset, and the block axis is unchanged.

The accepted arm-E boundary remains: 16/24px padding is exact; at 8px the normal 12px tab-stop inset leaves content 4px inside; 0px and unset are matching no-ops.

API

<TabList value={activeTab} onChange={setActiveTab} isFullBleed>
  <Tab value="details" label="Details" />
  <TabMenu label="More" options={moreTabs} />
</TabList>
change public? class documented verdict
+ TabList.isFullBleed?: boolean = false core subpath + root barrel existing full-bleed family; ruling 4 settles TabList JSDoc + typed docs + usage accepted

No new theme targets, vars, or tokens.

Breaking

  • API — no; additive and default false.
  • Visual — existing TabList callers do not move. The template intentionally moves its panel toggle to the content inset and extends the strip 4px to the header box edge.
  • Theme — no; existing targets and overrides still resolve.
  • Behaviour — no interaction/state/default change.

Performance and accessibility

No effect, listener, observer, dependency, or O(N) work is added. Exact-head unit, build, a11y, RTL, and visual jobs are green; a fresh Chromium matrix also matched logical LTR/RTL geometry.

Visual evidence

Manual capture was required because pixels intentionally change. Fresh sensor-backed Chromium frames on the real template showed only the intended panel-toggle shift; labels, underline, type, spacing, and block alignment were visually coherent. Geometry measured:

case result
real template, current main → exact head label 0→0px vs inset; nav box 4→0px from edge; ghost −12→0px; tab/ghost centres and header height unchanged
exact head 16/24px both logical edges exact
exact head 8px both logical edges +4px inside, accepted clamp
exact head 0px / unset matching no-ops with normal 12px stop inset
RTL, hug, fill + trailing TabMenu match LTR and pass

Tests

The old class-name assertions are gone and FullBleedGeometry.play contains real 16px hug/fill/TabMenu geometry assertions. But no CI test target enforces that play result: Vitest collects only *.test.*, and the visual/a11y loaders wait for rendered DOM without observing and failing on play outcomes. A broken geometry assertion can therefore leave required checks green.

Judgement

Request changes. The implementation and intended layout are correct; the remaining blocker is regression protection, not another API or layout redesign.

  1. Wire the existing assertions into a CI-enforced browser check.
  2. Include the accepted far side: 8px preserves the 12px stop inset; 0px/unset is a no-op.

AUTHOR CAN PROCEED: yes — the acceptance criterion is fully specified and no human-owned API/design choice remains.

The review, as drafted

Thanks — the one-file strip compensation is the right shape, and this head now holds the intended geometry for hug/fill, RTL, and a trailing TabMenu.

One blocker remains: FullBleedGeometry.play (apps/storybook/stories/TabList.stories.tsx:103) is not an enforced test. Vitest never collects stories, and the visual/a11y jobs load this story without waiting for or failing on its play result, so a broken geometry assertion can stay green.

Could you wire these assertions into a CI-enforced browser check and include the clamp's far side (8px preserves the 12px stop inset; 0/unset is a no-op)? If you'd rather talk it through with someone, we're in Discord.

[Reviewed by Robohands]

What changed before posting

Not posted — this was a read-only re-review; Cindy has not judged the draft.


Round 3a — correction

Superseded by round 3b. The held API question below is resolved: Cindy accepted LayoutHeader.paddingBlockEnd={0}.

LOOP VERSION: 1.4.0 AUDIT RUBRIC: 1.12 HEAD REVIEWED: 8a18544aaed20fe7760f16fb96eda7e87b6ffcf9 (same head as round 3) AUTHOR CAN PROCEED: yes

Round 3 graded this head against the wrong criterion, and round 2a is where the wrong criterion came from. Round 2a told the author to keep marginBottom: -16, marginTop: 12 and vAlign="center" and to remove only marginInline. That contradicted ruling 4, which had already settled the acceptance test in as many words:

The acceptance test is the template. detail-page's tabsRow must lose ALL its hand-written CSS — not shrink, disappear. … Everything is computed from the container system vars. No hardcoded amount anywhere — not in the prop, not in the template.

So round 3's verdict — "the implementation and intended layout are correct", with the unenforced play function as the only blocker — is withdrawn. The block it found is real and survives as the second finding below. The criterion it passed the layout against does not.

What is actually wrong at this head

tabsRow still exists (detail-page/page.tsx:56) and is still applied (:263). And the constant it carries is not merely untidy — it is correct at exactly one header padding:

header padding head 8a18544, underline → divider
{4} (16px) −1.00px — docked
{6} (24px) +7.00px — 8px short of the divider

marginBottom: -16 cancels a 16px padding by coincidence of value. The container already publishes that number as --container-padding-block-end.

Who should own the block-end dock

Driven, not argued — six arms on the real detail-page route through apps/template-viewer, one probe, one page. "5/5" means all five targets the criterion names: inline bleed + label compensation from container vars; underline docked on the header divider; ghost panel toggle centred on the tab row; prop-false defaults unchanged; RTL and padding variants.

arm template CSS new API @ padding={4} @ padding={6}
head 8a18544 marginTop:12; marginBottom:-16 5/5 underline +7.00
Z — delete tabsRow, nothing else none underline +15.00
A — LayoutHeader paddingBlockEnd (chosen) none 1 optional prop 5/5 5/5
A2 — LayoutHeader bottom docking slot none new slot + its own gap rule 5/5 not driven
B — TabList owns block docking none 1 prop, wrong owner underline +5.00, ghost −8.00
S — existing Section bleed seam none none 5/5 label −8.00, ghost −8.00

(a) beats (b) on measurement, not on taste. LayoutHeader publishes the padding and paints the divider; TabList can see neither through StackItem > HStack > VStack. Arm B's negative block-end margin shrinks only the nav's outer box inside the HStack, so the row stops sizing to the strip: the header keeps 11px of its padding and vAlign="center" re-centres the ghost 8px above the tab row.

The existing seam was tried before any API was invented. Section already cancels container padding on all four edges, so arm S wraps the row in <Section variant="transparent" padding={0} paddingInline={4}> and adds nothing to the public surface. It passes at padding={4} and puts both the label and the ghost 8px outside the content inset at padding={6}paddingInline={4} is the same restated 16 in prop clothing. It trades a magic number for a magic prop.

A2 works and costs more. The literal docking slot hits 5/5, but adds a slot concept, needs its own block-start gap rule (nothing supplies one outside children), and restructures the template's JSX. Arm A reaches the same geometry by naming one edge in a vocabulary that already exists. A2 was driven at padding={4} only; its dock reads the same container var, so it should generalise, but that is stated rather than measured.

Arm A, whole

armA-layoutheader-dock.diff — +22/−16 across two files.

// LayoutHeader.tsx — per-edge override last, so it wins on its own edge only
paddingBlockEnd != null && paddingBlockEndStyles[paddingBlockEnd],
paddingBlockEnd != null && containerPaddingBlockEndVarStyles[paddingBlockEnd],
// detail-page/page.tsx — the whole tabsRow object and style={tabsRow} are gone
<LayoutHeader hasDivider padding={4} paddingBlockEnd={0}><HStack vAlign="center">

Section already publishes all six per-edge padding overrides (Section.tsx:191-226), backed by padding.stylex.ts helpers LayoutHeader already imports. This is that class's second member, not a new concept, and the name is Section's — so there is no naming decision here. The container var moves with the padding so bleed children compensate against the padding actually applied, which is the rule Section.tsx:335 states for the same reason.

The other arms are banked beside it: armA2-layoutheader-slot.diff · armS-section-seam.diff · matrix-*.json.

Frames

Magenta = header content inset, red = header divider, blue = the two vertical centres. One clip per composition, guides drawn from live geometry.

The pair, at the template's own padding={4} — every measured target identical:

before and after

The boundary, at padding={6} — this is the frame the block rests on. The head's underline sits visibly above the red divider guide; arm A stays docked:

boundary at padding 6

frame build (a) first label vs inset (b) underline → divider (c) ghost vs tab-row centre
head-8a18544-header.png 8a18544 0.00px −1.00px 0.00px
armA-dock-header.png 4fe97cd 0.00px −1.00px 0.00px
head-pad6-header.png 217409e 0.00px +7.00px 0.00px
boundary-pad6-header.png b5c822e 0.00px −1.00px 0.00px

All four carry <shot>.sensors.json and match on every sensor except Build. The build sensor was proved live rather than assumed: capturing the head frame from the arm checkout failed closed with expected 8a18544…, observed 4fe97cd….

Sensor tests

tabrow-3938-r16c-tests.cjs. Expectations are authored from the contract before observation (expectedLabelDelta(pad) = max(12 − pad, 0)), never loosened to match output.

build result
arm A 78/78 pass
head 8a18544 78/78 pass — geometry parity; the head's failure is the retained CSS, not the pixels
Z, delete-only 73/78 — the 5 failures are exactly the dock, at every padding and both directions

Axes: five targets × real template at 0/8/16/24 × LTR/RTL; the story fixture's hug and fill at 0/8/16/24 × LTR/RTL with a real fragment-rendering TabMenu as the fill strip's last stop; and prop-absent defaults. On arm A the repo's own targets are green too: vitest --project ui Layout + TabList + Section 195/195, tsc --noEmit clean, eslint/prettier and all ten check:* scripts.

The second finding — round 3's block, re-verified

FullBleedGeometry.play (apps/storybook/stories/TabList.stories.tsx:103) is observed by nothing that can fail:

  • vitest.config.ts collects *.test.{ts,tsx,mjs} in both projects; no include pattern reaches *.stories.tsx.
  • There is no @storybook/test-runner in the repo.
  • The visual gate loads each story and waits only for #storybook-root > * and document.fonts.ready (.github/scripts/visual-gate/gate.mjs:381-382). It never awaits play, never inspects a story error, and its catch swallows a story that will not render.

Every prior finding, reconciled

round finding state at 8a18544
1 block-axis :first-child/:last-child gating pulled the strip 16px up fixed — deleted
2 edge stops do not compensate; label 4px short fixed — label 0.00
2 three tests assert only that className contains fullBleed fixed — replaced by real geometry assertions
2 D2's TabMenu rule can never fire moot — no child selectors at this head
2 dropping marginInline moves the ghost to the inset; call it out fixed — the PR body states it
2 branch ~904 commits behind; template path moved open — still needs a rebase
2 unrelated formatting churn in the template diff open — still noise
2 TabList renders no tablist/tab roles not this PR — separate issue
2a keep marginBottom:-16, marginTop:12, vAlign="center" superseded — see above
2a an arm deleting tabsRow "lifted the ghost 14px out of the row" corrected — not reproducible. Deleting tabsRow alone leaves the ghost-vs-tab-row centre at 0.00px (matrix-armZ-delete-only.json); the only thing it breaks is the dock (+15.00px). The 14px belonged to that arm's vAlign="center"→"end" flip, which this round does not make. The real ghost shift is −8.00px and belongs to arm B
3 FullBleedGeometry.play is not CI-enforced open — re-verified above
3 "the implementation and intended layout are correct" withdrawn

What this costs

With the hand-tuned marginTop: 12 gone, the tab row sits on the header stack's own gap={3} (12px, not 24px), so the header is 140.98px tall instead of 152.98px. That is the price of "no replacement magic numbers" — the 24px was never a token, and re-spelling it would re-fail the acceptance. Recorded rather than restored.

The accepted arm-E clamp is unchanged: 16/24px exact, 8px leaves the label 4px inside, 0px/unset are matching no-ops.

Ask

  1. Keep isFullBleed, its name and its implementation. The inline half is right.
  2. Delete the whole tabsRow object and style={tabsRow}. No replacement constant.
  3. Add paddingBlockEnd?: SpacingStep to LayoutHeader in Section's existing spelling, and pass paddingBlockEnd={0} on the header — held for ruling 12; this is new public surface on a second component, so the shape is Cindy's before it is built. Recommendation and the driven arm are on that entry.
  4. If the prop lands, give it a LayoutHeader.doc.mjs row. (padding is undocumented there already — that half is pre-existing debt, not this PR's.)
  5. Put the geometry assertions — now including the dock — in a target CI fails on, and keep the clamp's far side (8px preserves the 12px stop inset; 0/unset is a no-op). There is a harness rather than one to invent: pr-visual already walks every story in a real Chromium via gate.mjs, and pr-a11y already drives @axe-core/playwright. Which one is the author's call.
  6. Update the changeset: its last sentence still says the template "keeps only its hand-tuned block-dock margins in tabsRow."
  7. Rebase, and drop the unrelated Icon/MetadataListItem/Collapsible re-wrapping from the template diff.

Judgement

Request changes, with one ruling held alongside. Two blocks, each forcing the verdict independently: the template still carries hand-written tab-row CSS whose constant is padding-specific, and the replacement geometry guard is enforced by no CI target. A BLOCKS outranks a [Needs human judgement] in the brief's order, so the GitHub state is CHANGES_REQUESTED whichever way the ruling goes.

AUTHOR CAN PROCEED: partly, and the split is the honest part.

ask proceedable now?
Delete tabsRow and style={tabsRow} no — deletion alone leaves the underline 15.00px off the divider; it needs whatever replaces the dock, and that is the ruling
Add LayoutHeader.paddingBlockEnd no — new public surface on a second component; ruling 12, recommendation and driven arm attached
Put the geometry assertions in an enforced target yes — no decision left; two existing Chromium-in-CI harnesses named
Update the changeset's last sentence yes
Rebase; drop the unrelated formatting churn yes

Round 3 said "no human-owned API/design choice remains." That was the other half of its error: the remedy it implied needs one.

The review, as drafted

Thanks — the inline half is right, and the strip compensation holds for hug/fill, RTL and a trailing TabMenu.

An apology first: round 2a asked you to keep marginTop: 12 and marginBottom: -16. That was our mistake — the ruling behind #2622 says the template must need no hand-written CSS at all. You built what we asked for.

-16 only cancels a 16px padding by coincidence. At padding={6} the underline lands 7px off the divider:

head vs proposal at header padding 6

The header owns that edge, and Section already spells it. I drove LayoutHeader paddingBlockEndtabsRow disappears and the geometry holds at both paddings. It's new public API though, so Cindy picks the shape before you build it. I'm asking her now.

Separately: FullBleedGeometry.play (TabList.stories.tsx:103) is enforced by nothing — Vitest never collects stories and the visual job never awaits play.

[Reviewed by Robohands]

What changed before posting

Not posted — read-only re-review, no PR action taken. Cindy has not judged the draft.

One independent critic pass, blind to the code, graded the first draft FAIL and this record answers all of it: AUTHOR CAN PROCEED: yes was not earned (the remedy adds public surface on a second component — now ruling 12, with the approval split per ask); a block forced by a +7px pixel claim carried no frame (now head-pad6-header.png and the composed boundary above); "dock at any padding" was four driven values (now "at both paddings"); "every alternative was driven" contradicted the record's own table (the verdict now names what was not driven); "Second, smaller:" demoted a finding marked BLOCKS; the comment never told jiunshinn the loop had asked for the opposite two rounds ago (it now opens with the retraction); and the rebase and the formatting churn were listed open and then dropped from the ask (both are back).


Round 3b — ruling ratified

LOOP VERSION: 1.4.0

AUDIT RUBRIC: 1.12

HEAD REVIEWED: 8a18544aaed20fe7760f16fb96eda7e87b6ffcf9

AUTHOR CAN PROCEED: yes

Cindy ratified ruling 12: TabList owns inline full bleed; LayoutHeader owns the block-end dock. The existing isFullBleed implementation is accepted as an improvement over current behavior. The only API addition is LayoutHeader.paddingBlockEnd?: SpacingStep, using the per-edge family Section already publishes. There is no remaining choice about TabList, a docking slot, or whether to retain template CSS.

Remaining work

  1. Delete the whole tabsRow object and style={tabsRow}; add the LayoutHeader per-edge prop and pass paddingBlockEnd={0}. Arm A is the accepted implementation: 78/78 sensor assertions, 195/195 targeted tests, typecheck and all checks green.
  2. Put the geometry assertion in required Chromium CI. FullBleedGeometry.play is still unobserved: Vitest does not collect stories and the visual gate does not await play.

The redline remains the decisive visual evidence. At padding={6}, current head is +7.00px from the divider; the LayoutHeader arm is −1.00px (docked).

Redline of current head and LayoutHeader arm

Request changes. Both remaining items are proceedable; no human judgement is held.

Exact final public review — not posted

Thanks — Cindy accepted the ownership split: TabList owns inline full bleed, and LayoutHeader owns the block-end dock. isFullBleed is an improvement and needs no further design change.

Two changes remain:

  1. Delete tabsRow and style={tabsRow}; add LayoutHeader.paddingBlockEnd?: SpacingStep in Section's existing per-edge family and use paddingBlockEnd={0}.

Redline of current head and LayoutHeader arm

At padding={6}, current head leaves the underline +7.00px above the divider; the LayoutHeader arm is docked at −1.00px.

  1. Move FullBleedGeometry.play into required Chromium CI. Vitest never collects stories and the visual job never awaits play, so the current guard cannot fail CI.

Sorry for the contradictory round-2a direction; you built exactly what we asked for. Full review and measurements.

[Reviewed by Robohands]

Clone this wiki locally