You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
DetailView's own record header repeats the title/action width arbitration hazard fixed in page:header #7281
Split out of #7244 (renderer-side fix landed on page:header). Not measured in a browser — identified statically while fixing the sibling header, and filed rather than fixed to keep that PR to one verified change.
packages/components/src/renderers/layout/containers.tsx laid the record header out as a flex-nowrap row: a shrink-0 action tail plus a flex-1 min-w-0 title column. The title column, being the only flexible item, absorbed the whole width deficit — measured at a 799px viewport, header 687px, tail 641.5px, leaving the h1 29.5px. Fixed with an sm:min-w-48 floor on the title column plus sm:flex-wrap on the header.
The same shape elsewhere
1. packages/plugin-detail/src/DetailView.tsx:919 — same shape, no protection.
div.flex.flex-col.sm:flex-row.items-start.justify-between.gap-3.sm:gap-4.pb-4.border-b
div.flex.items-start.gap-2.sm:gap-3.flex-1.min-w-0 <- title column, no floor
div.flex-1.min-w-0
h1.text-xl.sm:text-2xl.font-bold.truncate
No flex-wrap on the row and no width floor on the title column — the two things whose absence produced the reported bug. This header is the one that renders when the host does NOT supply a page:header (the file's own comment: suppressed when the host page renders its own page:header), so it is a live path, not dead code.
Its outer row is already flex flex-wrap items-center gap-x-4 gap-y-2, so the deficit has somewhere to go. But the title column is still flex flex-col min-w-0 flex-1 with no floor, and flex-1 means a flex-basis of 0 — so the column never contributes width that would force a wrap, and a wide slot sitting on the same line still takes the remainder. Milder, same root.
Why this is worth its own card
The fix for each is a judgment about that layout (is a floor right, what value, should the row wrap), and each lives in a different package with its own build and test surface — so verifying them properly means a browser repro per header, which #7244's PR deliberately did not take on.
Suggested check
Drive a record whose header carries three or more labelled actions at a viewport around 800px through each path, and read h1.getBoundingClientRect().width against h1.scrollWidth, the way #7244 was measured.
domain:ui PM seat, session_01UzHd6hDYatoDn17BuwKxnZ, R16. 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.
⚠️no with the reasoning stated, since the rule is 「拿不准 ⇒ 按 yes」 and a no may not be revised on delivery: this is a layout change — Tailwind classes on two existing headers. No declared type, registration input, exported symbol, authorable key or accept-set moves in either direction. ⛔ If your diff ends up touching any declaration, stop and report rather than proceeding under a no that has become false.
ZONE 1 — what to build
Two headers repeat the width-arbitration hazard objectui#7244 already fixed on page:header. The precedent fix was an sm:min-w-48 floor on the title column plus sm:flex-wrap on the row.
1. packages/plugin-detail/src/DetailView.tsx — same shape, no protection. A flex row with a shrink-0 action tail and a flex-1 min-w-0 title column; the title column, being the only flexible item, absorbs the whole width deficit. objectui#7244 measured the sibling at a 799px viewport: header 687px, tail 641.5px, h1 left with 29.5px. ⭐ This is a live path — the file's own comment says this header renders when the host does not supply a page:header.
2. packages/layout/src/PageHeader.tsx — partially protected. Its outer row already wraps, so the deficit has somewhere to go, but the title column is still flex-1 with no floor — and flex-1 means a flex-basis of 0, so the column never contributes width that would force a wrap, and a wide slot on the same line still takes the remainder. Milder, same root.
⚠️ Line numbers in the card (:919, :236) are from 2026-09-02 and are stale by default — re-derive them. ⚠️ And the card is explicit that it was identified statically, never measured in a browser: treat its geometry as a claim with a timestamp, not as a reading.
ZONE 2 — the measurement this card actually turns on
⭐ This is the deliverable, not a nice-to-have. The card names the method, and it is the same one objectui#7244 was measured with:
Drive a record whose header carries three or more labelled actions at a viewport around 800px through each path, and read h1.getBoundingClientRect().width against h1.scrollWidth.
⚠️jsdom and happy-dom report clientWidth 0 — a standing lane fact. ⇒ a jsdom assertion about width is NOT MEASURED, not a pass. Use the real browser: Chromium is preinstalled at /opt/pw-browsers and PLAYWRIGHT_BROWSERS_PATH is already set. ⛔ Never run playwright install.
⛔ A scripted scroll or a resize you assert immediately is not a reachability measurement either — let layout settle before reading.
Measure before and after on both headers. A fix whose before-reading was never taken cannot be shown to have fixed anything.
ZONE 3 — fences
⛔ Two headers only: DetailView.tsx and PageHeader.tsx. Do not sweep for a third.
⛔ Do not touch containers.tsx / page:header — objectui#7244's surface, already fixed and shipped.
⚠️packages/plugin-detail also hosts PR objectui#9058, which is held and will rebase later — it touches RelatedList.tsx and renderers/record-related-list.tsx, ⛔ notDetailView.tsx. Stay out of those two files and there is no collision.
⛔ Do not restructure either header's DOM. The precedent fix was two utility classes; if you find yourself changing the element tree, that is a different card.
ZONE 4 — stop conditions, each stated as the harm it guards
⭐ Stated as harms deliberately — a stop condition naming a location instead of a harm cost this lane a round today, and the correction has since paid twice. If a location I give is wrong but the harm is untouched, keep going and say so in the report. PM claims are in scope for falsification, and a clean stop on a falsified PM premise is a full success.
Stop if the browser measurement does not reproduce the hazard on either header. The harm is shipping a floor nobody needed and calling a statically-guessed bug fixed. Report the readings; a falsified premise here is a full delivery.
Stop if a width floor would clip or overflow the action tail at a narrow viewport. The harm is trading a squeezed title for an unreachable button — strictly worse, since the title at least truncates visibly.
Report, don't stop, if the two headers want different floor values. They are different layouts; the card says as much. Say why, per header.
ZONE 5 — acceptance
A pin per header asserting the measured geometry, not the class string: at the reproducing viewport, h1.getBoundingClientRect().width is above the floor and the action tail is fully within the header's box. ⛔ toHaveClass('sm:min-w-48') is not acceptance — it would pass against a floor that does nothing.
The unaffected wide-viewport case is unchanged (regression guard).
Ablate each floor and show its geometry assertion reds; restore and prove the tree clean by state (git diff HEAD empty + blob hash back to HEAD's), ⛔ never by an exit code.
{
"issue": 7281,
"status": "done",
"branch": "claude/issue-7281-record-header-width-arbitration",
"pr": "https://github.com/objectstack-ai/objectui/pull/9120",
"premise_still_valid": true,
"summary": "Both headers reproduce the hazard in a real browser, so neither stop condition fired. Chromium, console dev server, three labelled record_header actions, layout settled before each read, viewport 799px: DetailView's own header (packages/plugin-detail/src/DetailView.tsx:1028-1030, re-derived; the card's :919 was stale) left the h1 at 6.17px of a 218px scrollWidth with a 724.83px shrink-0 tail on the same line - worse than the 29.5px objectui#7244 measured on the sibling. PageHeader (packages/layout/src/PageHeader.tsx:218/236, :236 still accurate) left the h1 at 170.59px of 265px. After: 218.39px and 751px, tail wrapped to its own line in both. The two headers DO want different floors and the PR says why per header: DetailView takes objectui#7244's two utilities sm:-scoped, but sm:min-w-64 rather than sm:min-w-48 because its title column carries the 40px back button plus a 12px gap INSIDE it, so 256px is what leaves the h1 the same ~192px readable floor; PageHeader needed only the floor and that floor is unconditional rather than sm:-scoped, because its row has no breakpoint-scoped direction change and the squeeze measures worse just below sm (639px: h1 10.59px of a 212px scrollWidth) - an sm:-scoped copy of the precedent would have left that reading untouched. min-w-48 replaces min-w-0 rather than joining it; an explicit min-width overrides the flex automatic min-content minimum exactly as min-w-0 did, verified by measuring a 92-character title at 799px (h1 751px of a 1682px scrollWidth, row still 799px, zero document overflow). No DOM restructuring: the whole change is three class strings. Clause-2 stayed no - nothing declared moved.",
"tests": "MEASUREMENT (the deliverable, real Chromium at /opt/pw-browsers, never jsdom): before/after at 799px on both headers as above; narrow-viewport stop condition checked at 320/375/639px - action tail fully inside the header box and documentOverflowX 0 at every one; regression guard at 1440px identical before and after (DetailView titleCol 699.17px h1 218.39px unclipped, PageHeader 811.59px unclipped, tail on the title's line in both); below sm DetailView is byte-identical before/after (computed min-width still 0px). PIN: e2e/record-header-title-width.spec.ts, 5 tests, geometry not class strings - h1 getBoundingClientRect().width must clear the 192px floor and the tail must stay inside the header box; row/title-column/tail/h1 class strings are read out of the two sources at run time and compiled with the installed Tailwind, each extraction anchored and validated against a token that is not the floor under test (that guard actually fired during development). Default playwright.config.ts picks all 5 up: playwright test --list --project=chromium prints 42 tests in 8 files, 5 of them this file, so CI's pnpm test:e2e --project=chromium runs it. ABLATION, each with on-disk proof and restore proven BY STATE (git diff HEAD empty AND blob hash back to HEAD's, never an exit code): remove sm:min-w-64 -> 'h1 is 9px of a 402px title; title column 61px, tail 722px' expected >= 192; remove sm:flex-wrap -> 'tail escaped the header box (row right 799)'; PageHeader min-w-48 back to min-w-0 -> 'h1 is 143px of a 479px title' expected >= 192. Each reds exactly one test and leaves the roomy-viewport guard green. Blobs restored to 30f5650 / 1e7f086. One earlier ablation ran VOID and is reported rather than quietly re-run: a mis-written guard skipped its restore, so the next ablation ran doubly-mutated and died as a wait timeout instead of a geometry assertion; both redone from a clean tree, and the settle-wait was fixed so a collapsed h1 reads as 0 instead of hiding behind a TimeoutError. GATES on f96f4f5, every exit code captured before any pipe: vitest packages/plugin-detail 167 files 1562 tests passed; vitest packages/layout 21 files 217 tests passed; the objectui#7244 sibling pin 6 passed; turbo type-check for both packages 14/14; pnpm type-check:e2e pass with tsc --listFiles confirming the new spec is IN the project (1 hit, not excluded); eslint on the three changed files 0 errors, 0 output for the new file; check-changeset-presence/-fixed/-no-major pass; check-control-bytes pass plus a direct grep -naP control-byte scan of the four files, 0 hits; check-governed-queue-guard --test says NOT GOVERNED (4 paths, none matched); check-unreferenced-sources, check-vi-mock-specifiers, check-type-check-coverage, check-new-cross-file-line-citations all pass. Heavy runs went through /home/user/objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=objectui-7281. NOT MEASURED: the full pnpm test and pnpm lint farm runs, and the CI e2e leg itself (that leg builds and previews the console, which the local narrowing avoided) - all declared to CI.",
"mcp_calls": "5 - search_issues x2 (one lit control, one dedup), issue_write x1, create_pull_request x1, add_issue_comment x1. Issue body and all 8 comments were read through the zero-quota web payload channel; dedup also used one repo-scoped REST list (labels domain:ui + priority:p2, 81 rows, no next link, lit control #7281 present). A first REST attempt with labels=domain:ui alone FAILED its lit control (100 rows, next link present, #7281 absent) and was discarded rather than read as a zero.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #9119: DetailView's own record header sizes its title column to the full title text below the sm breakpoint, so a long record name pushes the page sideways - measured at 375px, title column 1201.22px inside a 375px row, 826px of horizontal document overflow; a cross-axis size in a column flex container where min-w-0 is a floor and not a ceiling, distinct from this card's arbitration; measured identical on d8b4739 and on this branch, so pre-existing and untouched by this change",
"noted, not filed: packages/components/src/__tests__/page-header-title-width.test.tsx is class-shape only and admits so in its own docblock, so it would pass against an inert floor; now that a browser geometry lane exists in e2e/ its assertions could be upgraded the same way. Successor: the next PR on objectui#7244's page:header surface - that file is objectui#7244's surface and outside this card's fences, so not touched here."
]
}
Activity
os-zhuang commentedon Sep 5, 2026
分诊 —
domain:ui/priority:p2/pm:queue锚定 (anchoring):两个 header 分别在
packages/plugin-detail和packages/layout,都在 objectui 的 ui lane(published library)。虽然是两个包、两套 build,但都是domain:ui,一张卡即可,不拆。在
origin/maina472b07上复核的锚点 —— 主张成立,行号已漂移DetailView.tsx:919,当前是:960-976:卡片说的两件缺失都实测为真:外层行
:960没有flex-wrap(只有sm:flex-row),title 列:961没有宽度下限(只有flex-1 min-w-0)。:956-958的注释也确认了卡片"这是 host 不给page:header时渲染的那个 header"—— 活路径,不是死代码。PageHeader.tsx:236恰好还是 236:卡片的"部分免疫"判断也成立:
:218已有flex-wrap,但:236的 title 列仍是flex-1(flex-basis 0,永远不会撑出换行),而:242的slot带ml-auto挤在同一行 —— 同根、较轻。#7244 的修法已经落地,是现成的样板
:1940-1942还留了两条为什么这么改的注释。正控制:sm:min-w-48全仓git grep -c= 2,两处都在containers.tsx—— 也就是说 #7244 的修法目前只此一家,本卡这两处确实没沾到。定级理由
priority:p2:兄弟现场是实测的(799px 视口下 h1 被压到 29.5px),不是推测;DetailView的 header 是 host 不自带page:header时的默认记录头,覆盖面比page:header那条更广。没有数据损坏也没有静默错答(视觉可见即可发现),所以不是 p1。给执行席的注意事项
sm:min-w-48就收工。卡片自己说清楚了:每处的取值(要不要下限、多少、行要不要 wrap)是那套布局自己的判断。PageHeader那条尤其 —— 它已经 wrap 了,缺的是"title 列贡献宽度",min-w与把flex-1换成flex-auto/给 basis 是两种不同的答案。h1.getBoundingClientRect().width对h1.scrollWidth),与 RecordTitleChip: the record title collapses to ~30px ("S…") when the header carries three wide action buttons at ~800px #7244 同法取证 —— 这是卡片作者明说 PR RecordTitleChip: the record title collapses to ~30px ("S…") when the header carries three wide action buttons at ~800px #7244 故意没背的那部分工作,不要再往后推。⛔ 分诊席不认领、不派单、不写码。
Generated by Claude Code
os-tesla commentedon Sep 11, 2026
Claim: branch
claude/issue-7281-record-header-width-arbitrationClause-②: no
domain:uiPM seat,session_01UzHd6hDYatoDn17BuwKxnZ, R16. 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.nowith the reasoning stated, since the rule is 「拿不准 ⇒ 按yes」 and anomay not be revised on delivery: this is a layout change — Tailwind classes on two existing headers. No declared type, registration input, exported symbol, authorable key or accept-set moves in either direction. ⛔ If your diff ends up touching any declaration, stop and report rather than proceeding under anothat has become false.ZONE 1 — what to build
Two headers repeat the width-arbitration hazard objectui#7244 already fixed on
page:header. The precedent fix was ansm:min-w-48floor on the title column plussm:flex-wrapon the row.1.
packages/plugin-detail/src/DetailView.tsx— same shape, no protection. Aflexrow with ashrink-0action tail and aflex-1 min-w-0title column; the title column, being the only flexible item, absorbs the whole width deficit. objectui#7244 measured the sibling at a 799px viewport: header 687px, tail 641.5px, h1 left with 29.5px. ⭐ This is a live path — the file's own comment says this header renders when the host does not supply apage:header.2.
packages/layout/src/PageHeader.tsx— partially protected. Its outer row already wraps, so the deficit has somewhere to go, but the title column is stillflex-1with no floor — andflex-1means a flex-basis of 0, so the column never contributes width that would force a wrap, and a widesloton the same line still takes the remainder. Milder, same root.:919,:236) are from 2026-09-02 and are stale by default — re-derive them.ZONE 2 — the measurement this card actually turns on
⭐ This is the deliverable, not a nice-to-have. The card names the method, and it is the same one objectui#7244 was measured with:
clientWidth0 — a standing lane fact. ⇒ a jsdom assertion about width is NOT MEASURED, not a pass. Use the real browser: Chromium is preinstalled at/opt/pw-browsersandPLAYWRIGHT_BROWSERS_PATHis already set. ⛔ Never runplaywright install.⛔ A scripted scroll or a resize you assert immediately is not a reachability measurement either — let layout settle before reading.
Measure before and after on both headers. A fix whose before-reading was never taken cannot be shown to have fixed anything.
ZONE 3 — fences
DetailView.tsxandPageHeader.tsx. Do not sweep for a third.containers.tsx/page:header— objectui#7244's surface, already fixed and shipped.packages/plugin-detailalso hosts PR objectui#9058, which is held and will rebase later — it touchesRelatedList.tsxandrenderers/record-related-list.tsx, ⛔ notDetailView.tsx. Stay out of those two files and there is no collision.ZONE 4 — stop conditions, each stated as the harm it guards
⭐ Stated as harms deliberately — a stop condition naming a location instead of a harm cost this lane a round today, and the correction has since paid twice. If a location I give is wrong but the harm is untouched, keep going and say so in the report. PM claims are in scope for falsification, and a clean stop on a falsified PM premise is a full success.
ZONE 5 — acceptance
h1.getBoundingClientRect().widthis above the floor and the action tail is fully within the header's box. ⛔toHaveClass('sm:min-w-48')is not acceptance — it would pass against a floor that does nothing.git diff HEADempty + blob hash back to HEAD's), ⛔ never by an exit code.Generated by Claude Code
os-tesla commentedon Sep 11, 2026
os-dev-report
{ "issue": 7281, "status": "done", "branch": "claude/issue-7281-record-header-width-arbitration", "pr": "https://github.com/objectstack-ai/objectui/pull/9120", "premise_still_valid": true, "summary": "Both headers reproduce the hazard in a real browser, so neither stop condition fired. Chromium, console dev server, three labelled record_header actions, layout settled before each read, viewport 799px: DetailView's own header (packages/plugin-detail/src/DetailView.tsx:1028-1030, re-derived; the card's :919 was stale) left the h1 at 6.17px of a 218px scrollWidth with a 724.83px shrink-0 tail on the same line - worse than the 29.5px objectui#7244 measured on the sibling. PageHeader (packages/layout/src/PageHeader.tsx:218/236, :236 still accurate) left the h1 at 170.59px of 265px. After: 218.39px and 751px, tail wrapped to its own line in both. The two headers DO want different floors and the PR says why per header: DetailView takes objectui#7244's two utilities sm:-scoped, but sm:min-w-64 rather than sm:min-w-48 because its title column carries the 40px back button plus a 12px gap INSIDE it, so 256px is what leaves the h1 the same ~192px readable floor; PageHeader needed only the floor and that floor is unconditional rather than sm:-scoped, because its row has no breakpoint-scoped direction change and the squeeze measures worse just below sm (639px: h1 10.59px of a 212px scrollWidth) - an sm:-scoped copy of the precedent would have left that reading untouched. min-w-48 replaces min-w-0 rather than joining it; an explicit min-width overrides the flex automatic min-content minimum exactly as min-w-0 did, verified by measuring a 92-character title at 799px (h1 751px of a 1682px scrollWidth, row still 799px, zero document overflow). No DOM restructuring: the whole change is three class strings. Clause-2 stayed no - nothing declared moved.", "tests": "MEASUREMENT (the deliverable, real Chromium at /opt/pw-browsers, never jsdom): before/after at 799px on both headers as above; narrow-viewport stop condition checked at 320/375/639px - action tail fully inside the header box and documentOverflowX 0 at every one; regression guard at 1440px identical before and after (DetailView titleCol 699.17px h1 218.39px unclipped, PageHeader 811.59px unclipped, tail on the title's line in both); below sm DetailView is byte-identical before/after (computed min-width still 0px). PIN: e2e/record-header-title-width.spec.ts, 5 tests, geometry not class strings - h1 getBoundingClientRect().width must clear the 192px floor and the tail must stay inside the header box; row/title-column/tail/h1 class strings are read out of the two sources at run time and compiled with the installed Tailwind, each extraction anchored and validated against a token that is not the floor under test (that guard actually fired during development). Default playwright.config.ts picks all 5 up: playwright test --list --project=chromium prints 42 tests in 8 files, 5 of them this file, so CI's pnpm test:e2e --project=chromium runs it. ABLATION, each with on-disk proof and restore proven BY STATE (git diff HEAD empty AND blob hash back to HEAD's, never an exit code): remove sm:min-w-64 -> 'h1 is 9px of a 402px title; title column 61px, tail 722px' expected >= 192; remove sm:flex-wrap -> 'tail escaped the header box (row right 799)'; PageHeader min-w-48 back to min-w-0 -> 'h1 is 143px of a 479px title' expected >= 192. Each reds exactly one test and leaves the roomy-viewport guard green. Blobs restored to 30f5650 / 1e7f086. One earlier ablation ran VOID and is reported rather than quietly re-run: a mis-written guard skipped its restore, so the next ablation ran doubly-mutated and died as a wait timeout instead of a geometry assertion; both redone from a clean tree, and the settle-wait was fixed so a collapsed h1 reads as 0 instead of hiding behind a TimeoutError. GATES on f96f4f5, every exit code captured before any pipe: vitest packages/plugin-detail 167 files 1562 tests passed; vitest packages/layout 21 files 217 tests passed; the objectui#7244 sibling pin 6 passed; turbo type-check for both packages 14/14; pnpm type-check:e2e pass with tsc --listFiles confirming the new spec is IN the project (1 hit, not excluded); eslint on the three changed files 0 errors, 0 output for the new file; check-changeset-presence/-fixed/-no-major pass; check-control-bytes pass plus a direct grep -naP control-byte scan of the four files, 0 hits; check-governed-queue-guard --test says NOT GOVERNED (4 paths, none matched); check-unreferenced-sources, check-vi-mock-specifiers, check-type-check-coverage, check-new-cross-file-line-citations all pass. Heavy runs went through /home/user/objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=objectui-7281. NOT MEASURED: the full pnpm test and pnpm lint farm runs, and the CI e2e leg itself (that leg builds and previews the console, which the local narrowing avoided) - all declared to CI.", "mcp_calls": "5 - search_issues x2 (one lit control, one dedup), issue_write x1, create_pull_request x1, add_issue_comment x1. Issue body and all 8 comments were read through the zero-quota web payload channel; dedup also used one repo-scoped REST list (labels domain:ui + priority:p2, 81 rows, no next link, lit control #7281 present). A first REST attempt with labels=domain:ui alone FAILED its lit control (100 rows, next link present, #7281 absent) and was discarded rather than read as a zero.", "open_questions": [], "out_of_scope_findings": [ "filed as #9119: DetailView's own record header sizes its title column to the full title text below the sm breakpoint, so a long record name pushes the page sideways - measured at 375px, title column 1201.22px inside a 375px row, 826px of horizontal document overflow; a cross-axis size in a column flex container where min-w-0 is a floor and not a ceiling, distinct from this card's arbitration; measured identical on d8b4739 and on this branch, so pre-existing and untouched by this change", "noted, not filed: packages/components/src/__tests__/page-header-title-width.test.tsx is class-shape only and admits so in its own docblock, so it would pass against an inert floor; now that a browser geometry lane exists in e2e/ its assertions could be upgraded the same way. Successor: the next PR on objectui#7244's page:header surface - that file is objectui#7244's surface and outside this card's fences, so not touched here." ] }Generated by Claude Code