Skip to content

fix(frontend): render every status badge with shared tones and labels - #1374

Merged
IAmJSD merged 8 commits into
LeanerCloud:mainfrom
IAmJSD:ui/issue-737
Oct 24, 2026
Merged

IAmJSD merged 8 commits into
LeanerCloud:mainfrom
IAmJSD:ui/issue-737

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

Status badges came in four shapes with conflicting colors: "pending" was blue in RI exchange history and amber on Purchases, "running" was orange in one table and blue in another, there were four warning palettes (one at 2.99:1 contrast), and RI exchange printed the raw lowercase status. This PR adds one status-to-tone and status-to-label map and moves every status badge onto the same .badge pill built from tokens.

Closes #737

What changed

  • New frontend/src/lib/status-badge.ts: statusTone(status) maps every status from every API to one of five tones (success, warning, danger, info, muted); statusLabel(status) gives display text (in_progress -> "In Progress", canceled/cancelled -> "Cancelled", otherwise title case); badgeClass/badgeHtml/statusBadgeHtml render <span class="badge badge-<tone>"> with the label escaped. Unknown statuses are muted, never green.
  • Callers moved over: plans.ts (plan status, health, overdue, planned-purchase status), riexchange.ts (history status; the page-local getStatusBadgeClass is removed), modules/registrations.ts, and history.ts (statusBadgeHTML folds the lifecycle onto a display status and calls the shared map, with the same tones and labels as before). utils.ts getStatusBadge returns a tone.
  • CSS: components.css keeps one .badge pill whose five tones come from base.css tokens. Removed: the .status-badge variants, the unused .badge-admin/-user/-readonly rectangles, the planned-purchases Bootstrap badge palette in tables.css, and the whole-row status text colors (only paused rows stay dimmed). The Unsaved-changes pill and the provider-disabled banner (settings.css) use the warn tokens too.
  • Tests: new status-badge.test.ts (33 tests), new badge class/label cases in riexchange.test.ts, and updated plans, utils, css, xss-purchase-status tests and ri-exchange-confirmation.spec.ts (expects "Completed" instead of "completed").

Commits: da125a8a (shared map), 1c894d70 (callers and CSS).

Why this approach

The existing class names (.badge, .badge-success/-warning/-danger/-info/-muted) are kept instead of the issue's BEM --variants, because .badge-* is already used across history, users, API keys and settings, plus about 20 tests; renaming would be churn with no visual gain. The map lives in lib/ instead of utils.ts (the issue's suggestion) because 16 suites mock ../utils wholesale and would lose the new functions. Tone rule: anything not finished yet (pending, notified, scheduled, running, processing, partial) is amber, which is what the Purchases history, the most-used page, already did. Blue info is reserved for non-status tags. All five tone pairs meet 4.5:1 contrast.

Screenshots

Before is main (a14dc8f2), after is this branch. Both are headless Chromium at 1440x900 against the repo's mocked e2e API.

Before After
plans-cards before plans-cards after
The "Manual" plan shows an orange badge next to an amber "Health: 64" badge: two warning palettes on one card. Manual is blue info, health uses the shared amber, and Disabled is a muted pill of the same shape.
planned-purchases before planned-purchases after
Rectangular Bootstrap-palette badges (Running in blue), and each row's text tinted by status. The same pill and tones as everywhere else (pending, notified and running amber; paused muted). Row text is neutral and only the paused row is dimmed.
ri-exchange-history before ri-exchange-history after
Raw lowercase statuses, pending in blue, processing in low-contrast orange. Title-case labels in the shared tones; pending and processing match the Purchases page's amber.

Verification

Run on this branch's head 1c894d70 on top of main a14dc8f2, from frontend/:

  • npm run lint: 0 errors (142 warnings already on main, unchanged). Typecheck and build pass.
  • Jest: 110 suites, 3524 passed, 1 skipped. The changed suites pass with --randomize except riexchange.test.ts, which fails 29 openExchangeModal/fillQuoteFromRI tests under random order on main a14dc8f2 as well (same seed, unchanged copy), so that is an existing order dependency.
  • Playwright (e2e-local.sh): 157/157 passed.
  • Secrets scan clean (planted key flagged); scripts/test-git-secrets-allowlist.sh: 137 PASS.

Notes for review

Design choices to check:

  1. Running, processing and pending are all amber now. Before, RI exchange "pending" and planned-purchase "running" were blue. The alternative is blue for in-flight and amber only for awaiting action, but then the Purchases history "In Progress" badge would change color too.
  2. Plan "Manual" is blue info instead of orange, because it is a setting, not a warning.
  3. Planned-purchase rows no longer color all their text by status; only the badge carries color, and paused rows stay dimmed.
  4. Labels are title case everywhere ("Pending", not "pending"). Canceled shows as "Cancelled" to match the existing Purchases chip copy.
  5. Badges now have white-space: nowrap, so the planned-purchases Status column is a little wider.

Not touched: .badge-group in users/userList.ts has no CSS rule (it renders as an uncolored pill), and two #137333 text colors in settings.css are not badges.

