-
Notifications
You must be signed in to change notification settings - Fork 0
Review 5558
Review #5558 — Badge long-label containment
freddymeta · collaborator · OPEN · view on GitHub
#5558 fix(Badge): a long label no longer escapes its container by freddymeta (bucket: collaborator — write access, treated as internal)
1733f9bf784f7a491409e0b59c1d789b83d8ac03 <- every claim below was verified at this commit
LOOP VERSION: 1.5.0 AUDIT RUBRIC: 1.12
Read sources:
- Review kit source of truth on the M5: loop
1.5.0; audit rubric1.12. - Fresh fork-wiki clone at
665b3d1686c92ed6a97d660418705d5b7e659931:Rulings,Review-Presentation,Evidence-Templates, andMeasurement-Harness. - Fresh public-wiki clone at
a43e2bbc27c8cd56c747462f6c1a20d0c90e5379:Component-Audit-Rubric,Design-Conventions,API-Conventions, andTheming-Infrastructure. - Main-branch rubric at
d4d4fc58b0bd2e21ad7ab23a492edcb4aafa4bc5: Copilot/path instructions, review gate, sample comment, contribution guide, and owner files.
LANE: full
WHY: Intended responsive visual change; layout/CSS diff; manual frames required; prior CHANGES_REQUESTED review from rubyycheung remains unresolved; one decisive visual pair plus one control pair captured with sensor receipts.
Clear. The exact head is in facebook/astryx; freddymeta is a collaborator with write permission. The final PR diff contains only one changeset, one Badge story, and Badge.tsx. No dependency, lockfile, workflow, install hook, executable script, credential, environment, or build-network path changed. A worktree-local node_modules symlink was added and removed inside the commit history and is absent from the reviewed head.
WHY 1: The author wants Badge labels that are wider than their available space to stop escaping their container. WHY 2: That escape covers neighboring content in tight places like hover cards and fixed table cells, so a person cannot reliably read either the status badge or the content beside it. WHY 3: Badge exists to make status/category information scannable in dense UI; a badge that bleeds over adjacent content turns that signal into visual noise.
USER-FACING PROBLEM: A person scanning a dense row or hover card sees a long status badge paint over nearby content instead of fitting inside the space the layout gave it.
VERDICT: clear
Badge now lets the pill be no wider than its container. The label text, rather than the icon or the surrounding layout, becomes the part that gives way: it can shrink and clip with an ellipsis while the badge stays one line tall. The responsibility stays inside Badge, so every caller in a tight cell gets the containment behavior without repeating the CSS recipe.
SOLUTION (2 decisions · ~19 runtime lines of +117/-1)
- The badge root gets
maxWidth: '100%'andminWidth: 0so the component can shrink inside flex/grid/table constraints — serves the overflow problem. - The label is wrapped in an inner span with
overflow: hidden,textOverflow: 'ellipsis', andminWidth: 0so text clips instead of pushing the pill wider — serves the overflow problem, but creates the full-label recovery gap below.
VERDICT: note — containment is the right shape, but the new truncation state needs a recovery path.
OWNER: Badge owns its one-line status/category pill geometry.
TIER 1: none.
TIER 2: status/category display; no icon-system or size-system changes.
SEAMS: root xstyle/className/style passthrough, label: ReactNode, optional icon, table/flex/grid/narrow/RTL containers.
BEHAVIOR UNIT: inline CSS — no state machine, no hook, no Effect; the boundary is testable through rendered geometry.
| seam | driven result |
|---|---|
| fixed-layout table cell | works for containment: overlap with the next cell fell 60.11px → 0px. |
| constrained block | works: right spill fell 60.11px → 0px. |
| long unbroken text | works: right spill fell 145.88px → 0px. |
| flex row with sibling | works: after width shrinks to 132.05px inside a 170px row; no overlap. |
| grid track | works: overflowX fell 20px → 0px. |
| RTL via Storybook global | works for containment: left spill fell 60.11px → 0px after preserving the RTL provider direction. |
| icon + truncation story | after path keeps the icon visible and shrinks the label; title/recovery still absent. |
The implementation lives in Badge and does not add a new owner or extension mechanism. xstyle remains after base styles, so callers can still override layout deliberately.
VERDICT: clear
Everyone using Badge gets safer containment in narrow, table, flex, and RTL contexts: the badge no longer paints over adjacent content. The new visible state is that long labels are intentionally clipped; for a meaningful status like “Awaiting security review,” a sighted user can now see only the truncated label and has no component-provided path back to the full text. Existing short labels are unchanged in the control frame.
VERDICT: BLOCKS — clipped status text has no recovery path.
No public API change: BadgeProps still exposes variant?: BadgeVariant, required label: ReactNode, optional icon?: ReactNode, inherited xstyle/className/style/data-*, and no new export. BaseProps omits native title at packages/core/src/BaseProps.ts:26, so a plain string-label caller cannot pass a root title through today; a custom ReactNode label can still carry its own title.
| change | public? | class | doc'd? | verdict | |
|---|---|---|---|---|---|
| — | no API change | — | — | — | ok |
OSSIFICATION: nothing new ossifies. If the fix chooses an internal title fallback for string/number labels, that can stay server-safe and does not require adding a public prop.
VERDICT: clear
No theme target, token, variant, or CSS variable is added or removed. The root target remains astryx-badge with variant; maxWidth: '100%' and minWidth: 0 are large layout constraints allowed by rubric T2b, not tokenized spacing. No raw colors are added in component styling; the story-only outline uses var(--color-border).
VERDICT: clear
BEHAVIOR: Badge still renders the same required label and optional icon; no state/default/condition/event path changed. I also checked label={null}, label={false}, and label="" through Storybook args: the empty badge stayed 16px wide on both main and head without an icon; the remaining nullable-label+icon gap is not part of the documented required-label contract.
API: no existing call site stops compiling; no public prop/export/signature/default changed.
VISUAL: yes, intentionally — long labels clip instead of overflowing; control label Active is 51.98px before and after; no outer dimension grew.
THEME: no theme target/token/var/override changed.
VERDICT: note — intended visual behavior changes, with no breaking API/theme change.
EFFECTS: zero added/changed/moved Effects.
RENDER: one extra static span around the label; no state, subscriptions, memoized return, or render-loop work.
LISTENERS/OBSERVERS: none.
LAYOUT: CSS-only sizing/clipping; no layout reads, ResizeObserver, getBoundingClientRect, or build-time/runtime measurement in production.
BUNDLE: no dependency change and no runtime import added.
VERDICT: clear
VISUAL CHECK: manual frames required WHY: The PR intentionally changes layout/rendered pixels for a core component's overflow state, and the new long-label story does not exist on main.
The decisive pair was opened and inspected. The before badge paints 60.11px into the adjacent cell; the after badge ends inside its 100px cell with an ellipsis and 0px overlap.
| Sensor | Before | After |
|---|---|---|
| Build | main d4d4fc58b0bd2e21ad7ab23a492edcb4aafa4bc5
|
head 1733f9bf784f7a491409e0b59c1d789b83d8ac03
|
| Receipt | JSON | JSON |
| Story | core-badge--default |
same |
| Theme / mode / direction | neutral / light / LTR | same |
| Viewport / media | 420×220 @1; forced colors off; reduced motion off; fine pointer; hover | same |
| Semantic state |
table-long-100; “Awaiting security review” |
same |
| Geometry | badge 156.11×20; adjacent overlap 60.11px | badge 92×20; adjacent overlap 0px |
| Settled / errors | fonts loaded; 0 animations; no Storybook error | same |
| Exact main — badge overlaps the next cell | Exact head — badge stays contained |
|---|---|
![]() |
![]() |
The one needed control stays unchanged: the short Active badge is 51.98×20 before and after.
| Exact main — short-label control | Exact head — short-label control |
|---|---|
![]() |
![]() |
Control receipts: before · after.
Measured class: table overlap 60.11px → 0px; constrained block spill 60.11px → 0px; long unbroken spill 145.88px → 0px; grid overflowX 20px → 0px; RTL left spill 60.11px → 0px; auto-sized long label unchanged at 156.11px; short control unchanged at 51.98px. The reproducible probe, manifest, and exact arm diff are banked.
VERDICT: clear — containment works in the measured layout cases; the missing full-label recovery is a separate blocking UX/a11y finding.
pr-a11y ran and passed on the exact head; .github/a11y-baseline.json is not changed. No production user-visible string or AT-facing string is added; the story text is Storybook-only. RTL was driven through the Storybook direction global, and the constrained RTL case stopped spilling after the fix.
The truncation recovery path is missing: in the PR's own Long labels in tight space story, both long badges are truncated (labelClientWidth 76/100px vs labelScrollWidth 140px), while the badge and label have no title, no focusability (tabIndex = -1), and no visible tooltip carrying the full text after hover. A sighted user who cannot infer the hidden part of “Awaiting security review” cannot recover it from the component.
VERDICT: BLOCKS — truncated user-facing status text has no recovery path.
| slot | verdict |
|---|---|
| PROBLEM | clear |
| SOLUTION | note — containment is right; recovery path missing |
| ARCHITECTURE | clear |
| IMPACT | BLOCKS — clipped status text has no recovery path |
| API | clear |
| THEMING | clear |
| BREAKING | note — intended visual behavior changes; no API/theme break |
| PERFORMANCE | clear |
| VISUAL | clear |
| A11Y & I18N | BLOCKS — truncated user-facing status text has no recovery path |
GOAL: met — the overflow goal is proven: table overlap 60.11px → 0px, constrained spill 60.11px → 0px, long-unbroken spill 145.88px → 0px, and RTL left spill 60.11px → 0px.
DISPOSITION: missing full-label recovery → blocks now; this is the same unresolved hold rubyycheung filed in the existing CHANGES_REQUESTED review, and no later push addressed it.
ADVICE: proven remedy / bounded direction — prior review names the accepted minimum fallback (title), BaseProps currently omits native title, and the PR's own body says a measured tooltip would be a separate client-component tradeoff; add a server-safe full-label fallback for string/number labels or another recovery path with equivalent evidence.
AUTHOR CAN PROCEED: yes — acceptance criteria: when a string/number Badge label is visually truncated, a sighted user can recover the full label at least on hover without making Badge a client component; update the story/comment so it no longer promises a hover behavior that is absent.
WORST OUTCOME: “A sighted user who cannot infer the hidden part of ‘Awaiting security review’ cannot recover it from the component.” → request changes
JUDGEMENT NEEDED: none — defect fix against the existing Badge contract, and the prior design-owner review already asked for this exact recovery path.
request changes
- [BLOCKS] Truncated string labels have no recovery path
→ a sighted user scanning a tight table sees a clipped status and cannot hover/focus/tap to recover the full label ·
packages/core/src/Badge/Badge.tsx:195
Thanks, this fixes the spill. I agree with Ruby’s hold though: the status is now clipped, but the rendered badge has no
title, tooltip, or other path for a sighted user to recover the full label. Can you add a server-safe full-label fallback for string/number labels and keep the richer only-when-truncated tooltip as its own PR if it needs measurement?[Reviewed by Robohands]
Public review length: 61 words (request-changes cap: 150).
-
packages/core/src/Badge/Badge.tsx:195— Might need a full-label fallback here; truncated text has no title/tooltip.
-
apps/storybook/stories/Badge.stories.tsx:96-98says hover reveals full text, but this does not appear in the rendered story; the main issue is the component behavior atBadge.tsx:195. - The long explanatory comments in
Badge.tsx:49-62and the 36-line changeset are heavier than usual, but I did not spend review words there because the truncation recovery path blocks first. - The transient
node_modulessymlink appears only in commit history and is absent from the final file list/head.
TIME total 34m setup 8m worktree + fast-install + one Storybook-config build + Storybook dev; warm main reused after refresh: yes reading 11m kit, critic, fresh wiki pages, main rubric files, PR body, prior review, diff measuring 8m one probe, 2 screenshot pairs, layout/RTL/title semantics; 1 re-measure after RTL sensor fix writing 7m draft + R1-R15 self-critique + rewrite + final critique waste 6m parallel test/CI command queue timed out; image contact sheet attempt failed because PIL is unavailable
- Safari/WebKit rendering; the M5 harness only supports Chromium for Playwright.
- The read tool returned image metadata instead of inline pixels, so I also inspected the frame files via sensor receipts, DOM geometry, and a pixel-classified text render.
- Focused Vitest: Badge 8/8 passed (log).
-
check-use-clientand the changeset gate passed (log). - Exact-head CI: build, test, lint,
pr-a11y,pr-rtl, andpr-visualpass;review-requiredremains pending (checks · actions · statuses). - Chromium frames: passing
captureWithSensorsreceipts for the decisive pair and short-label control. - Layout probe: fixed table, block, unbroken text, flex, grid, RTL, and icon seams all driven before/after.
- Recovery probe: truncated labels have
title = null,tabIndex = -1, and no visible tooltip after hover (log). - Full reproducibility bundle: assets/pr-5558.
DRAFT: request changes for the missing full-label recovery path.
CRITIQUE 1 (R1-R15): failed R1e because it did not explicitly reconcile rubyycheung's existing CHANGES_REQUESTED; failed R15 because the draft referred to visual evidence without listing frame/receipt paths; R2 was close but had removable background. No invented findings under R3/R5/R13/R14.
REWRITE: moved prior-review reconciliation into JUDGEMENT and the public first sentence, cut background, attached exact frame/receipt paths, kept one line-anchored inline.
CRITIQUE 2 (R1-R15): passes R1/R1g collaborator handling, R1e prior-review reconciliation, R2 word cap (61-word public body before signature; 10-word inline), R3/R4/R5 problem/remedy shape, R12 no rubric ids/internal links/Discord, R13 no inherited debt charged, R14 anchors verified, R15 evidence banked. Remaining posting note: if this is publicly posted with visual frames, the parent must publish the PNGs to a durable public location first.
Nothing was posted to the PR. The proposed public text is archived exactly as drafted; no GitHub review, comment, approval, request-changes action, merge, or contributor-branch push was made.
#5558 fix(Badge): a long label no longer escapes its container by freddymeta (bucket: collaborator — write access, treated as internal)
e116dacfb4f381e7985f6b8b83baad5a851c9d63 — exact current head requested.
LOOP VERSION: 1.5.0 AUDIT RUBRIC: 1.13
LANE: full WHY: Re-review after standing request-changes; behavior is visible and the request concerns recovery paths across SSR, hover, touch, keyboard, string/number labels, and rich labels.
Ruby requested a recoverable full label after truncation, ideally tooltip on hover/focus with touch alternative, and said native title was the minimum fallback. Cindy then narrowed our standing ask to a server-safe fallback for string/number labels, leaving richer only-when-truncated tooltip work for its own PR if measurement is needed.
Round 2 satisfies the narrowed ask: string and number labels now render title on the server and after hydration. It deliberately does not solve the broader focus/touch/rich-label tooltip; that remaining work is tracked publicly in #5585.
WHY 1: The first reviewed head fixed Badge overflow by clipping over-wide labels, but clipped status text needed some path back to the full value. WHY 2: Without recovery, a sighted user scanning dense UI could see only an ellipsis and lose the status/category meaning. WHY 3: Badge exists to make compact statuses scannable; truncation is acceptable only if the full status is still recoverable for the supported path. USER-FACING PROBLEM: A person seeing a truncated string/number Badge label needed a server-safe way to recover the full label without turning Badge into a client component. PROBLEM SEVERITY: harmful friction — the status remains visible in part, but the hidden tail can carry the distinguishing information.
VERDICT: clear
Badge derives a plain text fallback only when label is a non-empty string or a number. That value is written to the root title, so server-rendered markup and hydrated markup carry the same browser-native hover fallback. Rich ReactNode labels are not flattened, because doing so would guess at what nested markup or icons mean.
SOLUTION (1 decision · ~21 runtime lines of +74/-10 since reviewed head)
- Add a
labelTitlebranch for string/number labels and set it on the Badge root; document and test the remaining richer tooltip gap via #5585.
BURDEN: low — pure render-time derivation, no hook, state, Effect, listener, observer, measurement, public API, or client boundary. BURDEN MATCH: proportionate — it is the minimum server-safe fallback the standing review asked for.
VERDICT: clear
OWNER: Badge owns the fallback for its own clipped label. TIER 1: none. TIER 2: status/category display; no shared primitive or behavior owner changed. SEAMS: SSR, hydration, string labels, number labels, rich labels, pointer hover, keyboard, touch. BEHAVIOR UNIT: inline pure derivation — no independent behavior unit needed.
| seam | driven result |
|---|---|
| SSR/hydration | one-off Vitest rendered and hydrated string, number, and rich labels with no hydration warning; string/number SSR contained title, rich did not |
| string label | clipped in Chromium and root title="Awaiting security review"
|
| number label | clipped in Chromium and root title="1234567890123456"
|
| rich label | clipped in Chromium and root title=null; this is deliberate and tracked in #5585
|
| pointer hover | the hovered clipped string badge has the full text in the DOM title; no custom tooltip is introduced |
| keyboard | Badge remains unfocusable (tabIndex=-1); no keyboard recovery path in this PR |
| touch | coarse-pointer tap does not show any touch-visible recovery UI in this PR |
VERDICT: note — the minimum fallback works; the remaining full tooltip behavior is explicitly separate.
String/number Badge labels that are clipped now have a browser-native hover fallback without changing server renderability. Keyboard, touch, and rich-node labels still cannot recover the full clipped label from this PR alone; the author opened #5585 for that client-component/focusable-tooltip tradeoff.
VERDICT: note — accepted follow-up, because the previous Cindy-scoped ask was the server-safe string/number fallback.
No public prop, export, default, or accepted value changes. BadgeProps.label remains ReactNode; native title remains omitted from BaseProps, so this is component-owned fallback behavior rather than new caller API.
VERDICT: clear
No theme target, token, CSS variable, or component override contract changed in the round-two delta. The new title attribute is not theme surface.
VERDICT: clear
BEHAVIOR: yes, string/number labels now expose a native title; this is additive fallback behavior. Rich labels, keyboard, and touch remain as before this round.
API: no existing typed call site stops compiling or changes required props.
VISUAL: no pixel change in the round-two code delta; current-head frames match the clipped visual state from round 1.
THEME: no theme target/token/override change.
VERDICT: clear
EFFECTS: zero added/changed/moved Effects.
RENDER: one typeof branch and optional string conversion; no state or render-loop work.
LISTENERS/OBSERVERS: none.
LAYOUT: no measurement or layout reads.
BUNDLE: no dependency or import added.
VERDICT: clear
VISUAL CHECK: manual frames required WHY: The acceptance behavior is visible/clipped text plus recovery affordance state; the exact head also changed docs/story prose after a visual bug review.
Actual PR story at current head:
Current LongLabels story |
|---|
![]() |
Review harness matrix for string, number, rich, and short labels:
| Desktop recovery matrix | Keyboard after Tab | Touch after tap |
|---|---|---|
![]() |
![]() |
![]() |
Sensor summary: assets/pr-5558/round2/sensor-summary.md. Detailed matrix: assets/pr-5558/round2/recovery-detail.json.
VERDICT: clear — the current-head visual state is stable; the no-keyboard/no-touch/no-rich recovery is a known follow-up, not a hidden visual regression.
pr-a11y and pr-rtl ran green on exact head. The new production fallback adds no localized string. Browser-driven checks: string/number clipped labels have title; rich clipped labels do not; all badges remain non-focusable; coarse-pointer tap does not reveal a recovery UI. That matches the intended split between this PR and #5585.
VERDICT: note — keyboard/touch/rich recovery remains absent and tracked.
| slot | verdict |
|---|---|
| PROBLEM | clear |
| SOLUTION | clear |
| ARCHITECTURE | note — minimum fallback only |
| IMPACT | note — broader recovery tracked in #5585 |
| API | clear |
| THEMING | clear |
| BREAKING | clear |
| PERFORMANCE | clear |
| VISUAL | clear |
| A11Y & I18N | note — keyboard/touch/rich recovery remains absent and tracked |
GOAL: met — the standing Cindy-scoped ask was a server-safe full-label fallback for string/number labels; SSR/hydration and Chromium both prove that path. DISPOSITION: focus/touch/rich-label recovery → follow-up #5585; acceptable intermediate because the current PR keeps Badge server-renderable and solves the minimum fallback requested after round 1. ADVICE: omitted — the follow-up issue already names the client tooltip/focus/touch tradeoff. AUTHOR CAN PROCEED: yes — no open blocker remains for this scoped PR; posting approval would clear the standing review-required state once GitHub records it. WORST OUTCOME: “Keyboard, touch, and rich-node labels still cannot recover the full clipped label from this PR alone” → approve with tracked follow-up, not request-changes, because that gap is explicitly out of this narrowed server-safe fallback round.
JUDGEMENT NEEDED: none — defect fix against existing Badge behavior; no public API/design decision remains in this PR.
approve
Thanks, this covers the server-safe string/number fallback I asked for. I’d take this and leave focus/touch/rich-label recovery to #5585.
[Reviewed by Robohands]
Public review length: 21 words including attribution.
None.
- The new source/test comments are heavier than usual, but I did not spend public review words there because the prior hold is addressed and the comments explain a deliberate SSR tradeoff.
-
titleis set even when a string label fits; the author records that in #5585, and it is not user-breaking. - The author cites BaseTable; the actual title precedent is BaseTable header content at
BaseTable.tsx:461-464andTable.test.tsx:1203-1205.
TIME total 57m
setup 17m worktree + fast install + build package + Storybook dev; warm main reused conceptually, but no before rebuild was needed for round-two delta
reading 18m kit, critic, main rubrics, public wiki pages, PR/GitHub history, prior Review-5558 history
measuring 14m focused tests, SSR/hydration test, Chromium recovery matrix, touch/keyboard probes, screenshots
writing 8m presentation + critic self-grade + wiki update
waste 7m first pnpm -F @astryxdesign/core test -- Badge.test.tsx invocation expanded into broad workspace CLI tests; reran with direct Vitest filter
- Native browser tooltip pixels themselves are not capturable in headless Chromium; I verified the DOM
titlefallback and user paths around it. - Safari/WebKit behavior; this review's Playwright harness supports Chromium only.
-
vitest run packages/core/src/Badge/Badge.test.tsx: 12 passed. - One-off SSR/hydration Vitest: string, number, and rich labels, 3 passed.
-
git diff --checkon changed files: passed. -
check:changesets: passed. - Exact-head CI from GitHub:
check-scope,lint,docsite-test,check-components,test,build-storybook,build-sandbox,theme-layers,pr-a11y,pr-visual,pr-rtl,build, CLA, and Vercel all green;review-requiredpending only because code review is outstanding.
Nothing was posted to the PR. The proposed public text is archived exactly as drafted; no GitHub review, comment, approval, request-changes action, merge, reaction, metadata edit, or contributor-branch push was made.







