Skip to content

Commit 7dae4cc

Browse files
samejrclaude
andcommitted
fix(webapp): stop the Colors tables repeating every theme inside every theme
The measured tables sit inside an Audit, which renders its children once per theme - and the table was building the five themes a second time internally. The result was twenty-five mode blocks per table, each one a full Token / Fill / Ratio / Verdict set, which made the section unreadable. The table is back to one mode: token details, then Fill, Ratio and Verdict. The themes come only from the Audit columns around it. That works without the table knowing anything about themes, because each column is already a `data-theme` context and the ratios are measured off the DOM, so the same table reports that column's answer. Audit takes a column width now, since a block holding four sub-columns needs more room than one holding a single sample. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 10a1a0a commit 7dae4cc

1 file changed

Lines changed: 69 additions & 108 deletions

File tree

  • apps/webapp/app/routes/storybook.colors

apps/webapp/app/routes/storybook.colors/route.tsx

Lines changed: 69 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import type {
2424
WorkerDeploymentStatus,
2525
} from "@trigger.dev/database";
2626
import type { WaitpointTokenStatus } from "@trigger.dev/core/v3";
27-
import { Fragment, type ReactNode, useEffect, useRef, useState } from "react";
27+
import { type ReactNode, useEffect, useRef, useState } from "react";
2828
import { BatchesIcon } from "~/assets/icons/BatchesIcon";
2929
import { ClockIcon } from "~/assets/icons/ClockIcon";
3030
import { DeploymentsIcon } from "~/assets/icons/DeploymentsIcon";
@@ -116,7 +116,7 @@ type ThemeColumn = (typeof THEME_COLUMNS)[number];
116116
* crushing five columns of live UI into slivers. */
117117
const THEME_COLUMN_MIN = "13rem";
118118

119-
const THEME_GRID_TEMPLATE = `repeat(5, minmax(${THEME_COLUMN_MIN}, 1fr))`;
119+
const themeGridTemplate = (min: string) => `repeat(5, minmax(${min}, 1fr))`;
120120

