57 lines
2.0 KiB
TypeScript
57 lines
2.0 KiB
TypeScript
const DARK_ACTION_FOREGROUND = "#18181B";
|
|
const LIGHT_ACTION_FOREGROUND = "#FFFFFF";
|
|
const MIN_TEXT_CONTRAST = 4.5;
|
|
|
|
function parseHexColor(color: string): [number, number, number] | null {
|
|
const value = color.trim().replace(/^#/, "");
|
|
const expanded =
|
|
value.length === 3
|
|
? value
|
|
.split("")
|
|
.map((character) => character.repeat(2))
|
|
.join("")
|
|
: value.slice(0, 6);
|
|
|
|
if (expanded.length !== 6 || !/^[0-9a-f]+$/i.test(expanded)) return null;
|
|
|
|
return [
|
|
Number.parseInt(expanded.slice(0, 2), 16),
|
|
Number.parseInt(expanded.slice(2, 4), 16),
|
|
Number.parseInt(expanded.slice(4, 6), 16),
|
|
];
|
|
}
|
|
|
|
function luminance(color: string): number | null {
|
|
const rgb = parseHexColor(color);
|
|
if (!rgb) return null;
|
|
|
|
const channels = rgb.map((channel) => {
|
|
const value = channel / 255;
|
|
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
});
|
|
|
|
return 0.2126 * channels[0]! + 0.7152 * channels[1]! + 0.0722 * channels[2]!;
|
|
}
|
|
|
|
function contrastRatio(foreground: string, background: string): number | null {
|
|
const foregroundLuminance = luminance(foreground);
|
|
const backgroundLuminance = luminance(background);
|
|
if (foregroundLuminance == null || backgroundLuminance == null) return null;
|
|
|
|
const lighter = Math.max(foregroundLuminance, backgroundLuminance);
|
|
const darker = Math.min(foregroundLuminance, backgroundLuminance);
|
|
return (lighter + 0.05) / (darker + 0.05);
|
|
}
|
|
|
|
/** Chooses a readable action label/icon color while preserving a valid requested color. */
|
|
export function resolveActionForeground(background: string, requested: string): string {
|
|
const requestedContrast = contrastRatio(requested, background);
|
|
if (requestedContrast == null || requestedContrast >= MIN_TEXT_CONTRAST) {
|
|
return requested;
|
|
}
|
|
|
|
const darkContrast = contrastRatio(DARK_ACTION_FOREGROUND, background) ?? 0;
|
|
const lightContrast = contrastRatio(LIGHT_ACTION_FOREGROUND, background) ?? 0;
|
|
return darkContrast >= lightContrast ? DARK_ACTION_FOREGROUND : LIGHT_ACTION_FOREGROUND;
|
|
}
|