fix(kanban): keep the swimlane column-header row out of the flex shrink pool - #8430
Merged
Merged
Conversation
…nk pool With swimlanes on, the board's column titles stayed in the DOM at height 0 and painted nothing (objectui#7303). The header row is a flex item of the swimlane region, and `overflow-x-auto` makes it a scroll container — which zeroes a flex item's automatic minimum size, so it may legally shrink to 0. The lanes below stay `overflow: visible`, so their automatic minimum size clamps them at content height and they refuse to shrink; once the lanes overflowed the bounded board, the entire deficit landed on the one shrinkable item. `shrink-0` takes the row out of that pool. Measured in Chromium 1194 at 1600x1000 on the component's own rendered markup: header row 0 -> 24, header cell 0 -> 24, and `elementFromPoint` at a title's centre returning the lane-collapse button before and the title itself after. The pin asserts the style contract that decides the height, not the row's existence — the row was present and correctly placed on the broken build — and carries a rendered lane, a rendered card and the flat board's own headings as controls. happy-dom performs no layout, so a height assertion there could not fail; the docstring says so and records the out-of-band measurement instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-justin
marked this pull request as ready for review
September 7, 2026 23:22
os-justin
deleted the
claude/issue-7303-kanban-swimlane-header-height
branch
September 7, 2026 23:38
This was referenced Sep 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7303
What the card said, re-derived on my own base (
0203a29e9)Every number in the card still holds. The three-layer ancestor chain it measured is
literally these lines, and the tag difference it noticed is real:
0203a29e90 / 0 / 16elementFromPointat a title's centre returns the lane<button>isSelfOrChild: false,hit: BUTTON.w-full flex items-center…)<h3>on the plain board,<span>in the swimlane rowKanbanImpl.tsx:319vs:661grouping.fields[0].fieldis the only authorable routeObjectKanban.tsx:753derivesswimlaneField ?? grouping.fields[0].field; unchanged by #8313Root cause: a flexbox rule, not a paint bug
The header row is a flex item of the swimlane region (
flex-col, inside aheight-bounded
h-fullboard).overflow-x-automakes it a scroll container, and aflex item's automatic minimum size (
min-height: auto) applies only while its overflowis
visible— so a scroll container may legally be shrunk to height 0. The lanes belowkeep
overflow: visible, so their automatic minimum size clamps them at content heightand they refuse to shrink. Once the lanes overflow the board, the entire deficit lands
on the one shrinkable item. Measured computed styles on the broken build:
The fix is
shrink-0on that row — one class, no layout decisions taken.board. The same fixture with 45 cards instead of 150 renders the row at 24px on the
broken build. So in the field this presents as "sometimes the column labels are missing",
not as a flat breakage of swimlanes.
Measured, in a real browser, on this component's own markup
Chromium 1194 @ 1600×1000, the real rendered DOM of
KanbanRenderer+ Tailwind-generatedCSS, lanes overflowing a 1000px board:
elementFromPointat title centre<button><span>itselfThe pin — and what it honestly is
in the right place, holding all five titles, at height 0. And vitest runs in happy-dom,
which performs no layout: a height assertion there could not fail in either world. So
packages/plugin-kanban/src/__tests__/swimlaneColumnHeaderRow-7303.test.tsxasserts thestyle contract that decides the height, stated as an invariant rather than one blessed
spelling: the row must not be both a scroll container and shrinkable (
shrink-0,flex-shrink-0, an inlineflexShrink, or amin-h-*floor all satisfy it;overflow-*-hiddenand
-clipcount as scroll containers, because they zero the auto minimum size too).The docstring says all of this in the file, including the browser numbers above, so nobody
later reads it as a height measurement.
Ablation — two mutations, from the committed implementation, each restored by state:
a834767db17a35db568de9fa73f2362703e0ae05shrink-0(the read site)b67eed7eb85293eaf1491094f1383427e19d3013AssertionError: the swimlane column-header row is a scroll container AND shrinkable … class was: "flex gap-3 sm:gap-4 pl-36 sm:pl-44 overflow-x-auto"81d4773821559ac1fa4ee0f3fa370b90ef5cab59AssertionError: the swimlane header row should carry the column title Open; row text was "▶ann(1)Alpha deal@ann"Restore verified by state after each (
git hash-objectback to the HEAD blob andgit diff HEADempty), never by an exit code.Would something strictly worse than the bug pass? No — that is what the second mutation
above tests. Deleting the swimlane header row, or the swimlane layout itself, reddens the
controls: a rendered lane (
['▶ann(1)', '▶bob(1)']), a rendered card inside a lane cell,and the titles read through
queryByTextintoexpect(value, message). A separateNON-REGRESSION case pins the flat board's own
<h3 id="kanban-col-open">, so "makeswimlanes render nothing" is not a way through either. The viewport is pinned explicitly
(1600×1000) even though this markup is width-independent — the file says which branch it
describes and that anything width-dependent added later must re-state it.
On the triage seat's boundary #1 (reuse
:319instead of fixing:659)Confirmed the two header implementations must stay separate, and the reason is now
written at the call site: the swimlane layout has no column components at all — every lane
paints its own row of plain column cells — so the titles have to be drawn once, above every
lane. Reusing
KanbanColumnView's<h3>would mean repeating the labels per lane, which isa product ruling the card does not settle (its Expected says "above the lanes"), so I did
not take it. The comment also pins the shared
pl-36 sm:pl-44indent, and the test assertsthe header row and the lane content rows carry the same indent tokens.
search_issuesreturnedAPI rate limit already exceeded for user ID 323634890, so per mydispatch I did not file unsearched. All three are in my report for the PM to file:
overflow-hidden(
scrollHeight 2104vsclientHeight 1000) and no ancestor scrolls(
document.documentElementis not scrollable) — with 3 lanes at 1600×1000, lanes 2 and 3cannot be reached at all. Fixing it means choosing where the swimlane board scrolls, which
is a product ruling.
scroll containers: driving one lane to
scrollLeft 298leaves the header at0, puttingthe "Open" title at
x=200while the Open cell sits atx=-97. So past this fix, ascrolled swimlane board shows labels over the wrong columns. Syncing them means deciding
whether all lanes scroll together — again a product ruling.
KanbanBoardInnerderivescolumnInlineStylefrom the board's own measured width and the flat path applies it, butthe swimlane header cells and lane cells keep the hard-coded
w-[85vw] sm:w-80— so anembedded/panelled swimlane board still sizes columns to 85% of the viewport, the exact
failure that sizing was introduced to remove.
Verification
pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-kanban^...' buildpnpm exec vitest run packages/plugin-kanban/(repo root, positional path)Test Files 31 passed (31)·Tests 206 passed (206)pnpm --filter @object-ui/plugin-kanban type-check(tsc --noEmit && tsc -p tsconfig.test.json)--listFileshit, with a control)pnpm --filter @object-ui/plugin-kanban lint(eslint .)no-explicit-anypattern in the new test, and--max-warningsis deliberately unset inlint.ymlnode scripts/check-changeset-presence.mjs✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/7303-kanban-swimlane-column-header-row.md.node scripts/check-changeset-no-major.mjs✅ No changeset declares a 'major' bump.(declaredpatch)node scripts/check-governed-queue-guard.mjs --test <the 3 paths>✅ NOT GOVERNED — 3 path(s) checked against 5 governed surface(s); none matched.(control:AGENTS.mdreads governed)Proposal — the standing gap this card exposes
Nothing in CI can measure a rendered height: unit tests run in happy-dom (no layout) and the
e2e suite needs a production build of the console. The measurement above is reproducible but
lives outside the repo — real component DOM + Tailwind-compiled CSS + Chromium, roughly 60
lines.
packages/plugin-grid/demo/vite.live.config.tsis the precedent for a standaloneharness. Worth a card if the maintainers want layout regressions of this class caught rather
than argued; I did not file one, for the rate-limit reason above.
Left as a draft for the PM's contract review.
🤖 Generated with Claude Code
https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Generated by Claude Code