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.
The record page's lifecycle path looks like a control and is inert: stageField renders as a segmented button group with no handler, no cursor, and no tab stop #5768
Transferred from objectstack-ai/objectstack#11243 (triage 2026-08-23, file-at-destination: this is generic record chrome in the console/record page — the body stands with objectstack removed). Original author: os-zhuang, filed 2026-08-23. Original body verbatim below.
The record page's lifecycle path looks like a control and is inert: it renders stageField as a segmented button group with no handler, no cursor, and no tab stop
Measured on 17.1.0 (open edition, objectstack dev), HotCRM crm_quote — but it is the generic record chrome, so it applies to every object that declares a stageField.
What renders
The record page draws the object's lifecycle across the top:
Pill-shaped segments, the current one filled in the accent colour, the rest in a resting state — visually a segmented button group, and positioned exactly where a CRM user expects the stage control to be.
What it is
document.querySelectorAll('main [role="listitem"]')// ← listitem, not button/tabgetComputedStyle(el).cursor// "auto"el.getAttribute('tabindex')// nullel.closest('button,[role=button],[role=tab],a')// null
A full pointer sequence (pointerdown → mousedown → pointerup → mouseup → click) on the target segment changes nothing; the record's status is still draft afterwards. Advancing the record needs 编辑 → the status field → 更新, or a bulk action.
Why it is worth changing
The path is the affordance every CRM user reaches for first — in the products this chrome is modelled on, clicking the next segment is how you advance a record. Rendering it as buttons and wiring nothing is the shape that costs a user two or three clicks before they conclude it is decoration, and it teaches them to distrust the rest of the chrome.
Two directions, either defensible:
Make it a control. Clicking a segment writes stageField, subject to the same permission/validation path an edit takes (so a locked or unauthorised record refuses with the normal envelope). This is what the visual promises.
Make it look like a readout. If advancing must stay in the form, drop the button styling — a progress rail without pill-and-fill reads as status, not as a control, and nobody loses a click to it.
What it should not keep doing is looking like (1) and behaving like (2).
Accessibility note
The segments carry role="listitem" with no tabindex, so a keyboard user cannot reach them at all — which is correct for a readout and wrong for a control. Whichever direction is taken, the role should follow: listitem for a readout, tab/button with a tab stop for a control.
Triage: needs-user-decision, domain:ui, Feature — the two directions genuinely diverge in product semantics (interactive stage control vs status readout), and direction 1 expands the interactive surface with a write path; that is the manual floor.
Maintainer ruling recorded (atomic four-part write)
Ruled 2026-08-23, live PM chat, verbatim: 「10950 不考虑存量,其他接受你的建议」 — this card falls under the accepted recommendations. Ruled: direction 2 first (stop-loss) — remove the button-like styling from the inert lifecycle path and fix the accessibility role to an honest readout (listitem family); THIS card is that stop-loss. Direction 1 (a real click-to-advance stage control reusing the edit permission/validation envelope) is NOT approved now — it awaits its own appetite; refile it as a separate Feature when wanted, ⛔ not folded here. needs-user-decision → pm:queue in the same stroke (domain:ui).
Claim — session 01EuPCi56cnGyykygi3z9w4m (os-dev, dispatched by the domain:ui PM seat), branch claude/issue-5768-inert-lifecycle-path.
Precedence evidence (checked before writing any code):
Comments re-read in full: 2 comments, neither is a claim from another session (one triage facets-block by os-zhuang, one maintainer-ruling record by claude[bot]).
git ls-remote --heads origin 'claude/issue-5768*' → no matching refs (no live branch).
Working the ruling as recorded 2026-08-23: direction 2 (stop-loss) — the lifecycle path stops claiming to be a control (de-button the styling, honest readout semantics). Direction 1 (click-to-advance writing stageField through the edit permission/validation envelope) is explicitly not folded in here.
{
"issue": 5768,
"status": "done",
"branch": "claude/issue-5768-inert-lifecycle-path",
"pr": "https://github.com/objectstack-ai/objectui/pull/5917",
"premise_still_valid": true,
"summary": "Located the live source at packages/plugin-detail/src/renderers/record-path.tsx; it matches the browser measurement exactly (role=\"listitem\", no tabindex, no handler). Measured the dispatch's question 1 first: NO write path reaches this surface — useRecordContext()'s value (RecordContextValue) carries data/refresh/headerSystemActions/onToggleFavorite and no record-field mutation; inline edit lives in record:details' InlineEditProvider + InlineEditSaveBar (dataSource.update with ifMatch) and actions in useActionEngine, neither reachable here. That is branch 3, and it agrees with the maintainer ruling of 2026-08-23 (direction 2, stop-loss). So the element stops claiming to be a control: each stage is now a thin decorative rail segment with its label as plain text beneath — the vocabulary RecordApprovalsPanel already uses — with the filled pill, shadow, ring, bordered mobile chip and equal-width tap target removed. No handler was added (a no-op or toast would trade a silent lie for a noisy one). One nuance worth recording: the ruling asked to 'fix the accessibility role to an honest readout (listitem family)', but the role was ALREADY correct — role=list/listitem, aria-current=step, no tab stop is exactly right for a readout, and the card's own accessibility note says so. That half was a no-op at the live source; the substantive change is the de-buttoning, and the accessible semantics are deliberately unmoved. data-stage-state / data-stage-terminal / data-stage-rail now carry the state colour used to be the sole carrier of, so the classification is assertable without CSS.",
"tests": "All at final commit 292d2f73f, under scripts/pm/os-verify-lock.sh, exit codes captured before any pipe. TYPE-CHECK+SUITE+LINT union: '> @object-ui/plugin-detail@17.6.0 type-check' / '> tsc --noEmit && tsc -p tsconfig.test.json'; 'Test Files 3 passed (3)' / 'Tests 110 passed (110)'; '> @object-ui/plugin-detail@17.6.0 lint' / '> eslint .'; 'os-verify-lock: VERDICT command-exit 0 · held the lock 39s · waited 19s'. Dependency closure built first (pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-detail^...' build, VERDICT command-exit 0). GATES, each quoted from its own verdict line: '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'; 'No changeset declares a `major` bump.'; 'All workspace packages are in the changeset fixed group.'; 'check-control-bytes: OK (scanned 4918 tracked text file(s); skipped 85 binary).' No i18n gate run and none owed — the change adds no user-facing string. NEW SUITE: record-path.inertReadout.test.tsx, 9 tests, asserting accessible semantics and structure only (the test DOM resolves no Tailwind, so getComputedStyle answers nothing there). REVERSE VERIFICATION — no rebuild was needed and none was skipped: the suite imports the subject RELATIVELY ('../record-path') and its only cross-package import (@object-ui/react) is mapped by the root vitest.config.mts alias table to packages/react/src, so the resolution path never touches any dist/. Both ablations ran under trap '<restore>' EXIT INT TERM and were confirmed ON DISK by grepping injected AND removed text, never an editor exit code. (1) Remove data-stage-rail — on-disk: removed-text count 0, injected-text count 1; predicted B4 red alone; observed 'Tests 1 failed | 8 passed (9)', the failure being B4. (2) Inject a <button> into each stage — on-disk: injected count 1, anchor still 1; predicted B1+B2 red; observed 'Tests 2 failed | 7 passed (9)', failures B1 and B2. RESTORE leg: restored file byte-identical to the pre-ablation copy (single unique sha256), zero ablation residue, suite back to '17 passed (17)', and the union above re-run green after the final commit. NON-DISCRIMINATING LEGS, named: B1 (no interactive role), B2 (no tab stop), B3 (inert on the card's full pointer sequence) were already true before this PR — they pin against the wrong repair rather than measuring this one. B4 is the only discriminating leg. LINT NARROWING, declared: whole touched package linted (eslint . in packages/plugin-detail, exit 0); narrowing to that package is justified on three pieces of evidence — (1) universe read from eslint's own config via --format json (the changeset .md came back ignored-by-config, both source files linted), (2) count read from --format json (3 submitted, 3 reported, 0 errors), (3) invariance: eslint.config.js declares no projectService and no parserOptions.project (0 matches), so no type-aware program exists and this diff cannot move any untouched file's verdict. Warning parity held: baseline record-path.tsx was 0 errors / 15 warnings, the rewrite came in at 16, and the one added no-explicit-any was removed, returning it to 0 / 15.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #5916: record:path conveys travelled / upcoming / lost stages by colour alone — the ✓ and ✗ glyphs are aria-hidden, so the state never reaches the accessible name (WCAG 2.2 SC 1.4.1). Pre-existing, not introduced here; a proper repair needs new user-facing strings and i18n keys, so it was not folded in. Filed unassigned and unlabeled for PM triage."
]
}
PM acknowledgement — branch 3 confirmed, and my ruling's second clause was already true
Measuring question 1 first is what decided this correctly. No write path reaches this surface: useRecordContext()'s value carries data / refresh / headerSystemActions / onToggleFavorite and no record-field mutation of any kind; inline edit lives in record:details' InlineEditProvider + InlineEditSaveBar (dataSource.update with ifMatch), actions go through useActionEngine with usePermissions gating, and neither is reachable from here. Connecting one would mean opening a new write channel plus permission gating, validation and OCC — the feature the maintainer deferred. Branch 3 it is, and it agrees with the 2026-08-23 ruling independently.
A correction to my ruling, and it is mine: the brief said to fix the accessible role to an honest readout. The role was already correct — role=list / listitem with aria-current=step and no tab stop is exactly right for a readout, and the card's own accessibility note says so. That half was a no-op at the live source. The substantive change is the de-buttoning: filled pill, shadow, ring, bordered mobile chip and equal-width tap target removed, labels as plain text under a thin rail, reusing RecordApprovalsPanel's existing vocabulary. Leaving the accessible semantics deliberately unmoved — and saying so — is better than "fixing" something that was not broken to satisfy a brief.
No handler was added. A no-op or a toast would trade a silent lie for a noisy one; that is the whole point of the card and it was held.
The honesty that makes this report worth reading
Of the nine new assertions, B1 / B2 / B3 (no interactive role, no tab stop, inert under the card's full pointer sequence) were already true before this PR — they pin against the wrong repair, they do not measure this one. B4 is the only discriminating leg, and the ablations were built around that: removing data-stage-rail reddened B4 alone as predicted; injecting a <button> into each stage reddened B1 + B2 as predicted. Reporting three quarters of a green suite as non-discriminating is the opposite of the instinct to quote "9 passed", and it is what lets the next reader trust the fourth.
The bundle corroborates the direction: plugin-detail244.01 → 243.38 KB. A fix that removes a false affordance should make the artifact smaller, and it did.
Moving the state classification onto data-stage-state / data-stage-terminal — where colour used to be its sole carrier — is what makes the classification assertable without CSS, and it is also what lets #5916 be split off cleanly: conveying travelled / upcoming / lost by colour alone with aria-hidden glyphs (WCAG 2.2 SC 1.4.1) is pre-existing, needs new user-facing strings and i18n keys, and would have been scope creep here. Correctly filed unassigned and unlabelled. ⛔ Not queued by this seat.
Activity
os-zhuang commentedon Aug 23, 2026
Triage:
needs-user-decision,domain:ui, Feature — the two directions genuinely diverge in product semantics (interactive stage control vs status readout), and direction 1 expands the interactive surface with a write path; that is the manual floor.[facets-block]
stageField的对象都会撞的通用底盘问题,不是单应用皮肤问题。listitem,control ⇒tab/button+tabstop),两方向都要修正现状的半吊子角色。一句话问题:记录页生命周期条——做成真控件(点击推进 stage,走编辑同款权限/校验),还是收敛为纯状态读出(去按钮化样式+角色修正)?
stageField,复用编辑权限/校验信封,角色改tab/button+tabstoplistitem推荐 2→1(置信度中高):现状的「说谎样式」是当下伤害,几行就能止;方向 1 值得做但属能力扩张,按创业阶段纪律单独定 appetite。裁决格式:回「1」/「2」/「2→1」。
Generated by Claude Code
claude commentedon Aug 23, 2026
Maintainer ruling recorded (atomic four-part write)
Ruled 2026-08-23, live PM chat, verbatim: 「10950 不考虑存量,其他接受你的建议」 — this card falls under the accepted recommendations. Ruled: direction 2 first (stop-loss) — remove the button-like styling from the inert lifecycle path and fix the accessibility role to an honest readout (
listitemfamily); THIS card is that stop-loss. Direction 1 (a real click-to-advance stage control reusing the edit permission/validation envelope) is NOT approved now — it awaits its own appetite; refile it as a separate Feature when wanted, ⛔ not folded here.needs-user-decision→pm:queuein the same stroke (domain:ui).Generated by Claude Code
claude commentedon Aug 23, 2026
Claim — session
01EuPCi56cnGyykygi3z9w4m(os-dev, dispatched by thedomain:uiPM seat), branchclaude/issue-5768-inert-lifecycle-path.Precedence evidence (checked before writing any code):
facets-blockby os-zhuang, one maintainer-ruling record by claude[bot]).git ls-remote --heads origin 'claude/issue-5768*'→ no matching refs (no live branch).stageFieldrenders as a segmented button group with no handler, no cursor, and no tab stop #5768.Working the ruling as recorded 2026-08-23: direction 2 (stop-loss) — the lifecycle path stops claiming to be a control (de-button the styling, honest readout semantics). Direction 1 (click-to-advance writing
stageFieldthrough the edit permission/validation envelope) is explicitly not folded in here.Generated by Claude Code
Generated by Claude Code
claude commentedon Aug 23, 2026
{ "issue": 5768, "status": "done", "branch": "claude/issue-5768-inert-lifecycle-path", "pr": "https://github.com/objectstack-ai/objectui/pull/5917", "premise_still_valid": true, "summary": "Located the live source at packages/plugin-detail/src/renderers/record-path.tsx; it matches the browser measurement exactly (role=\"listitem\", no tabindex, no handler). Measured the dispatch's question 1 first: NO write path reaches this surface — useRecordContext()'s value (RecordContextValue) carries data/refresh/headerSystemActions/onToggleFavorite and no record-field mutation; inline edit lives in record:details' InlineEditProvider + InlineEditSaveBar (dataSource.update with ifMatch) and actions in useActionEngine, neither reachable here. That is branch 3, and it agrees with the maintainer ruling of 2026-08-23 (direction 2, stop-loss). So the element stops claiming to be a control: each stage is now a thin decorative rail segment with its label as plain text beneath — the vocabulary RecordApprovalsPanel already uses — with the filled pill, shadow, ring, bordered mobile chip and equal-width tap target removed. No handler was added (a no-op or toast would trade a silent lie for a noisy one). One nuance worth recording: the ruling asked to 'fix the accessibility role to an honest readout (listitem family)', but the role was ALREADY correct — role=list/listitem, aria-current=step, no tab stop is exactly right for a readout, and the card's own accessibility note says so. That half was a no-op at the live source; the substantive change is the de-buttoning, and the accessible semantics are deliberately unmoved. data-stage-state / data-stage-terminal / data-stage-rail now carry the state colour used to be the sole carrier of, so the classification is assertable without CSS.", "tests": "All at final commit 292d2f73f, under scripts/pm/os-verify-lock.sh, exit codes captured before any pipe. TYPE-CHECK+SUITE+LINT union: '> @object-ui/plugin-detail@17.6.0 type-check' / '> tsc --noEmit && tsc -p tsconfig.test.json'; 'Test Files 3 passed (3)' / 'Tests 110 passed (110)'; '> @object-ui/plugin-detail@17.6.0 lint' / '> eslint .'; 'os-verify-lock: VERDICT command-exit 0 · held the lock 39s · waited 19s'. Dependency closure built first (pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-detail^...' build, VERDICT command-exit 0). GATES, each quoted from its own verdict line: '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'; 'No changeset declares a `major` bump.'; 'All workspace packages are in the changeset fixed group.'; 'check-control-bytes: OK (scanned 4918 tracked text file(s); skipped 85 binary).' No i18n gate run and none owed — the change adds no user-facing string. NEW SUITE: record-path.inertReadout.test.tsx, 9 tests, asserting accessible semantics and structure only (the test DOM resolves no Tailwind, so getComputedStyle answers nothing there). REVERSE VERIFICATION — no rebuild was needed and none was skipped: the suite imports the subject RELATIVELY ('../record-path') and its only cross-package import (@object-ui/react) is mapped by the root vitest.config.mts alias table to packages/react/src, so the resolution path never touches any dist/. Both ablations ran under trap '<restore>' EXIT INT TERM and were confirmed ON DISK by grepping injected AND removed text, never an editor exit code. (1) Remove data-stage-rail — on-disk: removed-text count 0, injected-text count 1; predicted B4 red alone; observed 'Tests 1 failed | 8 passed (9)', the failure being B4. (2) Inject a <button> into each stage — on-disk: injected count 1, anchor still 1; predicted B1+B2 red; observed 'Tests 2 failed | 7 passed (9)', failures B1 and B2. RESTORE leg: restored file byte-identical to the pre-ablation copy (single unique sha256), zero ablation residue, suite back to '17 passed (17)', and the union above re-run green after the final commit. NON-DISCRIMINATING LEGS, named: B1 (no interactive role), B2 (no tab stop), B3 (inert on the card's full pointer sequence) were already true before this PR — they pin against the wrong repair rather than measuring this one. B4 is the only discriminating leg. LINT NARROWING, declared: whole touched package linted (eslint . in packages/plugin-detail, exit 0); narrowing to that package is justified on three pieces of evidence — (1) universe read from eslint's own config via --format json (the changeset .md came back ignored-by-config, both source files linted), (2) count read from --format json (3 submitted, 3 reported, 0 errors), (3) invariance: eslint.config.js declares no projectService and no parserOptions.project (0 matches), so no type-aware program exists and this diff cannot move any untouched file's verdict. Warning parity held: baseline record-path.tsx was 0 errors / 15 warnings, the rewrite came in at 16, and the one added no-explicit-any was removed, returning it to 0 / 15.", "open_questions": [], "out_of_scope_findings": [ "filed as #5916: record:path conveys travelled / upcoming / lost stages by colour alone — the ✓ and ✗ glyphs are aria-hidden, so the state never reaches the accessible name (WCAG 2.2 SC 1.4.1). Pre-existing, not introduced here; a proper repair needs new user-facing strings and i18n keys, so it was not folded in. Filed unassigned and unlabeled for PM triage." ] }Generated by Claude Code
Generated by Claude Code
claude commentedon Aug 23, 2026
PM acknowledgement — branch 3 confirmed, and my ruling's second clause was already true
Measuring question 1 first is what decided this correctly. No write path reaches this surface:
useRecordContext()'s value carriesdata/refresh/headerSystemActions/onToggleFavoriteand no record-field mutation of any kind; inline edit lives inrecord:details'InlineEditProvider+InlineEditSaveBar(dataSource.updatewithifMatch), actions go throughuseActionEnginewithusePermissionsgating, and neither is reachable from here. Connecting one would mean opening a new write channel plus permission gating, validation and OCC — the feature the maintainer deferred. Branch 3 it is, and it agrees with the 2026-08-23 ruling independently.A correction to my ruling, and it is mine: the brief said to fix the accessible role to an honest readout. The role was already correct —
role=list/listitemwitharia-current=stepand no tab stop is exactly right for a readout, and the card's own accessibility note says so. That half was a no-op at the live source. The substantive change is the de-buttoning: filled pill, shadow, ring, bordered mobile chip and equal-width tap target removed, labels as plain text under a thin rail, reusingRecordApprovalsPanel's existing vocabulary. Leaving the accessible semantics deliberately unmoved — and saying so — is better than "fixing" something that was not broken to satisfy a brief.No handler was added. A no-op or a toast would trade a silent lie for a noisy one; that is the whole point of the card and it was held.
The honesty that makes this report worth reading
Of the nine new assertions, B1 / B2 / B3 (no interactive role, no tab stop, inert under the card's full pointer sequence) were already true before this PR — they pin against the wrong repair, they do not measure this one. B4 is the only discriminating leg, and the ablations were built around that: removing
data-stage-railreddened B4 alone as predicted; injecting a<button>into each stage reddened B1 + B2 as predicted. Reporting three quarters of a green suite as non-discriminating is the opposite of the instinct to quote "9 passed", and it is what lets the next reader trust the fourth.The bundle corroborates the direction:
plugin-detail244.01 → 243.38 KB. A fix that removes a false affordance should make the artifact smaller, and it did.Moving the state classification onto
data-stage-state/data-stage-terminal— where colour used to be its sole carrier — is what makes the classification assertable without CSS, and it is also what lets #5916 be split off cleanly: conveying travelled / upcoming / lost by colour alone witharia-hiddenglyphs (WCAG 2.2 SC 1.4.1) is pre-existing, needs new user-facing strings and i18n keys, and would have been scope creep here. Correctly filed unassigned and unlabelled. ⛔ Not queued by this seat.PR #5917 lands at its gate window.
Generated by Claude Code
Generated by Claude Code
fix(plugin-detail,i18n): announce record:path stage state in the acce…
fix(plugin-detail,i18n): announce record:path stage state in the acce…