Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
69 lines
2.4 KiB
TypeScript
69 lines
2.4 KiB
TypeScript
type ParsedHslToken = {
|
|
hue: number;
|
|
saturation: number;
|
|
lightness: number;
|
|
};
|
|
|
|
const BLACK_HSL = '0 0% 0%';
|
|
const WHITE_HSL = '0 0% 100%';
|
|
|
|
const parseHslToken = (value: string): ParsedHslToken | null => {
|
|
const match = /^\s*(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%\s*$/.exec(value);
|
|
if (!match) return null;
|
|
const hue = Number(match[1]);
|
|
const saturation = Number(match[2]);
|
|
const lightness = Number(match[3]);
|
|
if (![hue, saturation, lightness].every(Number.isFinite)) return null;
|
|
return {
|
|
hue: ((hue % 360) + 360) % 360,
|
|
saturation: Math.min(100, Math.max(0, saturation)) / 100,
|
|
lightness: Math.min(100, Math.max(0, lightness)) / 100,
|
|
};
|
|
};
|
|
|
|
const hslToRgb = ({ hue, saturation, lightness }: ParsedHslToken): [number, number, number] => {
|
|
if (saturation === 0) return [lightness, lightness, lightness];
|
|
|
|
const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;
|
|
const huePrime = hue / 60;
|
|
const x = chroma * (1 - Math.abs((huePrime % 2) - 1));
|
|
const [red, green, blue] =
|
|
huePrime < 1 ? [chroma, x, 0] :
|
|
huePrime < 2 ? [x, chroma, 0] :
|
|
huePrime < 3 ? [0, chroma, x] :
|
|
huePrime < 4 ? [0, x, chroma] :
|
|
huePrime < 5 ? [x, 0, chroma] :
|
|
[chroma, 0, x];
|
|
const match = lightness - chroma / 2;
|
|
return [red + match, green + match, blue + match];
|
|
};
|
|
|
|
const toLinearSrgb = (channel: number): number => (
|
|
channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4
|
|
);
|
|
|
|
export const getHslTokenRelativeLuminance = (value: string): number | null => {
|
|
const parsed = parseHslToken(value);
|
|
if (!parsed) return null;
|
|
const [red, green, blue] = hslToRgb(parsed).map(toLinearSrgb);
|
|
return 0.2126 * red + 0.7152 * green + 0.0722 * blue;
|
|
};
|
|
|
|
export const getContrastRatio = (foregroundLuminance: number, backgroundLuminance: number): number => {
|
|
const lighter = Math.max(foregroundLuminance, backgroundLuminance);
|
|
const darker = Math.min(foregroundLuminance, backgroundLuminance);
|
|
return (lighter + 0.05) / (darker + 0.05);
|
|
};
|
|
|
|
export const resolveReadableForegroundForHsl = (
|
|
backgroundHsl: string,
|
|
fallback: string = WHITE_HSL,
|
|
): string => {
|
|
const backgroundLuminance = getHslTokenRelativeLuminance(backgroundHsl);
|
|
if (backgroundLuminance == null) return fallback;
|
|
|
|
const blackContrast = getContrastRatio(0, backgroundLuminance);
|
|
const whiteContrast = getContrastRatio(1, backgroundLuminance);
|
|
return whiteContrast >= blackContrast ? WHITE_HSL : BLACK_HSL;
|
|
};
|