From 0a27393b53071702d1f45beaf58dc04e25f99583 Mon Sep 17 00:00:00 2001 From: Zbigniew Sobiecki Date: Tue, 23 Jun 2026 20:10:40 +0000 Subject: [PATCH] fix(web): overhaul dark-mode contrast in dashboard tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 → ; 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) Claude-Session: https://claude.ai/code/session_01RyBTx5JozjbpUko5SRyz4X --- .../components/global/organizations-table.tsx | 55 +++++--- .../projects/project-work-table.tsx | 76 +++++----- web/src/components/runs/run-status-badge.tsx | 2 +- web/src/components/runs/runs-table.tsx | 132 ++++++++---------- .../components/runs/work-item-runs-table.tsx | 83 +++++------ .../webhooklogs/webhooklogs-table.tsx | 93 ++++++------ web/src/index.css | 10 +- 7 files changed, 238 insertions(+), 213 deletions(-) diff --git a/web/src/components/global/organizations-table.tsx b/web/src/components/global/organizations-table.tsx index 1ccd33730..e48eb023a 100644 --- a/web/src/components/global/organizations-table.tsx +++ b/web/src/components/global/organizations-table.tsx @@ -1,4 +1,12 @@ import { Edit2 } from 'lucide-react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table.js'; interface Organization { id: string; @@ -12,28 +20,31 @@ interface OrganizationsTableProps { export function OrganizationsTable({ organizations, onEdit }: OrganizationsTableProps) { return ( -
-
- - - - - - - - +
+
IDNameActions
+ + + ID + Name + Actions + + + {organizations.length === 0 && ( - - - + + )} {organizations.map((org) => ( - - - - - + + ))} - -
+ + No organizations found -
{org.id}{org.name} + + {org.id} + {org.name} + -
+ + ); } diff --git a/web/src/components/projects/project-work-table.tsx b/web/src/components/projects/project-work-table.tsx index 1389bc370..b6f9e7059 100644 --- a/web/src/components/projects/project-work-table.tsx +++ b/web/src/components/projects/project-work-table.tsx @@ -1,5 +1,13 @@ import { useNavigate } from '@tanstack/react-router'; import { ClipboardList, ExternalLink, GitPullRequest } from 'lucide-react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table.js'; import { agentTypeLabel } from '@/lib/chart-colors.js'; import { useChartColors } from '@/lib/use-chart-colors.js'; import { formatCostSummary } from '@/lib/utils.js'; @@ -179,47 +187,46 @@ function WorkItemRow({ item, projectId, projectAvgDurationMs }: WorkItemRowProps }; return ( - {/* Type icon */} - + - + {/* PR title / number + Associated work item (stacked) */} - +
- +
{/* Duration bar */} - + - + {/* Run count */} - + {canNavigate ? ( {item.runCount} ) : ( item.runCount )} - + {/* Cost */} - + {formatCostSummary(item.totalCostUsd)} - - + + ); } @@ -269,28 +276,27 @@ export function ProjectWorkTable({ )} -
- - - - - - - - - - +
+
- - Title / Associated Item - - Duration - RunsCost
+ + + + Title / Associated Item + Duration + Runs + Cost + + + {pageItems.length === 0 && ( - - - + + )} {pageItems.map((item) => ( ))} - -
+ + No work found for this project -
+ +
{total > limit && ( diff --git a/web/src/components/runs/run-status-badge.tsx b/web/src/components/runs/run-status-badge.tsx index ccefd12e5..327b7558b 100644 --- a/web/src/components/runs/run-status-badge.tsx +++ b/web/src/components/runs/run-status-badge.tsx @@ -12,7 +12,7 @@ export function RunStatusBadge({ status }: { status: string }) { {status === 'timed_out' ? 'timed out' : status} diff --git a/web/src/components/runs/runs-table.tsx b/web/src/components/runs/runs-table.tsx index ab03823be..f8988cdac 100644 --- a/web/src/components/runs/runs-table.tsx +++ b/web/src/components/runs/runs-table.tsx @@ -1,5 +1,14 @@ import { Link } from '@tanstack/react-router'; import { Activity, ExternalLink } from 'lucide-react'; +import { Badge } from '@/components/ui/badge.js'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table.js'; import { formatCost, formatRelativeTime } from '@/lib/utils.js'; import { CancelRunButton } from './cancel-run-button.js'; import { LiveDuration } from './live-duration.js'; @@ -46,45 +55,33 @@ export function RunsTable({ return (
-
- - - - +
+
Agent
+ + + Agent {showOrg && ( - + Organization )} - - - - - - - - - - - - + + PR + Actions + + + {runs.length === 0 && ( - - - + + )} {runs.map((run) => ( - - + {showOrg && ( - + )} - - - - - - - - - + + ))} - -
- Organization - - Project - - Work Item - StatusStarted - Duration - - Cost - + Project + Work Item + Status + Started + Duration + Cost + Iterations - - PR - Actions
+
@@ -93,15 +90,12 @@ export function RunsTable({ Runs appear here when CASCADE processes work items.

-
+ + {run.agentType} - + {run.orgName ?? '-'} - + {run.projectName ?? '-'} - + + {run.workItemUrl && run.workItemTitle ? ( + + - + + {formatRelativeTime(run.startedAt)} - + + - + + {formatCost(run.costUsd)} - + + {run.llmIterations ?? '-'} - + + {run.prUrl ? ( + + -
+ +
{total > limit && ( diff --git a/web/src/components/runs/work-item-runs-table.tsx b/web/src/components/runs/work-item-runs-table.tsx index 9f49baca8..4e6fc1827 100644 --- a/web/src/components/runs/work-item-runs-table.tsx +++ b/web/src/components/runs/work-item-runs-table.tsx @@ -1,4 +1,12 @@ import { Link } from '@tanstack/react-router'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table.js'; import { formatCost, formatRelativeTime } from '@/lib/utils.js'; import { CancelRunButton } from './cancel-run-button.js'; import { LiveDuration } from './live-duration.js'; @@ -42,28 +50,25 @@ export function WorkItemRunsTable({ runs, isLoading, isError, error }: WorkItemR } return ( -
- - - - - - - - - - - - - - - +
+
AgentEngineModelStatusStartedDurationCostItersActions
+ + + Agent + Engine + Model + Status + Started + Duration + Cost + Iters + Actions + + + {runs.map((run) => ( - - - - - - - - - - - + + ))} - -
+ + {run.agentType} - {run.engine}{run.model ?? '-'} + + {run.engine} + {run.model ?? '-'} + - + + {formatRelativeTime(run.startedAt)} - + + - + + {formatCost(run.costUsd)} - + + {run.llmIterations ?? '-'} - + + -
+ +
); } diff --git a/web/src/components/webhooklogs/webhooklogs-table.tsx b/web/src/components/webhooklogs/webhooklogs-table.tsx index 114947b3c..7e8af7eb5 100644 --- a/web/src/components/webhooklogs/webhooklogs-table.tsx +++ b/web/src/components/webhooklogs/webhooklogs-table.tsx @@ -1,4 +1,12 @@ import { Badge } from '@/components/ui/badge.js'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table.js'; import { formatRelativeTime } from '@/lib/utils.js'; interface WebhookLog { @@ -53,48 +61,49 @@ export function WebhookLogsTable({ return (
-
- - - - - - - - - - - - - +
+
SourceEvent Type - Method - - Status - Processed - Reason - Time
+ + + Source + Event Type + Method + Status + Processed + Reason + Time + + + {logs.length === 0 && ( - - - + + )} {logs.map((log) => ( - onRowClick(log.id)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') onRowClick(log.id); }} > - - - - - - - + + ))} - -
+ + No webhook logs found -
+ - {log.eventType ?? '-'}{log.method} + + + {log.eventType ?? '-'} + + + {log.method} + + {log.statusCode != null ? ( - + + {log.processed ? ( Yes @@ -119,20 +128,20 @@ export function WebhookLogsTable({ No )} - + {log.decisionReason ?? '-'} - + + {formatRelativeTime(log.receivedAt)} -
+ +
{total > limit && ( diff --git a/web/src/index.css b/web/src/index.css index b0968a3c4..e9c5b9571 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -20,7 +20,8 @@ --color-accent: oklch(0.97 0 0); --color-accent-foreground: oklch(0.205 0 0); --color-destructive: oklch(0.577 0.245 27.325); - --color-destructive-foreground: oklch(0.577 0.245 27.325); + /* Light neutral so text on bg-destructive is legible (was red-on-red). */ + --color-destructive-foreground: oklch(0.985 0 0); --color-border: oklch(0.922 0 0); --color-input: oklch(0.922 0 0); --color-ring: oklch(0.708 0 0); @@ -37,6 +38,7 @@ --color-sidebar-accent-foreground: oklch(0.205 0 0); --color-sidebar-border: oklch(0.922 0 0); --color-sidebar-ring: oklch(0.708 0 0); + --radius: 0.625rem; --radius-sm: 0.25rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; @@ -55,11 +57,13 @@ --color-secondary: oklch(0.269 0 0); --color-secondary-foreground: oklch(0.985 0 0); --color-muted: oklch(0.269 0 0); - --color-muted-foreground: oklch(0.708 0 0); + /* Lifted from 0.708 for legible secondary text on dark surfaces. */ + --color-muted-foreground: oklch(0.75 0 0); --color-accent: oklch(0.269 0 0); --color-accent-foreground: oklch(0.985 0 0); --color-destructive: oklch(0.396 0.141 25.723); - --color-destructive-foreground: oklch(0.637 0.237 25.331); + /* Light neutral so text on bg-destructive is legible (was red-on-red). */ + --color-destructive-foreground: oklch(0.985 0 0); --color-border: oklch(0.269 0 0); --color-input: oklch(0.269 0 0); --color-ring: oklch(0.439 0 0);