Skip to content

fix(web): overhaul dark-mode contrast in dashboard tables#1436

Merged
zbigniewsobiecki merged 1 commit into
devfrom
fix/dark-mode-contrast
Jun 23, 2026
Merged

fix(web): overhaul dark-mode contrast in dashboard tables#1436
zbigniewsobiecki merged 1 commit into
devfrom
fix/dark-mode-contrast

Conversation

@zbigniewsobiecki

Copy link
Copy Markdown
Member

Problem

Dark mode reads as "pretty terrible": the Global Runs table header text is dark-gray on dark-gray (nearly invisible), secondary cells are dim, the "Unlinked" pill is low-contrast, and destructive action buttons render red-text-on-red-background.

Root cause is not missing dark: variants (an audit grep found zero) — it's token-choice in 5 raw <table> components plus two bad dark token values.

Changes

Tokens — web/src/index.css

  • Lift dark --color-muted-foreground 0.708 → 0.75 (legible secondary text).
  • Fix --color-destructive-foreground (both themes): was a saturated red on the red destructive bg (red-on-red) → near-white oklch(0.985). Un-breaks the Cancel/Delete buttons (bg-destructive text-destructive-foreground).
  • Define --radius (referenced by sonner.tsx, never declared).

Migrate 5 raw tables → shadcn Table primitive (runs-table, work-item-runs-table, project-work-table, organizations-table, webhooklogs-table). The primitive's TableHead uses text-foreground instead of the dim text-muted-foreground on bg-muted/50the invisible-header bug — and prevents any new raw table from reintroducing it. Behavior preserved: clickable rows, responsive hidden md:table-cell columns, alignment, tabular-nums, links, and title wrapping (whitespace-normal where the primitive's default nowrap differs).

Badges: "Unlinked" pill → <Badge variant="outline">; status-badge unknown fallback brightened to dark:bg-gray-700 dark:text-gray-100.

Verification

tsc -b ✅ · biome check ✅ · vite build ✅ · 562 web unit tests ✅ · full pre-push suite ✅. Visual confirmation to follow on the deployed dashboard (dark-mode Global Runs).

🤖 Generated with Claude Code

Dark mode read as "pretty terrible": the Global Runs table header text was
dark-gray on dark-gray (nearly invisible), secondary cells were dim, the
"Unlinked" pill was low-contrast, and destructive action buttons rendered
red-text-on-red-background.

Root cause was NOT missing `dark:` variants (a grep found zero) — it was
token-choice in 5 raw <table> components plus two bad dark token values.

Tokens (web/src/index.css):
- Lift dark --color-muted-foreground 0.708 → 0.75 for legible secondary text.
- Fix --color-destructive-foreground in both themes (was a saturated red on the
  red destructive bg → red-on-red) to near-white oklch(0.985). This un-breaks
  the Cancel/Delete buttons that use `bg-destructive text-destructive-foreground`.
- Define --radius (referenced by sonner.tsx but never declared).

Migrate the 5 raw tables to the shadcn Table primitive (runs-table,
work-item-runs-table, project-work-table, organizations-table,
webhooklogs-table). Its TableHead uses text-foreground instead of the dim
text-muted-foreground on bg-muted/50 — the invisible-header bug — and prevents
any new raw table from reintroducing it. Behavior preserved: clickable rows,
responsive hidden md:table-cell columns, alignment, tabular-nums, links, and
title wrapping (whitespace-normal where the primitive's default nowrap differs).

Badges: "Unlinked" pill → <Badge variant="outline">; status-badge unknown
fallback brightened to dark:bg-gray-700 dark:text-gray-100.

Verified: tsc -b, biome, vite build, and 562 web unit tests all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RyBTx5JozjbpUko5SRyz4X
@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@nhopeatall nhopeatall left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Well-scoped, correct dark-mode contrast fix. I traced the risky parts of the migration and they hold up; one gap worth a follow-up — a 6th raw table with the identical invisible-header pattern was missed.

Verified (no issues)

  • Dropping the outer overflow-x-auto is safe — the Table primitive wraps its <table> in <div className="relative w-full overflow-x-auto"> (web/src/components/ui/table.tsx:9), so horizontal scroll on narrow viewports is preserved for the wide tables (e.g. runs-table's 11 columns).
  • whitespace-normal overrides land correctlycn() is twMerge(clsx(...)) (web/src/lib/utils.ts), so the explicit whitespace-normal on title / empty-state cells properly beats the primitive's default whitespace-nowrap.
  • The header fix is genuine — the primitive's TableHead uses text-foreground (not text-muted-foreground), and [&_tr:last-child]:border-0 on the primitive TableBody preserves work-item-runs-table's old last:border-0 behavior.
  • Token changes check out--color-destructive-foreground was === --color-destructive in both themes (red-on-red); the token is only ever consumed as text-destructive-foreground on bg-destructive (4 call sites, no bg/border usage), so near-white is correct for both themes. --radius is genuinely referenced by sonner.tsx:30 and was previously undefined. runs-table empty-state colSpan (10 / 11) matches the column count.

Should Fix

  • web/src/components/llm-calls/llm-call-list.tsx:211-226 — a 6th raw <table> (the LLM-calls table on the run-detail page /runs/$runId) still has the exact pattern this PR is eliminating: text-muted-foreground <th>s inside <tr className="... bg-muted/50">. It wasn't migrated, so its header stays dim-gray-on-dark-gray in dark mode while the 5 migrated tables now render text-foreground headers — leaving the run-detail page exhibiting the very bug the PR fixes elsewhere, plus a visible header-contrast inconsistency between tables. The dark --muted-foreground lift (0.708 → 0.75) only marginally helps it. Recommend migrating it to the Table primitive too (or at minimum giving its <th>s text-foreground) so the fix — and the PR's "prevents any new raw table from reintroducing it" claim — is actually complete.

Non-blocking: the diff itself is correct and a clear net improvement.

🕵️ claude-code · claude-opus-4-8 · run details

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants