fix(insights): point derived score-band tokens at the AA-corrected semantic colours

The band primitives (score ring, coverage dots, contributor bars,
provenance chips + standard link, sparkline tint, trend arrow/delta)
painted straight from the raw `--dracula-*` variables, which carry no
`:root.light` override. On the white card the Alucard light theme uses,
the green/orange/yellow cleared roughly 1.1-1.5:1 — under AA — and the
`bg-dracula-yellow` confidence dot read near-invisible. The file comment
claimed they 'track automatically'; they did not.

Point BAND_NUMBER/PROGRESS/BORDER/VAR, the coverage-meter dot tint, the
provenance cyan chips, the sparkline stroke, and sentimentColorClass at
the semantic `--success`/`--warning`/`--destructive`/`--info` tokens
instead. Those alias the same Dracula tones in dark mode (pixel-identical
there, the primary target) but carry the darker AA-safe values under
`:root.light`. Clamp the score-ring provisional caption to one line so a
longer localised string cannot wrap over the ring arc.
This commit is contained in:
Marc-André Bombeck committed 2026-06-03 00:04:56 +02:00
1 parent ecf93317dd
commit 60c0ba4483
6 files changed
+54 -29

No files matched your search

+27 -17
View File
@@ -5,44 +5,54 @@
* exact same band colour grammar the shipped `health-score-card` already
* uses, without each component redeclaring the map (concept cohesion).
*
* No new tokens — these point at the existing `--dracula-*` variables in
* `globals.css`. A green/yellow/red band is the one semantic the whole
* v1.10 score language extends.
* These point at the *semantic* feedback tokens (`--success` / `--warning`
* / `--destructive`), NOT the raw `--dracula-*` primitives. That matters
* for contrast: `:root.light` (Alucard) overrides the semantic tokens to
* AA-safe darker tones (`--success: #14720a`, `--warning: #a34d14`,
* `--destructive: #cb3a2a`) precisely because straight Dracula
* green/orange on a white `--card` clears only ~1.1–1.5:1 — under AA.
* Painting the band off the semantic token means every score surface
* tracks that light-mode correction automatically, in both themes; the
* raw `--dracula-*` primitives do NOT carry a light override. A
* green/yellow/red band is the one semantic the whole v1.10 score
* language extends.
*/
export type ScoreBand = "green" | "yellow" | "red";
/** Tailwind text-colour class per band (the headline number colour). */
export const BAND_NUMBER_CLASS: Record<ScoreBand, string> = {
green: "text-dracula-green",
yellow: "text-dracula-orange",
red: "text-dracula-red",
green: "text-success",
yellow: "text-warning",
red: "text-destructive",
};
/** Tailwind background class per band (progress/fill bars). */
export const BAND_PROGRESS_CLASS: Record<ScoreBand, string> = {
green: "bg-dracula-green",
yellow: "bg-dracula-orange",
red: "bg-dracula-red",
green: "bg-success",
yellow: "bg-warning",
red: "bg-destructive",
};
/** Tailwind border-colour class per band (card / chip outlines). */
export const BAND_BORDER_CLASS: Record<ScoreBand, string> = {
green: "border-dracula-green/40",
yellow: "border-dracula-orange/40",
red: "border-dracula-red/40",
green: "border-success/40",
yellow: "border-warning/40",
red: "border-destructive/40",
};
/**
* The raw CSS variable per band — used where a Recharts `fill` needs an
* actual colour string rather than a Tailwind class (the score ring's
* `<RadialBar fill>`). Reads the same `--dracula-*` tokens so dark and
* Alucard light modes track automatically.
* `<RadialBar fill>`). Reads the semantic tokens so dark and Alucard light
* modes track the AA-safe light overrides automatically (the
* `--dracula-*` primitives carry no light override and would fail AA on
* the white card).
*/
export const BAND_VAR: Record<ScoreBand, string> = {
green: "var(--dracula-green)",
yellow: "var(--dracula-orange)",
red: "var(--dracula-red)",
green: "var(--success)",
yellow: "var(--warning)",
red: "var(--destructive)",
};
/**
@@ -48,12 +48,21 @@ export interface CoverageMeterProps {
className?: string;
}
/** Dot tint per confidence band — reuses the existing dracula vocabulary. */
/**
* Dot tint per confidence band. Points at the *semantic* feedback tokens
* (`--success` / `--warning` / `--destructive`), not the raw `--dracula-*`
* primitives, so the dots track the AA-safe `:root.light` overrides on the
* white card — straight `bg-dracula-yellow` cleared barely 1.1:1 on white
* and read near-invisible. The four bands collapse onto the three semantic
* severities (medium + low both ride the caution `--warning`); the lit-dot
* count + the percent label carry the medium↔low distinction, never colour
* alone.
*/
const BAND_DOT_CLASS: Record<DerivedConfidenceBand, string> = {
high: "bg-dracula-green",
medium: "bg-dracula-yellow",
low: "bg-dracula-orange",
draft: "bg-dracula-red",
high: "bg-success",
medium: "bg-warning",
low: "bg-warning",
draft: "bg-destructive",
};
function clampInt(value: number, min: number, max: number): number {
@@ -100,7 +100,7 @@ export function ProvenanceExplainer({
<li
key={input}
data-slot="provenance-explainer-input-chip"
className="border-dracula-cyan/30 text-dracula-cyan/90 inline-flex items-center rounded-full border bg-transparent px-1.5 py-0.5 text-[10px] leading-none"
className="border-info/40 text-info inline-flex items-center rounded-full border bg-transparent px-1.5 py-0.5 text-[10px] leading-none"
>
{input}
</li>
@@ -141,7 +141,7 @@ export function ProvenanceExplainer({
href={standard.url}
target="_blank"
rel="noopener noreferrer"
className="text-dracula-cyan hover:text-dracula-cyan/80 focus-visible:ring-ring/50 inline-flex items-center gap-1 rounded text-[11px] font-medium focus-visible:ring-2 focus-visible:outline-none"
className="text-info hover:text-info/80 focus-visible:ring-ring/50 inline-flex items-center gap-1 rounded text-[11px] font-medium focus-visible:ring-2 focus-visible:outline-none"
>
{t("insights.derived.provenance.standardLink", {
name: standard.name,
@@ -188,7 +188,7 @@ export function ScoreRing({
{!hasScore && (
<span
data-slot="score-ring-provisional"
className="text-muted-foreground absolute inset-x-0 bottom-0 text-center text-[10px] leading-tight"
className="text-muted-foreground absolute inset-x-0 bottom-0 line-clamp-1 px-1 text-center text-[10px] leading-tight"
>
{t("insights.derived.scoreRing.provisionalCaption")}
</span>
@@ -94,11 +94,13 @@ export function SparklineDeltaTile({
// The sparkline tints with the trend sentiment so it reads "same signal"
// as the arrow; neutral metrics ride the muted-foreground line.
// Semantic tokens, not raw --dracula-*, so the sparkline tint tracks the
// AA-safe :root.light overrides on the white card (the band-token fix).
const strokeVar =
arrowSentiment === "positive"
? "var(--dracula-green)"
? "var(--success)"
: arrowSentiment === "negative"
? "var(--dracula-orange)"
? "var(--warning)"
: "var(--muted-foreground)";
const fillId = `spark-${label.replace(/[^a-zA-Z0-9]/g, "")}`;
+6 -2
View File
@@ -44,8 +44,12 @@ export function getTrendSentiment(
export function sentimentColorClass(
direction: TrendSentimentDirection,
): string {
if (direction === "positive") return "text-dracula-green";
if (direction === "negative") return "text-dracula-orange";
// Semantic tokens, not raw --dracula-*: `--success` / `--warning` alias
// the same Dracula green/orange in dark mode (pixel-identical there) but
// carry the AA-safe `:root.light` overrides, so the trend arrow + delta
// clear AA on the white card instead of the ~1.2:1 the raw primitives hit.
if (direction === "positive") return "text-success";
if (direction === "negative") return "text-warning";
return "text-muted-foreground";
}