Repository navigation
Kanban swimlanes render the status column-header row at height 0 — the board loses its column labels #7303
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 5, 2026 分诊 —
domain:ui/priority:p2/pm:queue/bug锚定 (anchoring):
packages/plugin-kanban/src/KanbanImpl.tsx⇒domain:ui。纯本仓、无上游依赖、不需要新键 ⇒pm:queue。在
origin/maina472b07上定位了卡片测到的那三层祖先卡片给的是浏览器实测(DOM 坐标 +
elementFromPoint+ computed style + 截图),本席无法复跑;能做的是把它落到源码行。三层 class 逐字对上,就是这里:packages/plugin-kanban/src/KanbanImpl.tsx:659 <div className="flex gap-3 sm:gap-4 pl-36 sm:pl-44 overflow-x-auto"> packages/plugin-kanban/src/KanbanImpl.tsx:661 <div key={col.id} className="w-[85vw] sm:w-80 shrink-0 text-center"> packages/plugin-kanban/src/KanbanImpl.tsx:662 <span className=" text-xs sm:text-sm font-semibold tracking-wider text-primary/90 uppercase">{col.title}</span>与卡片测到的
height 0 / height 0 / height 16三层一一对应。⭐ 根因比卡片说的更具体:swimlane 路径另起了一行表头,而不是复用列自己的表头
卡片观察到"
<h3>in the plain board,<span>inside the swimlane header row,所以两条路径构建方式不同"。源码给出了为什么:packages/plugin-kanban/src/KanbanImpl.tsx:319 <h3 id={`kanban-col-${column.id}`} className="text-xs sm:text-[13px] font-semibold tracking-tight truncate text-foreground/85 uppercase">{column.title}</h3>:319的<h3>在列组件自己内部 —— 每一列渲染自己的表头,所以普通看板天然有标题。而 swimlane 路径在:659-662平行地造了第二套表头行(<span>,不同 class,不同id语义 ——:319那个id={kanban-col-…}很可能是别处aria-labelledby的目标,:659这套没有)。也就是说这不是"一个 flex 容器算错了高度"的孤立样式 bug,而是 一处重复实现:同一个概念(列标题)在同一个文件里有两份,其中一份从来没被真正验证过。这决定了修法(见下)。
顺带一条对齐线索:
:689的泳道行是pl-36 sm:pl-44,与:659表头行的缩进相同 —— 表头行本就是设计成与泳道左侧标签列对齐的,修高度时别把这个缩进一起动了。定级理由
priority:p2:- 静默降级,卡片这句判断我复述并同意:视图校验通过、泳道看着正常、缺失的那一行容易被读成设计选择而不是 bug。
- 后果不是装饰性的:看板的列就是它的语义。没有列标题,
Open和Done只能靠拖一张卡看它落在哪来分辨。 - 真实业务代价已实测:
objectstack-ai/duly#108要的就是按 source 分泳道(售前演示屏),泳道功能是好的、却被整个放弃了 —— 因为"没有列标题的看板比没有泳道的看板更糟"。那一行重新打开的代码就躺在src/views/task.view.ts的注释里等这张卡。
不上 p1:无数据损坏、无错误数据、无安全面;是"看不见"而非"看到错的"。
定型:Bug,不踩 manual floor
grouping是已声明、已支持的授权键(卡片指出这还是唯一可写的路:KanbanConfigSchema是 strict 的,拒绝kanban.swimlaneField,所以 relay 从swimlaneField || grouping.fields[0].field派生)。作者写了合法元数据,渲染器丢了一行 —— 恢复 declared = enforced ⇒ Bug。不加宽任何授权面。⛔ 给执行席的边界
- 优先考虑让 swimlane 路径复用
:319的列表头,而不是去修:659-662的高度。 只把height: 0修好,等于把那份重复实现坐实、留给下一次漂移(两套表头的 class、语义标签、aria关联已经不同了)。⛔ 如果确认结构上必须分开画(表头要脱离泳道浮在顶部),那就在代码里写明为什么必须有两份,并把aria关联补齐 —— 别让它继续是一份没人知道存在的副本。 - 加回归测试。 这个 bug 的性质是"DOM 在、高度为 0",所以断言"标题存在"抓不住它 —— 必须断言可见性/高度,或者断言 swimlane 与非 swimlane 两条路径产出同一套表头元素。⛔ 不要写一个
getByText('Open')就收工,那是本 bug 已经能通过的测试。 - 修好后回
duly#108,把src/views/task.view.ts里那行注释掉的重新打开 —— 那是这张卡的下游验收点。
⛔ 分诊席不认领、不派单、不写码。
Generated by Claude Code
Claim: session
session_01YBWFb5YgMU5dw8p2VKj16S· branchclaude/issue-7303-kanban-swimlane-header-heightPM dispatch (
domain:uiseat). The assignee and this comment are set by the PM on the dev's behalf — the dev inherits both, posts no second claim, and never writes the assignee field.⚠️ Re-derive on your own base —plugin-kanbanmoved today (objectui#8313 / PR #8328 declared four array/object-armed spec keys on the registration), so any line number in this card is stale.This one is visual, which is the trap: a pin that asserts "the header row exists in the DOM" passes on the broken build, because the row IS there — at height 0. The assertion has to be about the measured height or the applied class/style that collapses it, and it must redden for that specific cause. If the only honest instrument is a screenshot diff rather than a unit assertion, say so and propose, rather than shipping a pin that cannot fail.
Every negative needs a control that fires: "the column labels are absent" is trivially true on a board that rendered nothing, so pin a rendered swimlane and a rendered card in the same fixture.
Generated by Claude Code
os-dev-report
{ "issue": 7303, "status": "done", "branch": "claude/issue-7303-kanban-swimlane-header-height", "pr": "https://github.com/objectstack-ai/objectui/pull/8430", "premise_still_valid": true, "summary": "Re-derived on my own base 0203a29e9: every number in the card still holds (KanbanImpl.tsx:659/661/662 header row, :319 flat h3, ObjectKanban.tsx:753 grouping to swimlaneField derivation, unchanged by #8313). Root cause is a flexbox rule, not paint: the header row is a flex item of the swimlane region (flex-col in a height-bounded h-full board) and its 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 stay overflow:visible so their auto min size clamps them at content height and they refuse to shrink, and the entire deficit lands on the one shrinkable item. Fix is shrink-0 on that row plus a call-site comment recording why two header implementations must exist. TRIGGER, and it decides who saw this: the collapse needs the lanes to OVERFLOW the board. The same fixture with 45 cards instead of 150 renders the row at 24px on the BROKEN build, so in the field this reads as sometimes the labels are missing, not as a flat breakage.", "tests": "MEASURED IN A REAL BROWSER (Chromium 1194, the card's own build, at 1600x1000) on this component's own rendered DOM plus Tailwind-generated CSS, lanes overflowing a 1000px board. Before: headerRowH 0, headerCellH 0, titleH 16, elementFromPoint at a title's centre returning the lane-collapse BUTTON (isSelfOrChild false) — the card's 0/0/16 triple reproduced exactly. After: headerRowH 24, headerCellH 24, titleH 16, the title winning its own hit test. Computed styles on the broken build: headerRow flex-shrink 1, min-height auto, overflow-x auto, overflow-y auto, height 0.0; firstLane flex-shrink 1, min-height auto, overflow visible, height 674.5. || WHAT THE COMMITTED PIN MEASURES: not existence — the row was present, correctly placed, holding all five titles, at height 0 on the broken build. vitest runs in happy-dom which performs NO layout, so a height assertion there could not fail in either world; the docstring says so and records the browser numbers instead. The pin asserts the style contract that decides the height, as an INVARIANT rather than one spelling: the row must not be both a scroll container and shrinkable (shrink-0 / flex-shrink-0 / inline flexShrink / a min-h floor all satisfy it; overflow-*-hidden and -clip count as scroll containers because they zero the auto min size too). VIEWPORT PINNED: window.innerWidth/innerHeight set to 1600x1000, with the file stating that this markup is width-independent (sm: variants are decided by CSS, which happy-dom never applies) and that anything width-dependent added later must re-state it. || ABLATION, from the committed implementation, trap on EXIT INT TERM with absolute paths. HEAD blob a834767db17a35db568de9fa73f2362703e0ae05. (1) remove shrink-0 at the read site: worktree hash b67eed7eb85293eaf1491094f1383427e19d3013, anchor count 1 to 0 verified by grep, RED by name — 'x renders above the lanes and stays out of the flex shrink pool' / 'AssertionError: 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\"' / 'Tests 1 failed | 1 passed (2)'. (2) delete the header row entirely (an implementation strictly WORSE than the bug): worktree hash 81d4773821559ac1fa4ee0f3fa370b90ef5cab59, RED by name — 'AssertionError: the swimlane header row should carry the column title Open; row text was \"(lane)ann(1)Alpha deal@ann\"'. Restore after each verified BY STATE, never by exit code: git hash-object back to a834767db17a35db568de9fa73f2362703e0ae05 AND git diff HEAD empty. Controls that fire in the same fixture: lane buttons ['ann(1)','bob(1)'], a card rendered inside the lane cell 'Open - ann cards', titles read via queryByText into expect(value, message); plus a NON-REGRESSION case pinning the flat board's own h3#kanban-col-open, so deleting swimlanes is not a way through. || GATES: closure build pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-kanban^...' build exit 0. Package suite from repo root with a positional path, pnpm exec vitest run packages/plugin-kanban/ — 'Test Files 31 passed (31)', 'Tests 206 passed (206)'. type-check exit 0, and the pin is genuinely in the program: tsc -p tsconfig.test.json --listFiles hits the new file (with KanbanImpl.tsx as the control), so the package's second tsc pass really covers tests. eslint . package-scoped — 0 errors, 163 pre-existing warnings over 45 files, both changed files in the corpus; lint.yml deliberately sets no --max-warnings. check-changeset-presence: 'OK 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.' check-changeset-no-major: 'OK No changeset declares a major bump.' (declared patch). check-governed-queue-guard --test with the 3 real paths: 'NOT GOVERNED — 3 path(s) checked against 5 governed surface(s); none matched', with AGENTS.md as a lit control. Control-byte scan clean with a lit control (a 0x0b byte fires the same pattern; a tab does not, since 0x09 is out of that class).", "mcp_calls": "6 — issue get, issue get_comments, search_issues (rate-limited, failed), create_pull_request, pull_request_read, this comment. REST was probed first and is unavailable this session: https://api.github.com/repos/objectstack-ai/objectui/issues/7303 returned HTTP 403 'GitHub access is not enabled for this session', so every GitHub read and write above went over MCP. Declaring the channel switch.", "open_questions": [], "out_of_scope_findings": [ "NOT FILED — search_issues returned 'API rate limit already exceeded for user ID 323634890', and the dispatch says do not file unsearched and do not drop the finding. All three below are measured, not inferred, and need PM filing.", "(1) Swimlane lanes below the fold are unreachable. The swimlane region is overflow-hidden with scrollHeight 2104 vs clientHeight 1000, and document.documentElement is not scrollable — at 1600x1000 with 3 lanes, lanes 2 and 3 cannot be reached at all. Distinct defect class from this card (no vertical scroll vs a collapsed row). Not fixed here: choosing where a swimlane board scrolls is a product ruling, which my escalation fence reserves.", "(2) The column-header row and each lane content row scroll horizontally OUT OF SYNC — they are independent scroll containers. Measured: driving one lane to scrollLeft 298 leaves the header row at 0, putting the 'Open' title at x=200 while the Open cell sits at x=-97. Consequence worth PM attention: past this fix, a horizontally scrolled swimlane board shows labels over the WRONG columns, and at 1600px with 5 columns the row already scrolls (scrollWidth 1840 vs clientWidth 1552). Not fixed here: syncing them means deciding whether all lanes scroll together — a product ruling.", "(3) The swimlane path ignores the container-aware column sizing. KanbanBoardInner derives columnInlineStyle from the board's own measured width and the flat path applies it (KanbanColumnView drops the viewport-relative classes when it arrives), but the swimlane header cells and lane cells keep the hard-coded w-[85vw] sm:w-80 — so an embedded or panelled swimlane board still sizes columns to 85% of the VIEWPORT, the exact failure that sizing was introduced to remove.", "(4) PROPOSAL, not a defect: 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. My measurement is reproducible but lives outside the repo (real component DOM + Tailwind-compiled CSS + Chromium, ~60 lines); packages/plugin-grid/demo/vite.live.config.ts is the precedent for a standalone harness. Worth a card if maintainers want this class of regression caught rather than argued." ], "contradicts_the_brief": [ "The dispatch and the triage seat both lean toward making the swimlane path REUSE the per-column h3 at :319 rather than fixing the row's height. I confirmed structurally that the two header implementations must stay separate and did not take that route: the swimlane layout instantiates no column components at all (each lane paints plain column cells), so reusing KanbanColumnView's heading would repeat the labels PER LANE — which the card's own Expected settles the other way ('paints the column titles above the lanes'). Per the escalation fence that is a product ruling, so instead the reason is now written at the call site, as the triage's own fallback clause required. The aria half of that clause (the swimlane titles carry no id, unlike kanban-col-*) is left undone and is not filed, for the rate-limit reason above.", "The brief says a class assertion is the fallback if a height cannot be measured. Sharper reading: the height CAN be measured honestly in this container (Chromium 1194 is preinstalled and Tailwind v4 compiles the real CSS), just not from CI — so this PR ships both, and the pin's docstring names which of the two is which. The measurement is evidence in the PR, not a test.", "AGENTS.md's own house style for this package says pins should assert a resolved VALUE and 'never a className' (ObjectKanban.navWidthDefault.test.tsx says so in as many words). This pin does assert class tokens, deliberately and against that line: for a pure-CSS collapse the class IS the resolved declaration, and happy-dom offers no other honest reading. Flagging it rather than quietly departing from the house rule.", "The dispatch's 'plugin-kanban moved today, every line number is stale' warning did not hold in the direction implied: #8313/#8328 added registration declarations in index.tsx, and the three ancestor lines the card measured are still at 659/661/662, with the flat h3 still at 319. Re-derived rather than trusted, but the card's numbers were accurate.", "Repo-shape note for the PM's own rules: objectui has no scripts/pm/os-verify-lock.sh (that lives in objectstack), so the shared verification lock does not apply here; runs were kept targeted instead (one package build closure, one package suite, --workspace-concurrency=2)." ] }Draft PR: #8430. Left in draft for the PM's contract review; the PR body carries the same
evidence plus the before/after measurement table.
Generated by Claude Code
Turning swimlanes on for an
object-kanbanremoves the status column headers from the screen. They stay in the DOM, at the right coordinates, and paint nothing — so the board becomes a grid of cards with no way to tell Open from Done except by dragging one and watching where it lands.Measured on
@objectstack/console17.2.0, served by@objectstack/cli17.2.0, in Chromium 1194 at 1600×1000, against a livepnpm demoof thedulyapp (duly_task.board,groupByField: 'status').Reproduce
Author a kanban list view and give the view (not the
kanbanblock) agrouping:The relay derives the swimlane from
swimlaneField || grouping.fields[0].field, so this is the authorable route (KanbanConfigSchemais strict and rejectskanban.swimlaneField, so it is the only one).What happens
Lanes render correctly —
▼ CATALOG (19)and siblings, cards inside them. The column headers do not.With
groupingpresent, the five titles are in the DOM but invisible:Their ancestors, from the title outwards:
document.elementFromPoint()at a title's own centre returns the lane collapse<button>(▶catalog(19)) sitting behind it, and a screenshot cropped to that band shows blank space above the first lane. Computed style on the title is unremarkable —color: oklab(0.403 -0.049 -0.039 / 0.9),font-size: 14px,opacity: 1,visibility: visible— so this is layout, not paint: the header row's flex container measures 0 high and its children collapse with it.With
groupingabsent and nothing else changed, the same board renders the titles as real elements at the same band and they are visible:Note the tag change —
<h3>in the plain board,<span>inside the swimlane header row — so the two paths build the header row differently and only the swimlane one collapses.Why it matters
A kanban's columns are its semantics. Without the labels the swimlane board reads as an unordered card wall: the lane tells you the caliber of the work and nothing tells you its state. It also degrades silently — the view validates, the lanes look right, and the missing row is easy to read as a design choice rather than a bug.
Practical effect downstream:
objectstack-ai/duly#108asked for swimlanes by source on its board (a pre-sales demo screen). Swimlanes work and were dropped anyway, because a board with no column titles is worse than a board with no lanes; the card face carriessourceinstead. The one-line re-enable is sitting in a comment insrc/views/task.view.tswaiting on this.Expected
The swimlane header row keeps its height and paints the column titles above the lanes, as the non-swimlane board does.