121121
/**
122122
* A cell rendered in one theme's context. The four fixed columns restate the
@@ -167,11 +167,14 @@ function Audit({
167167
title,
168168
where,
169169
note,
170+
columnMin = THEME_COLUMN_MIN,
170171
children,
171172
}: {
172173
title: string;
173174
where: string[];
174175
note: ReactNode;
176+
/** Widen the columns for blocks holding a table rather than a single sample. */
177+
columnMin?: string;
175178
children: ReactNode;
176179
}) {
177180
return (
@@ -191,13 +194,13 @@ function Audit({
191194
strip counts as a child and pushes a stray rule onto the first cell. */}
192195
<div
193196
className="grid border-b border-grid-dimmed bg-background-dimmed [&>span]:px-3 [&>span]:py-1"
194-
style={{ gridTemplateColumns: THEME_GRID_TEMPLATE }}
197+
style={{ gridTemplateColumns: themeGridTemplate(columnMin) }}
195198
>
196199
<ThemeColumnLabels />
197200
</div>
198201
<div
199202
className="grid divide-x divide-grid-dimmed"
200-
style={{ gridTemplateColumns: THEME_GRID_TEMPLATE }}
203+
style={{ gridTemplateColumns: themeGridTemplate(columnMin) }}
201204
>
202205
{THEME_COLUMNS.map((column) => (
203206
<ThemeCell key={column.key} column={column} className="p-3">
@@ -268,82 +271,76 @@ type ContrastEntry = {
268271
};
269272

270273
/** Name+usage, fill, ratio, verdict - wide enough that "passes 4.5:1" doesn't wrap. */
271-
/* Token details on the left, then Fill / Ratio / Verdict repeated for each of
272-
the four themes and once more for the preference - sixteen columns in one flat
273-
grid, so a sub-column lines up with its header all the way down. Wide by
274-
design: the whole point is one token against every background at once, so the
275-
table scrolls sideways rather than compressing. */
276-
const CONTRAST_TOKEN_COL = "minmax(12rem, 1.2fr)";
277-
/** Fill, Ratio, Verdict. Verdict is widest - it carries "passes 4.5:1". */
278-
const CONTRAST_SUB_COLS = "2.25rem 3.5rem 4.25rem";
279-
const CONTRAST_GRID_TEMPLATE = `${CONTRAST_TOKEN_COL} repeat(5, ${CONTRAST_SUB_COLS})`;
280-
281-
/** Left edge of each theme group, so the three readings read as one band. */
282-
const THEME_GROUP_EDGE = "border-l border-grid-dimmed";
274+
/* One mode's readings: token details, then Fill / Ratio / Verdict. The five
275+
themes come from the Audit columns wrapping this, not from the table - each
276+
column is already a `data-theme` context, so the same table measured inside it
277+
reports that theme's answer. Building the themes in here as well is what
278+
produced twenty-five mode blocks per table. */
279+
const CONTRAST_GRID = "grid grid-cols-[minmax(0,1fr)_2.5rem_3.5rem_4.75rem]";
280+
281+
/** Wider than a component column: each of these holds four sub-columns. */
282+
const CONTRAST_COLUMN_MIN = "22rem";
283283

284284
/**
285-
* The three readings for one token in one theme.
286-
*
287-
* `display: contents` on the themed wrapper is doing the work here: it puts
288-
* `data-theme` in the ancestor chain - so the tokens, the `dark:` variant and
289-
* the preference all resolve to that theme - while leaving the three cells as
290-
* direct grid items of the row, which is what keeps them aligned under their
291-
* headers. A wrapper with a box would nest them one level down and break the
292-
* grid.
285+
* One measured swatch. The ratio is the sample's own computed color against the
286+
* surface behind it, read off the DOM - so it follows the theme of whichever
287+
* column it lands in, the contrast slider, and the preference, with no table of
288+
* hard-coded values to fall out of date.
293289
*/
294-
function ContrastCells({ entry, column }: { entry: ContrastEntry; column: ThemeColumn }) {
295-
const fillRef = useRef<HTMLDivElement>(null);
290+
function ContrastRow({ entry }: { entry: ContrastEntry }) {
291+
const sampleRef = useRef<HTMLSpanElement>(null);
296292
const revision = useThemeRevision();
297-
const documentTheme = useDocumentTheme();
298293
const [ratio, setRatio] = useState<number | null>(null);
299294

300295
useEffect(() => {
301-
if (!fillRef.current) return;
302-
setRatio(measureTextContrast(fillRef.current));
296+
if (!sampleRef.current) return;
297+
setRatio(measureTextContrast(sampleRef.current));
303298
}, [revision, entry.token, entry.className]);
304299

300+
const name = entry.token ? entry.token.replace("--color-", "") : entry.className;
301+
const style = entry.token ? { color: `var(${entry.token})` } : undefined;
305302
/* Under the 3:1 floor the value is unusable for anything, text or not - call
306303
it out in bold red so the failures pull the eye down a long table. The
307304
verdict words carry the same information, so the red is reinforcement
308305
rather than the signal. */
309306
const fails = ratio !== null && ratio < NON_TEXT_THRESHOLD;
310-
const cell = "flex items-center bg-background-bright py-1";
311307

312308
return (
313309
<div
314-
className="contents"
315-
data-theme={column.theme ?? documentTheme}
316-
data-icon-contrast={column.strongerColors ? "true" : "false"}
310+
className={cn(
311+
CONTRAST_GRID,
312+
"items-center gap-x-2 border-b border-grid-dimmed py-1 last:border-b-0"
313+
)}
317314
>
318-
{/* The fill block is also the measured sample: it carries the token as its
319-
own `color`, which is what the ratio reads, and as its background,
320-
which is the shape non-text contrast is judged on. For a raw utility
321-
the class sets `color`, so the fill comes from currentcolor instead. */}
322-
<div className={cn(cell, THEME_GROUP_EDGE, "px-1.5")}>
323-
<div
324-
ref={fillRef}
325-
className={cn("h-4 w-full rounded-xs border border-grid-bright", entry.className)}
326-
style={{
327-
color: entry.token ? `var(${entry.token})` : undefined,
328-
backgroundColor: entry.token ? `var(${entry.token})` : "currentcolor",
329-
}}
330-
/>
315+
{/* The name is the measured sample: rendered in the token's own color, it
316+
is the text whose ratio the row reports. */}
317+
<div className="min-w-0">
318+
<span
319+
ref={sampleRef}
320+
style={style}
321+
className={cn("block truncate font-mono text-xs", entry.className)}
322+
>
323+
{name}
324+
</span>
325+
<span className="block truncate text-xxs text-text-dimmed">{entry.usedBy}</span>
331326
</div>
327+
{/* The same value as a filled block - the shape non-text contrast is
328+
actually judged on. For a raw utility the class sets `color`, so the
329+
fill has to come from currentcolor on this same element. */}
332330
<div
331+
className={cn("h-5 rounded-xs border border-grid-bright", entry.className)}
332+
style={{ backgroundColor: entry.token ? `var(${entry.token})` : "currentcolor" }}
333+
/>
334+
<span
333335
className={cn(
334-
cell,
335-
"justify-end font-mono text-xs tabular-nums",
336+
"text-right font-mono text-xs tabular-nums",
336337
fails ? "font-bold text-error" : "text-text-bright"
337338
)}
338339
>
339340
{ratio === null ? "\u2014" : ratio.toFixed(2)}
340-
</div>
341-
<div
342-
className={cn(
343-
cell,
344-
"justify-end pr-2 text-xxs",
345-
fails ? "font-bold text-error" : "text-text-dimmed"
346-
)}
341+
</span>
342+
<span
343+
className={cn("text-right text-xxs", fails ? "font-bold text-error" : "text-text-dimmed")}
347344
>
348345
{ratio === null
349346
? ""
@@ -352,67 +349,25 @@ function ContrastCells({ entry, column }: { entry: ContrastEntry; column: ThemeC
352349
: ratio < TEXT_THRESHOLD
353350
? "3:1 only"
354351
: "passes 4.5:1"}
355-
</div>
352+
</span>
356353
</div>
357354
);
358355
}
359356

360-
function ContrastRow({ entry }: { entry: ContrastEntry }) {
361-
const name = entry.token ? entry.token.replace("--color-", "") : entry.className;
362-
363-
return (
364-
<div
365-
className="grid items-stretch border-b border-grid-dimmed last:border-b-0"
366-
style={{ gridTemplateColumns: CONTRAST_GRID_TEMPLATE }}
367-
>
368-
<div className="min-w-0 self-center py-1 pr-2">
369-
<span className="block truncate font-mono text-xs text-text-bright">{name}</span>
370-
<span className="block truncate text-xxs text-text-dimmed">{entry.usedBy}</span>
371-
</div>
372-
{THEME_COLUMNS.map((column) => (
373-
<ContrastCells key={column.key} entry={entry} column={column} />
374-
))}
375-
</div>
376-
);
377-
}
378-
379-
/** Two header rows on the same template: theme names spanning their three
380-
* readings, then the readings themselves. */
381-
function ContrastHeader() {
357+
function ContrastTable({ entries }: { entries: ContrastEntry[] }) {
382358
return (
383-
<div className="text-xxs uppercase text-text-dimmed">
384-
<div className="grid" style={{ gridTemplateColumns: CONTRAST_GRID_TEMPLATE }}>
385-
<span className="truncate pr-2">Token &amp; where it's used</span>
386-
{THEME_COLUMNS.map((column) => (
387-
<span
388-
key={column.key}
389-
className={cn("col-span-3 truncate px-1.5 text-text-bright", THEME_GROUP_EDGE)}
390-
>
391-
{column.label}
392-
</span>
393-
))}
394-
</div>
359+
<div>
395360
<div
396-
className="grid border-b border-grid-bright pb-1"
397-
style={{ gridTemplateColumns: CONTRAST_GRID_TEMPLATE }}
361+
className={cn(
362+
CONTRAST_GRID,
363+
"gap-x-2 border-b border-grid-bright pb-1 text-xxs uppercase text-text-dimmed"
364+
)}
398365
>
399-
<span />
400-
{THEME_COLUMNS.map((column) => (
401-
<Fragment key={column.key}>
402-
<span className={cn("truncate px-1.5", THEME_GROUP_EDGE)}>Fill</span>
403-
<span className="truncate text-right">Ratio</span>
404-
<span className="truncate pr-2 text-right">Verdict</span>
405-
</Fragment>
406-
))}
366+
<span className="truncate">Token &amp; where it's used</span>
367+
<span>Fill</span>
368+
<span className="text-right">Ratio</span>
369+
<span className="text-right">Verdict</span>
407370
</div>
408-
</div>
409-
);
410-
}
411-
412-
function ContrastTable({ entries }: { entries: ContrastEntry[] }) {
413-
return (
414-
<div className="overflow-x-auto">
415-
<ContrastHeader />
416371
{entries.map((entry) => (
417372
<ContrastRow key={entry.token ?? entry.className} entry={entry} />
418373
))}
@@ -836,6 +791,7 @@ export default function Story_() {
836791
>
837792
<StorySubSection title="Status & text tokens">
838793
<Audit
794+
columnMin={CONTRAST_COLUMN_MIN}
839795
title="Status and text accents"
840796
where={["tailwind.css"]}
841797
note="The four status tokens carry every run, deployment, batch, waitpoint and queue state in the app, plus the timeline and trace bars."
@@ -846,6 +802,7 @@ export default function Story_() {
846802

847803
<StorySubSection title="Environment tokens">
848804
<Audit
805+
columnMin={CONTRAST_COLUMN_MIN}
849806
title="Environment accents"
850807
where={["tailwind.css", "EnvironmentLabel.tsx"]}
851808
note="Staging is the one env token that still differs per mode. Preview and its branch labels are blue in every theme now, so they read the same at both ends of the `system` setting."
@@ -856,6 +813,7 @@ export default function Story_() {
856813

857814
<StorySubSection title="Navigation & section accents">
858815
<Audit
816+
columnMin={CONTRAST_COLUMN_MIN}
859817
title="Nav icon accents"
860818
where={["tailwind.css", "sideMenuSections.tsx", "favoritePages.tsx"]}
861819
note="29 accents, one per nav section. Several land on the same hue (metrics / regions / aiMetrics are all green; concurrency / errors / apiKeys are all amber), so the accent identifies a section only in combination with its icon."
@@ -866,6 +824,7 @@ export default function Story_() {
866824

867825
<StorySubSection title="Raw palette classes (not themed)">
868826
<Audit
827+
columnMin={CONTRAST_COLUMN_MIN}
869828
title="Palette utilities used directly in components"
870829
where={[
871830
"TaskRunStatus.tsx",
@@ -884,6 +843,7 @@ export default function Story_() {
884843

885844
<StorySubSection title="Run-status chart series">
886845
<Audit
846+
columnMin={CONTRAST_COLUMN_MIN}
887847
title="Run status chart colors"
888848
where={["tailwind.css", "TaskRunStatus.tsx"]}
889849
note="17 series on one chart, deliberately spaced within three families (blues, roses, charcoals). Judged at 3:1 against the plot surface — and, separately, against each other."
@@ -894,6 +854,7 @@ export default function Story_() {
894854

895855
<StorySubSection title="Callout accents">
896856
<Audit
857+
columnMin={CONTRAST_COLUMN_MIN}
897858
title="Callout text and icon accents"
898859
where={["Callout.tsx", "tailwind.css"]}
899860
note="Measured against this card, not the callout's own tint — the real ratio inside a callout is a little lower for the text tokens and a little higher for the icons."

0 commit comments

Comments
 (0)