Overlaps

Note for the UI owner

This changes UI code that @cristim owns. IAmJSD reviewed the before/after screenshots and asked for it to be merged; any design point you disagree with can be a follow-up.

🤖 Generated with Claude Code

IAmJSD and others added 2 commits October 24, 2026 00:06
Status badges picked their colors per page: "pending" was blue in RI
exchange history and amber on Purchases, "running" was orange in one
table and blue in another, and RI exchange printed the raw lowercase
status. lib/status-badge.ts is the single place that maps a status from
any API to a tone (success, warning, danger, info, muted) and to display
text, plus helpers that render the shared .badge markup with the label
escaped. Unknown statuses are muted, never green. It lives in lib/ rather
than utils.ts because most suites mock utils wholesale.

The callers move over in the next commit.

Refs LeanerCloud#737

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plans, planned purchases, RI exchange history, registrations and the
Purchases history now render `.badge .badge-<tone>` from
lib/status-badge.ts, so the same status has one color and one label on
every page (RI exchange shows "Pending", not "pending").

- components.css: one .badge pill whose tones come from base.css tokens
  (all >= 4.5:1). The .status-badge variants, the dead .badge-admin /
  -user / -readonly rectangles and the planned-purchases Bootstrap
  palette in tables.css are removed. The orange #ffe0b2/#e65100 pair
  (2.99:1) is gone.
- Not-finished statuses (pending, notified, scheduled, running,
  processing) are amber everywhere, matching what the Purchases history
  already did; paused/canceled/expired/disabled are muted.
- Plan "Manual" is info (blue) instead of the orange warning: it is a
  valid configuration, not a problem.
- Planned purchase rows no longer tint all their text by status (a
  running row had blue text next to its badge); paused rows stay dimmed.
- The Unsaved-changes pill and the provider-disabled banner use the
  warn tokens instead of two more ad-hoc amber palettes.

Refs LeanerCloud#737

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged urgency/this-quarter Within the quarter impact/many Affects most users effort/m Days type/bug Defect labels Oct 24, 2026
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 18 billable files and costs up to $4.50.

Or wait 59 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 049c617 and dd38dda.

Files selected for processing: 18
  • frontend/src/__tests__/css.test.ts
  • frontend/src/__tests__/plans-range-validation.test.ts
  • frontend/src/__tests__/plans.test.ts
  • frontend/src/__tests__/riexchange-audit-321.test.ts
  • frontend/src/__tests__/riexchange.test.ts
  • frontend/src/__tests__/status-badge.test.ts
  • frontend/src/__tests__/utils.test.ts
  • frontend/src/__tests__/xss-purchase-status.test.ts
  • frontend/src/history.ts
  • frontend/src/lib/status-badge.ts
  • frontend/src/modules/registrations.ts
  • frontend/src/plans.ts
  • frontend/src/riexchange.ts
  • frontend/src/styles/components.css
  • frontend/src/styles/settings.css
  • frontend/src/styles/tables.css
  • frontend/src/utils.ts
  • frontend/tests-e2e/ri-exchange-confirmation.spec.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

