From 40c6c1166b17815e0e414f028ae7cbedb90cc421 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Thu, 1 Oct 2026 06:39:56 +0700 Subject: [PATCH] Port upstream 0.70.0: refresh 16 provider brand accents Adopt the accents upstream CodexBar 0.70.0 took from its palette audit (#4075): Abacus, Amp, Augment, Bedrock, ClinePass, Codebuff, Command Code, Cursor, DeepSeek, Devin, Kiro, LongCat, Mistral, Neuralwatt, sub2api and Venice. The Rust brand_color table, the frontend PROVIDER_ICON_REGISTRY and the --chart- tokens that mirror them move together. Tests pin the adopted accents, check upstream's contrast non-regression rule against the previous Windows accents on white and #222222, and keep the Rust table, the registry and the chart tokens in sync. --- .../providers/providerIcons.test.ts | 89 ++++++++++ .../src/components/providers/providerIcons.ts | 32 ++-- apps/desktop-tauri/src/styles.css | 14 +- rust/src/core/provider.rs | 155 ++++++++++++++++-- 4 files changed, 249 insertions(+), 41 deletions(-) diff --git a/apps/desktop-tauri/src/components/providers/providerIcons.test.ts b/apps/desktop-tauri/src/components/providers/providerIcons.test.ts index 27c677024b..bcad08918d 100644 --- a/apps/desktop-tauri/src/components/providers/providerIcons.test.ts +++ b/apps/desktop-tauri/src/components/providers/providerIcons.test.ts @@ -1,11 +1,100 @@ +import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { TEST_PROVIDER_CATALOG } from "../../test/providerCatalog"; import { PROVIDER_ICON_REGISTRY } from "./providerIcons"; +// Repository root, from apps/desktop-tauri/src/components/providers. +const REPO_ROOT = import.meta.dirname + "/../../../../../"; + +/** + * The `ProviderId::X => "#RRGGBB"` rows of the Rust `brand_color` table in + * `rust/src/core/provider.rs`, keyed by the lowercased variant name (which + * is the registry id for every provider). + */ +function rustBrandColors(): Map { + const source = readFileSync(REPO_ROOT + "rust/src/core/provider.rs", "utf8"); + const start = source.indexOf("pub fn brand_color(id: ProviderId)"); + const end = source.indexOf("\n}\n", start); + const rows = new Map(); + if (start < 0 || end < 0) return rows; + for (const match of source + .slice(start, end) + .matchAll(/ProviderId::(\w+) => "(#[0-9A-Fa-f]{6})"/g)) { + rows.set(match[1].toLowerCase(), match[2].toLowerCase()); + } + return rows; +} + +/** `--chart-: rgb(r, g, b);` provider tokens from styles.css, as hex. */ +function chartTokens(): Array<[string, string]> { + const css = readFileSync(import.meta.dirname + "/../../styles.css", "utf8"); + const tokens: Array<[string, string]> = []; + for (const match of css.matchAll( + /--chart-([a-z0-9]+): rgb\((\d+), (\d+), (\d+)\);/g, + )) { + const hex = [match[2], match[3], match[4]] + .map((channel) => Number(channel).toString(16).padStart(2, "0")) + .join(""); + tokens.push([match[1], `#${hex}`]); + } + return tokens; +} + +// Upstream 0.70.0 palette audit (#4075, docs/provider-palette.md): the 16 +// accents it adopted. The Rust side pins the same values and checks their +// contrast against the previous Windows accents. +const ADOPTED_ACCENTS: Array<[string, string]> = [ + ["abacus", "#814ee8"], + ["amp", "#f34e3f"], + ["augment", "#1aa049"], + ["bedrock", "#01a88d"], + ["clinepass", "#5487c8"], + ["codebuff", "#00ff95"], + ["commandcode", "#8c4edd"], + ["cursor", "#f54e00"], + ["deepseek", "#4d6bfe"], + ["devin", "#317cff"], + ["kiro", "#9046ff"], + ["longcat", "#29e154"], + ["mistral", "#ff5229"], + ["neuralwatt", "#d55934"], + ["sub2api", "#14b8a6"], + ["venice", "#3c8fdd"], +]; + describe("provider icon registry", () => { it("has explicit icon metadata for every provider in the catalog", () => { for (const [id] of TEST_PROVIDER_CATALOG) { expect(PROVIDER_ICON_REGISTRY[id], id).toBeDefined(); } }); + + it("uses the accents adopted by the upstream 0.70.0 palette audit", () => { + for (const [id, color] of ADOPTED_ACCENTS) { + expect(PROVIDER_ICON_REGISTRY[id]?.brandColor.toLowerCase(), id).toBe(color); + } + }); + + it("matches the Rust brand_color table for every provider", () => { + const rust = rustBrandColors(); + expect(rust.size).toBeGreaterThan(0); + expect([...rust.keys()].sort()).toEqual(Object.keys(PROVIDER_ICON_REGISTRY).sort()); + for (const [id, color] of rust) { + expect(PROVIDER_ICON_REGISTRY[id]?.brandColor.toLowerCase(), id).toBe(color); + } + }); + + it("keeps provider chart tokens on the brand color", () => { + // Grok's chart token has been pure black since before the registry + // existed (xAI shares it), so it is the one intentional exception. + const checked = chartTokens().filter( + ([id]) => id !== "grok" && PROVIDER_ICON_REGISTRY[id] !== undefined, + ); + expect(checked.map(([id]) => id)).toEqual( + expect.arrayContaining(["amp", "augment", "codebuff", "cursor", "deepseek", "kiro", "mistral"]), + ); + for (const [id, color] of checked) { + expect(color, `--chart-${id}`).toBe(PROVIDER_ICON_REGISTRY[id].brandColor.toLowerCase()); + } + }); }); diff --git a/apps/desktop-tauri/src/components/providers/providerIcons.ts b/apps/desktop-tauri/src/components/providers/providerIcons.ts index 2fdc4380d7..d1aefaea64 100644 --- a/apps/desktop-tauri/src/components/providers/providerIcons.ts +++ b/apps/desktop-tauri/src/components/providers/providerIcons.ts @@ -162,26 +162,26 @@ const RAW: Record = { export const PROVIDER_ICON_REGISTRY: Record = { alibaba: { id: "alibaba", brandColor: "#ff6a00", fallbackLetter: "阿", svgPath: RAW.alibaba }, alibabatokenplan: { id: "alibabatokenplan", brandColor: "#ff6a00", fallbackLetter: "阿", svgPath: RAW.alibaba }, - amp: { id: "amp", brandColor: "#dc2626", fallbackLetter: "⚡", svgPath: RAW.amp }, + amp: { id: "amp", brandColor: "#f34e3f", fallbackLetter: "⚡", svgPath: RAW.amp }, antigravity: { id: "antigravity", brandColor: "#60ba7e", fallbackLetter: "◉", svgPath: RAW.antigravity }, - augment: { id: "augment", brandColor: "#6366f1", fallbackLetter: "A", svgPath: RAW.augment }, + augment: { id: "augment", brandColor: "#1aa049", fallbackLetter: "A", svgPath: RAW.augment }, claude: { id: "claude", brandColor: "#cc7c5e", fallbackLetter: "◈", svgPath: RAW.claude }, pi: { id: "pi", brandColor: "#7c3aed", fallbackLetter: "P" }, - codebuff: { id: "codebuff", brandColor: "#44ff00", fallbackLetter: "B", svgPath: RAW.codebuff }, + codebuff: { id: "codebuff", brandColor: "#00ff95", fallbackLetter: "B", svgPath: RAW.codebuff }, coderabbit: { id: "coderabbit", brandColor: "#ff5c35", fallbackLetter: "C", svgPath: RAW.coderabbit }, codex: { id: "codex", brandColor: "#49a3b0", fallbackLetter: "◆", svgPath: RAW.codex }, copilot: { id: "copilot", brandColor: "#a855f7", fallbackLetter: "⬡", svgPath: RAW.copilot }, - cursor: { id: "cursor", brandColor: "#00bfa5", fallbackLetter: "▸", svgPath: RAW.cursor }, + cursor: { id: "cursor", brandColor: "#f54e00", fallbackLetter: "▸", svgPath: RAW.cursor }, deepgram: { id: "deepgram", brandColor: "#13ef93", fallbackLetter: "D", svgPath: RAW.deepgram }, deepinfra: { id: "deepinfra", brandColor: "#2a3275", fallbackLetter: "D", svgPath: RAW.deepinfra }, fireworks: { id: "fireworks", brandColor: "#f25b1c", fallbackLetter: "F", svgPath: RAW.fireworks }, aiand: { id: "aiand", brandColor: "#e25c2b", fallbackLetter: "&", svgPath: RAW.aiand }, - clinepass: { id: "clinepass", brandColor: "#61a3fa", fallbackLetter: "C", svgPath: RAW.clinepass }, - longcat: { id: "longcat", brandColor: "#ffd100", fallbackLetter: "L", svgPath: RAW.longcat }, - neuralwatt: { id: "neuralwatt", brandColor: "#38d98c", fallbackLetter: "N", svgPath: RAW.neuralwatt }, + clinepass: { id: "clinepass", brandColor: "#5487c8", fallbackLetter: "C", svgPath: RAW.clinepass }, + longcat: { id: "longcat", brandColor: "#29e154", fallbackLetter: "L", svgPath: RAW.longcat }, + neuralwatt: { id: "neuralwatt", brandColor: "#d55934", fallbackLetter: "N", svgPath: RAW.neuralwatt }, zoommate: { id: "zoommate", brandColor: "#0B5CFF", fallbackLetter: "Z", svgPath: RAW.zoommate }, zenmux: { id: "zenmux", brandColor: "#6c5ce7", fallbackLetter: "Z", svgPath: RAW.zenmux }, - deepseek: { id: "deepseek", brandColor: "#527df0", fallbackLetter: "D", svgPath: RAW.deepseek }, + deepseek: { id: "deepseek", brandColor: "#4d6bfe", fallbackLetter: "D", svgPath: RAW.deepseek }, elevenlabs: { id: "elevenlabs", brandColor: "#111827", fallbackLetter: "E", svgPath: RAW.elevenlabs }, factory: { id: "factory", brandColor: "#ff6b35", fallbackLetter: "◎", svgPath: RAW.factory }, gemini: { id: "gemini", brandColor: "#ab87ea", fallbackLetter: "✦", svgPath: RAW.gemini }, @@ -193,13 +193,13 @@ export const PROVIDER_ICON_REGISTRY: Record = { typesafe: { id: "typesafe", brandColor: "#2563eb", fallbackLetter: "T" }, jetbrains: { id: "jetbrains", brandColor: "#ff3399", fallbackLetter: "J", svgPath: RAW.jetbrains }, kilo: { id: "kilo", brandColor: "#5d87ff", fallbackLetter: "K", svgPath: RAW.kilo }, - bedrock: { id: "bedrock", brandColor: "#ff9900", fallbackLetter: "B", svgPath: RAW.bedrock }, + bedrock: { id: "bedrock", brandColor: "#01a88d", fallbackLetter: "B", svgPath: RAW.bedrock }, kimi: { id: "kimi", brandColor: "#fe603c", fallbackLetter: "☽", svgPath: RAW.kimi }, kimik2: { id: "kimik2", brandColor: "#4c00ff", fallbackLetter: "☽", svgPath: RAW.kimi }, - kiro: { id: "kiro", brandColor: "#ff9900", fallbackLetter: "K", svgPath: RAW.kiro }, + kiro: { id: "kiro", brandColor: "#9046ff", fallbackLetter: "K", svgPath: RAW.kiro }, llmproxy: { id: "llmproxy", brandColor: "#4f46e5", fallbackLetter: "L", svgPath: RAW.llmproxy }, minimax: { id: "minimax", brandColor: "#fe603c", fallbackLetter: "M", svgPath: RAW.minimax }, - mistral: { id: "mistral", brandColor: "#ff500f", fallbackLetter: "M", svgPath: RAW.mistral }, + mistral: { id: "mistral", brandColor: "#ff5229", fallbackLetter: "M", svgPath: RAW.mistral }, muse: { id: "muse", brandColor: "#0668e1", fallbackLetter: "M", svgPath: RAW.muse }, ollama: { id: "ollama", brandColor: "#8b95b0", fallbackLetter: "○", svgPath: RAW.ollama }, azureopenai: { id: "azureopenai", brandColor: "#0078d4", fallbackLetter: "A" }, @@ -216,11 +216,11 @@ export const PROVIDER_ICON_REGISTRY: Record = { // Aliases / Rust-side normalizations without their own SVG. nanogpt: { id: "nanogpt", brandColor: "#687fa1", fallbackLetter: "N" }, infini: { id: "infini", brandColor: "#687fa1", fallbackLetter: "I" }, - abacus: { id: "abacus", brandColor: "#7c3aed", fallbackLetter: "A", svgPath: RAW.abacus }, + abacus: { id: "abacus", brandColor: "#814ee8", fallbackLetter: "A", svgPath: RAW.abacus }, manus: { id: "manus", brandColor: "#34322d", fallbackLetter: "M", svgPath: RAW.manus }, mimo: { id: "mimo", brandColor: "#ff6900", fallbackLetter: "M", svgPath: RAW.mimo }, doubao: { id: "doubao", brandColor: "#2563eb", fallbackLetter: "D", svgPath: RAW.doubao }, - commandcode: { id: "commandcode", brandColor: "#44ff00", fallbackLetter: "C", svgPath: RAW.commandcode }, + commandcode: { id: "commandcode", brandColor: "#8c4edd", fallbackLetter: "C", svgPath: RAW.commandcode }, crof: { id: "crof", brandColor: "#7c3aed", fallbackLetter: "C", svgPath: RAW.crof }, crossmodel: { id: "crossmodel", brandColor: "#c084fc", fallbackLetter: "X", svgPath: RAW.crossmodel }, qoder: { id: "qoder", brandColor: "#2563eb", fallbackLetter: "Q", svgPath: RAW.qoder }, @@ -228,13 +228,13 @@ export const PROVIDER_ICON_REGISTRY: Record = { codebuddy: { id: "codebuddy", brandColor: "#0052d9", fallbackLetter: "C" }, sakana: { id: "sakana", brandColor: "#0ea5e9", fallbackLetter: "S", svgPath: RAW.sakana }, stepfun: { id: "stepfun", brandColor: "#999999", fallbackLetter: "S", svgPath: RAW.stepfun }, - sub2api: { id: "sub2api", brandColor: "#2dc6d8", fallbackLetter: "S", svgPath: RAW.sub2api }, - venice: { id: "venice", brandColor: "#111827", fallbackLetter: "V", svgPath: RAW.venice }, + sub2api: { id: "sub2api", brandColor: "#14b8a6", fallbackLetter: "S", svgPath: RAW.sub2api }, + venice: { id: "venice", brandColor: "#3c8fdd", fallbackLetter: "V", svgPath: RAW.venice }, openaiapi: { id: "openaiapi", brandColor: "#10a37f", fallbackLetter: "O" }, chutes: { id: "chutes", brandColor: "#ff5c35", fallbackLetter: "C" }, litellm: { id: "litellm", brandColor: "#0ea5e9", fallbackLetter: "L" }, poe: { id: "poe", brandColor: "#5d5fef", fallbackLetter: "P" }, - devin: { id: "devin", brandColor: "#111827", fallbackLetter: "D" }, + devin: { id: "devin", brandColor: "#317cff", fallbackLetter: "D" }, zed: { id: "zed", brandColor: "#084ccf", fallbackLetter: "Z" }, qwencloud: { id: "qwencloud", brandColor: "#615CED", fallbackLetter: "Q" }, notion: { id: "notion", brandColor: "#337EA9", fallbackLetter: "N", svgPath: RAW.notion }, diff --git a/apps/desktop-tauri/src/styles.css b/apps/desktop-tauri/src/styles.css index a41405732c..1ccd41bed5 100644 --- a/apps/desktop-tauri/src/styles.css +++ b/apps/desktop-tauri/src/styles.css @@ -96,26 +96,26 @@ --chart-claude: rgb(204, 124, 94); --chart-codex: rgb(73, 163, 176); --chart-gemini: rgb(171, 135, 234); - --chart-cursor: rgb(0, 191, 165); + --chart-cursor: rgb(245, 78, 0); --chart-copilot: rgb(168, 85, 247); --chart-jetbrains: rgb(255, 51, 153); --chart-antigravity: rgb(96, 186, 126); - --chart-augment: rgb(99, 102, 241); - --chart-amp: rgb(220, 38, 38); + --chart-augment: rgb(26, 160, 73); + --chart-amp: rgb(243, 78, 63); --chart-factory: rgb(255, 107, 53); --chart-droid: rgb(255, 107, 53); --chart-kimi: rgb(254, 96, 60); --chart-kimik2: rgb(76, 0, 255); - --chart-kiro: rgb(255, 153, 0); + --chart-kiro: rgb(144, 70, 255); --chart-opencode: rgb(59, 130, 246); --chart-minimax: rgb(254, 96, 60); --chart-vertexai: rgb(66, 133, 244); --chart-zai: rgb(232, 90, 106); --chart-alibaba: rgb(255, 106, 0); --chart-nanogpt: rgb(104, 127, 161); - --chart-mistral: rgb(255, 80, 15); - --chart-codebuff: rgb(68, 255, 0); - --chart-deepseek: rgb(82, 125, 240); + --chart-mistral: rgb(255, 82, 41); + --chart-codebuff: rgb(0, 255, 149); + --chart-deepseek: rgb(77, 107, 254); --chart-windsurf: rgb(34, 197, 94); --chart-elevenlabs: rgb(17, 24, 39); --chart-deepgram: rgb(19, 239, 147); diff --git a/rust/src/core/provider.rs b/rust/src/core/provider.rs index cc954839f7..5ccbf4032b 100755 --- a/rust/src/core/provider.rs +++ b/rust/src/core/provider.rs @@ -1022,27 +1022,32 @@ pub fn cli_name_map() -> HashMap<&'static str, ProviderId> { } /// The shipped brand color (hex) for a provider, mirroring the frontend -/// `PROVIDER_ICON_REGISTRY` in `providerIcons.ts`. Used as the default -/// accent color before any per-provider override (#2972). +/// `PROVIDER_ICON_REGISTRY` in `providerIcons.ts` and the `--chart-` +/// tokens in `styles.css` (`providerIcons.test.ts` checks both). Used as the +/// default accent color before any per-provider override (#2972). +/// +/// Upstream 0.70.0 audited the palette (#4075, `docs/provider-palette.md`). +/// The 16 accents it adopted are pinned in the tests below; changing one +/// must not materially reduce contrast on white or `#222222`. pub fn brand_color(id: ProviderId) -> &'static str { match id { ProviderId::Codex => "#49A3B0", ProviderId::Claude => "#CC7C5E", ProviderId::Pi => "#7C3AED", - ProviderId::Cursor => "#00BFA5", + ProviderId::Cursor => "#F54E00", ProviderId::Factory => "#FF6B35", ProviderId::Gemini => "#AB87EA", ProviderId::Antigravity => "#60BA7E", ProviderId::Copilot => "#A855F7", ProviderId::Zai => "#E85A6A", ProviderId::MiniMax => "#FE603C", - ProviderId::Kiro => "#FF9900", + ProviderId::Kiro => "#9046FF", ProviderId::VertexAI => "#4285F4", - ProviderId::Augment => "#6366F1", + ProviderId::Augment => "#1AA049", ProviderId::OpenCode => "#3B82F6", ProviderId::Kimi => "#FE603C", ProviderId::KimiK2 => "#4C00FF", - ProviderId::Amp => "#DC2626", + ProviderId::Amp => "#F34E3F", ProviderId::Warp => "#6366F1", ProviderId::Ollama => "#8B95B0", ProviderId::AzureOpenAI => "#0078D4", @@ -1054,24 +1059,24 @@ pub fn brand_color(id: ProviderId) -> &'static str { ProviderId::NanoGPT => "#687FA1", ProviderId::Infini => "#687FA1", ProviderId::Perplexity => "#1FB8CD", - ProviderId::Abacus => "#7C3AED", - ProviderId::Mistral => "#FF500F", + ProviderId::Abacus => "#814EE8", + ProviderId::Mistral => "#FF5229", ProviderId::OpenCodeGo => "#3B82F6", ProviderId::Kilo => "#5D87FF", - ProviderId::Bedrock => "#FF9900", - ProviderId::Codebuff => "#44FF00", + ProviderId::Bedrock => "#01A88D", + ProviderId::Codebuff => "#00FF95", ProviderId::CodeRabbit => "#FF5C35", - ProviderId::DeepSeek => "#527DF0", + ProviderId::DeepSeek => "#4D6BFE", ProviderId::DeepInfra => "#2A3275", ProviderId::AiAnd => "#E25C2B", ProviderId::Windsurf => "#22C55E", ProviderId::Manus => "#34322D", ProviderId::MiMo => "#FF6900", ProviderId::Doubao => "#2563EB", - ProviderId::CommandCode => "#44FF00", + ProviderId::CommandCode => "#8C4EDD", ProviderId::Crof => "#7C3AED", ProviderId::StepFun => "#999999", - ProviderId::Venice => "#111827", + ProviderId::Venice => "#3C8FDD", ProviderId::OpenAIApi => "#10A37F", ProviderId::Grok => "#111827", ProviderId::ElevenLabs => "#111827", @@ -1085,18 +1090,18 @@ pub fn brand_color(id: ProviderId) -> &'static str { ProviderId::Chutes => "#FF5C35", ProviderId::LiteLLM => "#0EA5E9", ProviderId::Poe => "#5D5FEF", - ProviderId::Devin => "#111827", + ProviderId::Devin => "#317CFF", ProviderId::Zed => "#084CCF", ProviderId::CrossModel => "#C084FC", ProviderId::Qoder => "#2563EB", ProviderId::CodeBuddy => "#0052D9", ProviderId::Sakana => "#0EA5E9", - ProviderId::Sub2Api => "#2DC6D8", + ProviderId::Sub2Api => "#14B8A6", ProviderId::Wayfinder => "#14B8A6", ProviderId::ZenMux => "#6C5CE7", - ProviderId::ClinePass => "#61A3FA", - ProviderId::LongCat => "#FFD100", - ProviderId::Neuralwatt => "#38D98C", + ProviderId::ClinePass => "#5487C8", + ProviderId::LongCat => "#29E154", + ProviderId::Neuralwatt => "#D55934", ProviderId::ZoomMate => "#0B5CFF", ProviderId::QwenCloud => "#615CED", ProviderId::Notion => "#337EA9", @@ -1503,4 +1508,118 @@ mod tests { Some(ProviderId::Grok) ); } + + /// The 16 accents upstream 0.70.0 adopted in its palette audit (#4075), + /// each with the accent Windows shipped before this port. + const ADOPTED_ACCENTS: [(ProviderId, &str, &str); 16] = [ + (ProviderId::Abacus, "#7C3AED", "#814EE8"), + (ProviderId::Amp, "#DC2626", "#F34E3F"), + (ProviderId::Augment, "#6366F1", "#1AA049"), + (ProviderId::Bedrock, "#FF9900", "#01A88D"), + (ProviderId::ClinePass, "#61A3FA", "#5487C8"), + (ProviderId::Codebuff, "#44FF00", "#00FF95"), + (ProviderId::CommandCode, "#44FF00", "#8C4EDD"), + (ProviderId::Cursor, "#00BFA5", "#F54E00"), + (ProviderId::DeepSeek, "#527DF0", "#4D6BFE"), + (ProviderId::Devin, "#111827", "#317CFF"), + (ProviderId::Kiro, "#FF9900", "#9046FF"), + (ProviderId::LongCat, "#FFD100", "#29E154"), + (ProviderId::Mistral, "#FF500F", "#FF5229"), + (ProviderId::Neuralwatt, "#38D98C", "#D55934"), + (ProviderId::Sub2Api, "#2DC6D8", "#14B8A6"), + (ProviderId::Venice, "#111827", "#3C8FDD"), + ]; + + /// Audited providers whose accent upstream kept, where the Windows accent + /// already equals upstream's final value. Chutes, Deepgram, Doubao, Groq, + /// Kilo, LiteLLM, Perplexity, Qoder, Sakana, T3 Chat and Warp keep older + /// Windows accents that differ from upstream; aligning them is out of + /// scope for the 0.70.0 port. + const RETAINED_ACCENTS: [(ProviderId, &str); 7] = [ + (ProviderId::AiAnd, "#E25C2B"), + (ProviderId::Copilot, "#A855F7"), + (ProviderId::Fireworks, "#F25B1C"), + (ProviderId::JetBrains, "#FF3399"), + (ProviderId::Kimi, "#FE603C"), + (ProviderId::Notion, "#337EA9"), + (ProviderId::OpenCode, "#3B82F6"), + ]; + + /// WCAG relative luminance of a `#RRGGBB` color, as upstream's + /// `ProviderPaletteRegressionTests` computes it. + fn relative_luminance(hex: &str) -> f64 { + let digits = hex.strip_prefix('#').expect("hex color starts with #"); + assert_eq!(digits.len(), 6, "{hex} is not #RRGGBB"); + let linear = |offset: usize| { + let channel = + f64::from(u8::from_str_radix(&digits[offset..offset + 2], 16).expect("hex digits")) + / 255.0; + if channel <= 0.04045 { + channel / 12.92 + } else { + ((channel + 0.055) / 1.055).powf(2.4) + } + }; + 0.2126 * linear(0) + 0.7152 * linear(2) + 0.0722 * linear(4) + } + + fn contrast_ratio(color: &str, background: &str) -> f64 { + let foreground = relative_luminance(color); + let backdrop = relative_luminance(background); + (foreground.max(backdrop) + 0.05) / (foreground.min(backdrop) + 0.05) + } + + #[test] + fn contrast_ratio_matches_upstream_audit_values() { + // Rows of upstream docs/provider-palette.md, rounded to two decimals. + let rounded = |value: f64| (value * 100.0).round() / 100.0; + assert_eq!(rounded(contrast_ratio("#814EE8", "#FFFFFF")), 5.01); + assert_eq!(rounded(contrast_ratio("#814EE8", "#222222")), 3.17); + assert_eq!(rounded(contrast_ratio("#FF9900", "#FFFFFF")), 2.14); + assert_eq!(rounded(contrast_ratio("#01A88D", "#222222")), 5.29); + assert_eq!(rounded(contrast_ratio("#00FF95", "#FFFFFF")), 1.33); + assert_eq!(rounded(contrast_ratio("#FFFFFF", "#FFFFFF")), 1.0); + } + + #[test] + fn adopted_upstream_palette_accents_are_pinned() { + for (id, _, adopted) in ADOPTED_ACCENTS { + assert_eq!(brand_color(id), adopted, "{id:?}"); + } + for (id, retained) in RETAINED_ACCENTS { + assert_eq!(brand_color(id), retained, "{id:?}"); + } + } + + #[test] + fn adopted_palette_accents_do_not_materially_regress_contrast() { + // Upstream's controlled light and dark menu surfaces. A material + // regression falls below 3:1 while losing at least 0.5 of contrast + // against the accent Windows shipped before. + for (id, previous, _) in ADOPTED_ACCENTS { + for background in ["#FFFFFF", "#222222"] { + let current = contrast_ratio(brand_color(id), background); + let before = contrast_ratio(previous, background); + assert!( + current >= 3.0 || before - current < 0.5, + "{id:?} on {background}: {before:.2} -> {current:.2}" + ); + } + } + } + + #[test] + fn brand_colors_are_uppercase_hex() { + for id in ProviderId::all() { + let color = brand_color(*id); + assert!( + color.len() == 7 + && color.starts_with('#') + && color[1..] + .chars() + .all(|c| c.is_ascii_digit() || matches!(c, 'A'..='F')), + "{id:?} has {color}" + ); + } + } }