Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
5.2 kB · 154 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155import { borderAlphas, surfaces, textAlphas } from "./tokens";
/** * Chrome colors: the neutral scaffolding of the UI (surfaces, text, hairline * borders) derived from one background and one foreground per color scheme. * The defaults in tokens.ts are exactly such a derivation (a near-black base * stepped toward white; white text at fixed alphas), so a node that sets its * own pair through branding gets the same ramp structure in its own colors. */export interface ChromeColors { /** Base surface, e.g. "#0a0a0b". */ background: string; /** Text and border color, e.g. "#ffffff". */ foreground: string;}
export interface DerivedChrome { surface: { 0: string; 1: string; 2: string; 3: string }; text: { 1: string; 2: string; 3: string; 4: string }; border: { subtle: string; default: string; strong: string };}
type RGB = { r: number; g: number; b: number };
/** Parse #rgb / #rrggbb / #rrggbbaa (alpha ignored). Null when invalid. */export function parseHexColor(input: string | undefined | null): RGB | null { if (!input) return null; const hex = input.trim().replace(/^#/, ""); if (!/^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$|^[0-9a-fA-F]{8}$/.test(hex)) { return null; } const full = hex.length === 3 ? hex .split("") .map((c) => c + c) .join("") : hex.slice(0, 6); return { r: parseInt(full.slice(0, 2), 16), g: parseInt(full.slice(2, 4), 16), b: parseInt(full.slice(4, 6), 16), };}
const toHex = ({ r, g, b }: RGB) => "#" + [r, g, b] .map((v) => Math.round(Math.max(0, Math.min(255, v))) .toString(16) .padStart(2, "0"), ) .join("");
const mix = (a: RGB, b: RGB, t: number): RGB => ({ r: a.r + (b.r - a.r) * t, g: a.g + (b.g - a.g) * t, b: a.b + (b.b - a.b) * t,});
const rgba = ({ r, g, b }: RGB, alpha: number) => `rgba(${Math.round(r)},${Math.round(g)},${Math.round(b)},${alpha})`;
/** Relative luminance, for telling a dark pair from a light one. */export function luminance({ r, g, b }: RGB): number { const lin = (c: number) => { const s = c / 255; return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; }; return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);}
// How far each raised surface steps from the base toward the foreground.// Dark schemes step further: the same 3% reads as less on a dark ground.const SURFACE_STEPS = { dark: [0, 0.03, 0.06, 0.09], light: [0, 0.02, 0.045, 0.075],} as const;const TEXT_ALPHAS = [0.92, 0.65, 0.45, 0.3] as const;const BORDER_ALPHAS = { dark: { subtle: 0.06, default: 0.08, strong: 0.1 }, light: { subtle: 0.06, default: 0.09, strong: 0.13 },} as const;
/** White or near-black, whichever reads on `color`. */export function contrastForeground(color: string): string { const rgb = parseHexColor(color); if (!rgb) return "#ffffff"; return luminance(rgb) > 0.4 ? "#09090b" : "#ffffff";}
/** `color` (hex) at `alpha`, as an rgba() string. */export function withAlpha(color: string, alpha: number): string { const rgb = parseHexColor(color); if (!rgb) return color; return rgba(rgb, alpha);}
/** * Derive the surface / text / border ramps for one scheme. Either color may * be omitted: a missing foreground is white or near-black by the * background's luminance, a missing background is the scheme's default. * Returns null when neither is set (or both are invalid), so callers keep * the token defaults. */export function deriveChrome( chrome: Partial<ChromeColors> | undefined, isDark: boolean,): DerivedChrome | null { const defaults = isDark ? DEFAULT_CHROME.dark : DEFAULT_CHROME.light; let bg = parseHexColor(chrome?.background); let fg = parseHexColor(chrome?.foreground); if (!bg && !fg) return null; if (!bg) bg = parseHexColor(defaults.background); if (!fg) fg = parseHexColor(contrastForeground(toHex(bg!))); if (!bg || !fg) return null; const steps = isDark ? SURFACE_STEPS.dark : SURFACE_STEPS.light; const borders = isDark ? BORDER_ALPHAS.dark : BORDER_ALPHAS.light; return { surface: { 0: toHex(mix(bg, fg, steps[0])), 1: toHex(mix(bg, fg, steps[1])), 2: toHex(mix(bg, fg, steps[2])), 3: toHex(mix(bg, fg, steps[3])), }, text: { 1: rgba(fg, TEXT_ALPHAS[0]), 2: rgba(fg, TEXT_ALPHAS[1]), 3: rgba(fg, TEXT_ALPHAS[2]), 4: rgba(fg, TEXT_ALPHAS[3]), }, border: { subtle: rgba(fg, borders.subtle), default: rgba(fg, borders.default), strong: rgba(fg, borders.strong), }, };}
/** The design-token defaults in DerivedChrome shape. */export function defaultChrome(isDark: boolean): DerivedChrome { return { surface: isDark ? surfaces.dark : surfaces.light, text: isDark ? textAlphas.dark : textAlphas.light, border: isDark ? borderAlphas.dark : borderAlphas.light, };}
/** The default pairs, as an operator would enter them. */export const DEFAULT_CHROME: { dark: ChromeColors; light: ChromeColors } = { dark: { background: surfaces.dark[0], foreground: "#ffffff" }, light: { background: surfaces.light[0], foreground: "#09090b" },};