IAmJSD and others added 4 commits October 24, 2026 17:13
Conflicts:
- frontend/src/riexchange.ts (renderExchangeHistory row): main (LeanerCloud#284)
  renders an empty payment_due as a dash instead of a lone "$"; this
  branch renders the status cell with statusBadgeHtml() instead of the
  removed getStatusBadgeClass(). Combined: main's payment_due cell plus
  this branch's shared status badge.
- frontend/src/styles/components.css: main (LeanerCloud#318) added the
  .upfront-in-monthly / .upfront-in-monthly-note rules right above the
  "Purchase detail status badges" block that this branch deletes. Kept
  main's new rules and dropped the .status-badge.* block (nothing renders
  .status-badge any more; css.test.ts pins its absence).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- frontend/src/plans.ts: imports kept both (status-badge + main's
  purchase-row-guards). Planned-purchase status cell keeps main's LeanerCloud#181
  layout (.status-badges wrapper, Overdue badge, .overdue-hint) with this
  branch's statusBadgeHtml(status) as the status pill; the Overdue badge is
  now badgeHtml('danger', 'Overdue', ...) instead of the retired
  .status-badge class.
- frontend/src/riexchange.ts: imports kept both (skeletonText from main +
  statusBadgeHtml). History status cell uses statusBadgeHtml(rec.status)
  followed by main's LeanerCloud#1348 error line (.status-detail).
- frontend/src/history.ts: imports kept both (status-badge + main's
  local-date).

Semantic fixes in this merge:
- __tests__/riexchange-audit-321.test.ts (main): expects the shared
  .badge.badge-danger with "Failed" instead of .status-badge "failed".
- __tests__/css.test.ts: the retired-variant check is now /\.status-badge\b/
  so main's LeanerCloud#181 .status-badges wrapper rule does not trip it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- frontend/src/riexchange.ts: import block. Main (LeanerCloud#1333) replaced the
  skeletonText import with showSkeletonText; this branch added the
  statusBadgeHtml import. Kept main's skeleton import and this branch's
  status-badge import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD and others added 2 commits October 24, 2026 22:16
Conflicts:
- frontend/src/styles/components.css: main (LeanerCloud#1378 typography tokens)
  only swapped literal font weights for --cudly-fw-* tokens inside the
  .status-badge and .badge-admin/-user/-readonly rules, which this branch
  removes; kept the removal. In .badge kept this branch's
  line-height: var(--cudly-lh-tight) and white-space: nowrap together with
  main's tokenized font weight and radius.
- frontend/src/styles/tables.css: main only tokenized the font weight of
  the planned-purchases Bootstrap badge palette, which this branch
  replaces with the shared .badge tones; kept this branch's
  .planned-purchase-row.status-paused dimming rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE dd38ddafa4c27bc290200012256054b65d832530

Blocking:

  • none
    Non-blocking:
  • frontend/src/history.ts:608: the local statusBadgeHTML and the imported statusBadgeHtml (line 25) differ only in letter case. Someone editing line 1380 can easily call the wrong one and skip the lifecycle fold (e.g. "approved" would then show green "Approved" instead of amber "In Progress"). Rename the local one (historyStatusBadgeHtml) or inline it.
  • frontend/src/plans.ts:820: the plan-card Overdue badge is still a hand-written <span class="badge badge-danger" ...> literal, while the planned-purchase Overdue at :445 uses badgeHtml('danger', ...). It renders the same, but the two are inconsistent.
  • frontend/src/modules/registrations.ts:48: createStatusBadge now title-cases the label and shows unknown statuses as muted (before, they were danger). No unit test covers it. registrations.test.ts only tests the filter dropdown.
  • frontend/src/plans.ts:247: the Status column filter popover lists raw values ("pending", "paused"), but cells now read "Pending"/"Paused". This mismatch existed before (CSS text-transform: capitalize), so it's not a regression. statusLabel() could label the popover.
  • frontend/src/styles/settings.css:442: .provider-disabled-banner keeps its yellow border #ffecb5 on the new orange --cudly-warn-bg.
  • Retoning .badge-warning to the warn tokens also changes non-status badges that aren't in the screenshots: API key "expired" (apikeys.ts:134), user MFA "Disabled" (userList.ts:124), settings (settings.ts:625). This is intended, but reviewers should know about it.
    Attacked: Read the full git diff origin/main...HEAD (merge-base == main 049c617 == dd38dda^2, so the 2-dot and 3-dot diffs are identical). Checked that every removed line is either the intended .status-badge/.badge-admin/-user/-readonly/planned-purchases palette or a literal swapped for a token. Main's fix(frontend): RI-exchange cost chips and totals hardcode "$" and discard currency_code #284 payment_due dash, fix(frontend): show RI exchange failure reasons, stop double-escaping #1348 .status-detail error line, fix(frontend): impossible/placeholder values surfaced for scheduled and pending purchases #181 .status-badges wrapper + Overdue + .overdue-hint, fix(frontend): use skeletons for loading and add a slow-response note #1333 showSkeletonText, chore(frontend): add typography foundation for text, headings, controls #1378 fw tokens on .badge and chore(frontend): low money-path findings from the 2026-09-02 audit #318 upfront rules all survive. history.ts's lifecycle-to-label/tone mapping matches main's literals case by case. Escaping: badgeHtml escapes the label and title, riexchange/plans pass raw status only through statusBadgeHtml, and registrations uses textContent. Grepped src for any remaining .status-badge, status- or badge-* users: none lack a rule. All tokens exist in base.css. Computed warn and success contrast at about 4.58:1 and 4.56:1. The css.test /\.status-badge\b/ regex correctly ignores .status-badges. Ran 24 affected jest suites (746 pass) and tsc in a scratch worktree. No follow-up commits besides the 6 merges; the 6603f78 semantic test edits are correct.

@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Local verification at dd38ddaf merged onto current main (d82f0dc2; merge-tree 2272d080; Linux, Node, headless Chromium):

  • This branch's own checks passed at this head: lint, typecheck, build, the full jest suite, the full Playwright e2e suite, and the git-secrets checks (see Verification in the PR body).
  • The combined tree of every UI PR in this merge wave (tree 22a093f5, main plus 5 PRs) passes npm ci, lint, typecheck, build, jest (1 skipped, 4374 passed, 4375 total), and the full Playwright suite (259 passed).
  • CI is green on dd38ddaf (CI Success, Run pre-commit hooks).
  • The independent review above approves this exact head.
  • Nothing was applied to any cloud.

@IAmJSD
IAmJSD marked this pull request as ready for review October 24, 2026 22:57
@IAmJSD
IAmJSD merged commit 6cc5e35 into LeanerCloud:main Oct 24, 2026
29 checks passed
@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Marked ready and merged by IAmJSD's agent, at IAmJSD's request (IAmJSD reviewed the before/after screenshots; cristim is traveling), after the independent review and local verification above, all at dd38ddaf.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(frontend): status badges use four shapes and conflicting colors per status

1 participant