theme_check.ts

The numeric-twin accessibility gates and the cap recomputation for the derived OKLCH color system, over the resolution core in theme_resolver.ts:

  • check_theme evaluates the gamut, ramp-monotonicity, and contrast gates against an arbitrary theme (the base theme's defaults included), reusing the ramps.ts numeric twin, oklch.ts conversions, and wcag.ts ratios. It is report-only and never throws.
  • compute_theme_caps recomputes the per-stop worst-hue chroma caps for a theme's own hues and the palette lightness each stop resolves to, returning _palette_chroma_NN caps where the baked ones no longer fit. compile_theme in theme_compile.ts pairs them with the re-check and the lint.
view source

Declarations
#

17 declarations

check_theme
#

theme_check.ts view source

(theme: { name: string; variables: { name: string; dark?: string | undefined; base?: string | undefined; summary?: string | undefined; }[]; summary?: string | undefined; scheme?: "dark" | "light" | "dual" | undefined; }, options?: ThemeResolverOptions): ThemeCheckReport import {check_theme} from '@fuzdev/fuz_css/theme_check.js';

Runs the gamut, monotonicity, and contrast gates against a theme, resolving its authored CSS back to numbers through the resolution core. Report-only: failures land in entries (with pass: false), inputs that can't be evaluated land in unchecked, and ok is true only when every entry passes and nothing is unchecked. Never throws.

Every color a gate reads is looked up by its variable name, so a theme that authors one directly - a stop (palette_a_50, shade_00, text_max, border_color_30) or a role (text_color, link_color, border_color) - is measured at the authored color when it is an oklch(L C H) numeric literal or an exact var() reference to another color the gates evaluate, and recorded in unchecked otherwise. An authored color a gate depends on never passes unread, and one no gate reads stays out of the report.

A contrast subject names what was measured: the default stop while a role is at its default (text_80 on shade_00), the role once the theme authors it (text_color on shade_00).

An intent stop that renders the same color as a palette letter's (the default for every intent) folds into that letter's gamut entry rather than duplicating it, so reports for letter-bound themes list fewer subjects than the full letters × intents grid.

theme

the theme to check

type Theme

options

the compiled caps to check the theme with, as compute_theme_caps returns them

default {}

returns

ThemeCheckReport

compute_theme_caps
#

theme_check.ts view source

(theme: { name: string; variables: { name: string; dark?: string | undefined; base?: string | undefined; summary?: string | undefined; }[]; summary?: string | undefined; scheme?: "dark" | "light" | "dual" | undefined; }): { ...; }[] import {compute_theme_caps} from '@fuzdev/fuz_css/theme_check.js';

Recomputes a theme's per-stop worst-hue chroma caps from its own hues and the palette lightness each stop resolves to, then returns _palette_chroma_NN caps wherever the baked ones no longer fit - the fix for a theme (rotated hues, a monochrome collapse, a moved lightness ramp) whose gamut headroom the baked worst-hue table misjudges. The caps are derived data beside the theme, never in it: a theme can't set an internal variable, so they render through render_theme_style's caps option and recomputing them for the same theme gives the same caps.

Each stop's cap is computed at the lightness the resolution core resolves for the stop in that scheme - the theme's endpoint-and-curve ramp - so the caps describe the same colors check_theme gates.

A stop is emitted when either scheme's recomputed cap is tighter than the baked value, or looser by more than the emit epsilon. Nothing is emitted when a hue fails to resolve to a number, since caps computed without it could overshoot its gamut, and a stop whose lightness fails to resolve is skipped. An override emits both slots together, so a one-scheme override can't silently kill the base default's other slot by cascade-layer order, collapsing to a single slot in the base position when the two rendered values coincide.

A stanced theme's two schemes resolve to the stanced appearance through the mirror, so its caps are baselined against the stanced scheme's baked table - the values the mirror already re-slots - and a stance alone (hues and lightness unmoved) emits nothing. Its overrides coincide across schemes and emit single-slot unless the theme authors a dark slot that splits them.

The caps bound the requested chroma only: chroma_scale and the per-slot *_chroma_scale multipliers apply above the clamp. A theme that raises one past 1 clips by design - its gamut failures survive the compile, and a cap the compile loosens can add to them, since the multiplier then pushes more stops past the looser cap. Gate the theme with them through check_theme(theme, {caps}).

theme

the theme to recompute caps for

type Theme

returns

StyleVariable[]

the _palette_chroma_NN caps that moved, empty when the baked ones all still fit

GATE_BODY_TEXT
#

theme_check.ts view source

7 import {GATE_BODY_TEXT} from '@fuzdev/fuz_css/theme_check.js';

AAA body text: --text_color (text_80 by default) on shade_00/05/10.

GATE_BORDER
#

theme_check.ts view source

1.5 import {GATE_BORDER} from '@fuzdev/fuz_css/theme_check.js';

Control borders: --border_color (shade_30 by default) vs shade_00. A regression floor for the shipped design, not a WCAG level - 1.4.11's 3:1 applies to required component boundaries, and fuz borders sit deliberately softer - so a theme can't silently wash control borders out.

GATE_BORDER_DIVIDER
#

theme_check.ts view source

1.3 import {GATE_BORDER_DIVIDER} from '@fuzdev/fuz_css/theme_check.js';

Divider borders: the border_color_30 alpha color composited over shade_00 (the hr pairing in style.css). A regression floor like GATE_BORDER.

GATE_FILL_TEXT
#

theme_check.ts view source

3 import {GATE_FILL_TEXT} from '@fuzdev/fuz_css/theme_check.js';

Large-text floor: text_max on every stop-50 fill.

GATE_GAMUT_TOLERANCE
#

theme_check.ts view source

0.0001 import {GATE_GAMUT_TOLERANCE} from '@fuzdev/fuz_css/theme_check.js';

Gamut gate: how far outside sRGB (the largest channel excess) a stop may sit and still pass - float noise, not visible clipping.

GATE_LINK
#

GATE_PALETTE_TEXT
#

theme_check.ts view source

4.5 import {GATE_PALETTE_TEXT} from '@fuzdev/fuz_css/theme_check.js';

AA colored labels: every palette hue at stop 60 on shade_00 (colored text) and on its own stop-10 tint (the chip), and the .palette_X button label - the hue at stop 70 - on each of the button's fills. The fills follow the style.css recipe: the hue at stop 60 at 8% alpha at rest (and focused), 16% hovered, and 24% pressed, composited over shade_00 in gamma-encoded sRGB. A hovered label mixes 10% toward shade_max in oklab, so a mouse press pairs that label with the pressed fill, and a keyboard press, which doesn't hover, pairs the plain label with it. An 8-bit raster quantizes both colors, so a rendered measurement lands within about 0.15 of the ratio, and the gate reads it low more often than high.

GATE_SELECTED_TEXT
#

theme_check.ts view source

4.5 import {GATE_SELECTED_TEXT} from '@fuzdev/fuz_css/theme_check.js';

AA selected-control inverse text: text_00 on shade_60 and on every stop-60 fill - the selected-button pairings in style.css (.selected fills with shade_60, .palette_X.selected with palette_X_60). The endpoint stop is immune to text_lightness_curve bends, so this gate moves only when a theme moves the endpoints, the fill ramps, or the hues.

GATE_SUBTLE_TEXT
#

theme_check.ts view source

3 import {GATE_SUBTLE_TEXT} from '@fuzdev/fuz_css/theme_check.js';

Disabled/secondary floor: --text_disabled (text_50 by default) on shade_00.

GATE_UI
#

theme_check.ts view source

3 import {GATE_UI} from '@fuzdev/fuz_css/theme_check.js';

WCAG 1.4.11 non-text: every hue at stop 50 vs shade_00, and an authored --outline_color (the focus ring, the accent at stop 50 by default).

theme_gate_role_names
#

theme_check.ts view source

readonly string[] import {theme_gate_role_names} from '@fuzdev/fuz_css/theme_check.js';

The role variables the contrast gates follow - each aliases a color stop or another role by default (text_color is var(--text_80)), and check_theme measures whatever a theme points it at instead of the stop it left.

ThemeCheckReport
#

ThemeGateEntry
#

theme_check.ts view source

ThemeGateEntry import type {ThemeGateEntry} from '@fuzdev/fuz_css/theme_check.js';

A single gate measurement against a theme. The value/threshold comparison direction depends on the gate: contrast passes at value >= threshold (a WCAG ratio floor), monotonicity at value > threshold (the minimum ramp step must exceed 0), and gamut at value <= threshold (sRGB channel excess stays within tolerance). pass is authoritative; renderers formatting the comparison switch on gate.

gate

type ThemeGateId

scheme

type ColorSchemeVariant

subject

type string

value

type number

threshold

type number

pass

type boolean

ThemeGateId
#

theme_check.ts view source

ThemeGateId

type "gamut" | "monotonicity" | "contrast"

import type {ThemeGateId} from '@fuzdev/fuz_css/theme_check.js';

Which accessibility gate an entry belongs to.

ThemeUncheckedEntry
#

theme_check.ts view source

ThemeUncheckedEntry import type {ThemeUncheckedEntry} from '@fuzdev/fuz_css/theme_check.js';

A gate input the checker couldn't evaluate, so the gates reading it were skipped: a knob or derived stop that doesn't resolve to a number, or a pinned color in a form the gates don't read.

variable

type string

value

type string

reason

type string

Depends on
#

Imported by
#