makeover 3.6.0

Shared theme loading for the make-family apps: TOML theme files parsed into intent-based color tokens, with perceptual derivations and WCAG contrast.
Documentation
//! Every stated contrast floor, checked on every bundled theme.
//!
//! A theme value used to be checked against the default theme by eye, so a
//! token could clear its floor on akari-dawn and miss it on half the corpus
//! without anything objecting. This is the one table of floors, and the build
//! fails when a theme falls under one.
//!
//! Each floor carries the themes already known to miss it. The list is asserted
//! in both directions, the way the corpus tests in `intent.rs` are: a theme
//! newly failing is a regression, and a theme that starts passing has to be
//! taken off the list by hand, so no exemption outlives its reason. Shrinking a
//! list is the fix. Most of what is listed is imported palettes, which were
//! drawn for syntax highlighting and owe the depth model nothing.
//!
//! Adding a derived token means adding its row here.

use makeover::{
    DISTINCT, Rgb, STEP_FLOOR, embedded_themes, parse_theme_str, resolve, wcag_contrast,
};

/// WCAG AA for normal text.
const TEXT: f32 = 4.5;

/// The muted tier's bar: present and readable, deliberately under body text.
/// `selection` grades a theme whose muted text clears this and not [`TEXT`]
/// as standard rather than accessible.
const QUIET_TEXT: f32 = 3.0;

struct Floor {
    fg: &'static str,
    bg: &'static str,
    min: f32,
    known: &'static [&'static str],
}

const FLOORS: &[Floor] = &[
    // Body text on every surface it is set on.
    Floor {
        fg: "content",
        bg: "surface-page",
        min: TEXT,
        known: &[],
    },
    Floor {
        fg: "content",
        bg: "surface-raised",
        min: TEXT,
        known: &["solarized-dark"],
    },
    Floor {
        fg: "content",
        bg: "surface-well",
        min: TEXT,
        known: &[],
    },
    Floor {
        fg: "content",
        bg: "surface-sunken",
        min: TEXT,
        known: &["ayu-light", "solarized-dark"],
    },
    // A tonal step has to be visible as a step before it is anything else.
    Floor {
        fg: "content-secondary",
        bg: "content",
        min: STEP_FLOOR,
        known: &[],
    },
    Floor {
        fg: "content-secondary",
        bg: "surface-page",
        min: TEXT,
        known: &["ayu-light", "solarized-dark"],
    },
    // The table header's ink on its strip, wiki `table-model`.
    Floor {
        fg: "content-secondary",
        bg: "surface-sunken",
        min: TEXT,
        known: &[
            "ayu-light",
            "catppuccin-latte",
            "dawnfox",
            "flatwhite",
            "rosepine-dawn",
            "solarized-dark",
        ],
    },
    Floor {
        fg: "content-secondary",
        bg: "surface-well",
        min: TEXT,
        known: &[
            "ayu-light",
            "catppuccin-latte",
            "dawnfox",
            "flatwhite",
            "rosepine-dawn",
            "solarized-dark",
        ],
    },
    Floor {
        fg: "content-muted",
        bg: "surface-page",
        min: QUIET_TEXT,
        known: &[
            "ayu-light",
            "catppuccin-latte",
            "dawnfox",
            "flatwhite",
            "rosepine-dawn",
            "solarized-dark",
        ],
    },
    Floor {
        fg: "content-muted",
        bg: "surface-well",
        min: QUIET_TEXT,
        known: &[
            "ayu-light",
            "catppuccin-latte",
            "dawnfox",
            "flatwhite",
            "gruvbox-light",
            "rosepine-dawn",
            "solarized-dark",
        ],
    },
    // Text on the table model's row tones, wiki `table-model`.
    Floor {
        fg: "content",
        bg: "row-stripe",
        min: TEXT,
        known: &["solarized-dark"],
    },
    Floor {
        fg: "content",
        bg: "row-hover",
        min: TEXT,
        known: &["solarized-dark"],
    },
    Floor {
        fg: "content",
        bg: "row-selected",
        min: TEXT,
        known: &["solarized-dark"],
    },
    // Ink on a status chip, wiki `table-model`.
    Floor {
        fg: "content",
        bg: "info-surface",
        min: TEXT,
        known: &["solarized-dark"],
    },
    // Ink on a status chip, wiki `table-model`.
    Floor {
        fg: "content",
        bg: "success-surface",
        min: TEXT,
        known: &["solarized-dark"],
    },
    // Ink on a status chip, wiki `table-model`.
    Floor {
        fg: "content",
        bg: "warning-surface",
        min: TEXT,
        known: &["solarized-dark"],
    },
    // Ink on a status chip, wiki `table-model`.
    Floor {
        fg: "content",
        bg: "danger-surface",
        min: TEXT,
        known: &["solarized-dark"],
    },
    Floor {
        fg: "content-on-action",
        bg: "action",
        min: TEXT,
        known: &[],
    },
    // A focus ring is a boundary, not text, so it answers to the 3:1 two areas
    // need. goingson is one of ours and is listed on the page only: its #3B70D9
    // accent clears raised, and the blue page it sits on is the half left to fix.
    Floor {
        fg: "focus-ring",
        bg: "surface-page",
        min: DISTINCT,
        known: &["ayu-light", "goingson"],
    },
    Floor {
        fg: "focus-ring",
        bg: "surface-raised",
        min: DISTINCT,
        known: &["ayu-light"],
    },
];

#[test]
fn every_bundled_theme_clears_every_stated_floor() {
    let themes: Vec<_> = embedded_themes()
        .map(|(id, source)| (id, resolve(&parse_theme_str(id, source, false).unwrap())))
        .collect();
    assert!(
        themes.len() >= 31,
        "expected the full bundled corpus, got {}",
        themes.len()
    );

    let mut problems = Vec::new();
    for floor in FLOORS {
        let mut failing: Vec<(&str, f32)> = Vec::new();
        for (id, t) in &themes {
            let rgb = |k: &str| t.hex(k).and_then(Rgb::from_hex);
            let (Some(fg), Some(bg)) = (rgb(floor.fg), rgb(floor.bg)) else {
                continue;
            };
            let ratio = wcag_contrast(fg, bg);
            if ratio < floor.min {
                failing.push((id, ratio));
            }
        }

        let pair = format!("{} on {} (>= {})", floor.fg, floor.bg, floor.min);
        for (id, ratio) in &failing {
            if !floor.known.contains(id) {
                problems.push(format!("{pair}: {id} is {ratio:.2}:1, a new failure"));
            }
        }
        for id in floor.known {
            if !failing.iter().any(|(f, _)| f == id) {
                problems.push(format!("{pair}: {id} now clears it; take it off the list"));
            }
        }
    }

    assert!(
        problems.is_empty(),
        "contrast floors:\n  {}",
        problems.join("\n  ")
    );
}