fix(theme): text on glass holds 4.5:1 — light --muted kept at 6:1 on the page, tonal fills opaque

Pixel-measured over every scheme x mode x backdrop: the item count fell to 3.8:1 (dark too: its 8%
fill let the toolbar glass and the thumbnails under it through), Channels' tabs to 4.1:1 and the
current Files row to 4.4:1. Light --muted moves 15% toward --fg in every scheme; the badges, the item
count and the current rail row paint opaque bg-tint* fills mixed into the surface. The token contract
now asks 6:1 of --muted on the bare page and checks those three sites use the tints.
This commit is contained in:
2026-10-17 20:06:54 +02:00
parent d74f2a9ca1
commit 3ada34073b
6 changed files with 58 additions and 20 deletions
+5 -3
View File
@@ -7,10 +7,12 @@ import type { ReactNode } from "react";
// at 8%, so it reads on the dark schemes and on white alike; `onDark` is the always-dark player's. A
// `tone` says which kind of label it is (a role, a state to notice): its own colour at 6%, the most
// that keeps its text at 4.5:1 on light (theme.contract.test.ts measures all three in every scheme).
// Each tint is mixed into the surface (`bg-tint*`), opaque: a translucent one lets the glass and what
// scrolls under it through, and the text's contrast goes with them.
const TONES = {
neutral: "bg-fg/8 text-muted",
accent: "bg-accent/6 text-accent",
warn: "bg-warn/6 text-warn",
neutral: "bg-tint text-muted",
accent: "bg-tint-accent text-accent",
warn: "bg-tint-warn text-warn",
} as const;
export function Badge({
+3 -2
View File
@@ -8,7 +8,8 @@ import { formatNumber } from "../../lib/format";
const OVERFLOW_AT = 1_000_000;
// A compact, FIXED-WIDTH count pill for a toolbar: the filtered item count at the toolbar's 28, flat
// and tonal like a `Badge` — no edge, because it does nothing (edged = pressable, flat = just
// and tonal like a `Badge` (the same opaque `bg-tint`: the toolbar is glass over scrolling thumbnails)
// — no edge, because it does nothing (edged = pressable, flat = just
// says; it used to wear the filter dropdowns' glass pill and read as one more). The fixed width is the point — a
// changing number must never shove the controls beside it, so the box is sized to the widest value
// ("999,999 items") and the content is centred within it. A count of a million or more collapses to
@@ -46,7 +47,7 @@ export default function ItemCount({
failed ? t("common.countFailed") : count == null ? undefined : formatNumber(count, lang)
}
aria-busy={stale || undefined}
className="inline-flex h-7 min-w-[9rem] shrink-0 items-center justify-center gap-1.5 rounded-full bg-fg/8 px-3 text-sm whitespace-nowrap text-muted"
className="inline-flex h-7 min-w-[9rem] shrink-0 items-center justify-center gap-1.5 rounded-full bg-tint px-3 text-sm whitespace-nowrap text-muted"
>
<Layers className="h-3.5 w-3.5 shrink-0 text-muted" aria-hidden="true" />
<span className={clsx("tabular-nums transition-opacity", stale && "opacity-60")}>