chore(repo): reinitialize repository

This commit is contained in:
2026-03-18 11:29:54 +08:00
commit 24871e213a
288 changed files with 44369 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
import { describe, expect, it } from "vitest";
import themeCss from "./styles/theme-vars.css?raw";
interface ParsedColor {
rgb: [number, number, number];
alpha: number;
}
function parseThemeVars() {
const matches = [
...themeCss.matchAll(/:root([^{]*)\{([\s\S]*?)\n {2}\}/g),
];
const byTheme: Record<string, Record<string, string>> = {};
for (const match of matches) {
const selector = match[1];
const body = match[2];
const themes = [
...selector.matchAll(/data-theme="([^"]+)"/g),
].map((themeMatch) => themeMatch[1]);
if (themes.length === 0) continue;
const vars: Record<string, string> = {};
for (const varMatch of body.matchAll(/--([\w-]+):\s*([^;]+);/g)) {
vars[varMatch[1]] = varMatch[2].trim();
}
for (const theme of themes) {
byTheme[theme] = { ...(byTheme[theme] ?? {}), ...vars };
}
}
return byTheme;
}
function parseColor(value: string | undefined): ParsedColor {
if (!value) {
throw new Error("Missing color token.");
}
const hexMatch = value.match(/^#([\da-f]{3,8})$/i);
if (hexMatch) {
let hex = hexMatch[1];
if (hex.length === 3) {
hex = hex
.split("")
.map((char) => char + char)
.join("");
}
let alpha = 1;
if (hex.length === 8) {
alpha = parseInt(hex.slice(6), 16) / 255;
hex = hex.slice(0, 6);
}
return {
rgb: [
parseInt(hex.slice(0, 2), 16),
parseInt(hex.slice(2, 4), 16),
parseInt(hex.slice(4, 6), 16),
],
alpha,
};
}
const rgbMatch = value.match(/^rgba?\(([^)]+)\)$/i);
if (rgbMatch) {
const parts = rgbMatch[1].split(",").map((part) => part.trim());
return {
rgb: [
Number(parts[0]),
Number(parts[1]),
Number(parts[2]),
],
alpha: parts[3] === undefined ? 1 : Number(parts[3]),
};
}
throw new Error(`Unsupported color format: ${value}`);
}
function blendColor(foreground: ParsedColor, background: ParsedColor): ParsedColor {
if (foreground.alpha >= 1) return foreground;
return {
rgb: foreground.rgb.map((channel, index) =>
channel * foreground.alpha + background.rgb[index] * (1 - foreground.alpha),
) as [number, number, number],
alpha: 1,
};
}
function relativeChannel(channel: number) {
const normalized = channel / 255;
return normalized <= 0.03928
? normalized / 12.92
: ((normalized + 0.055) / 1.055) ** 2.4;
}
function luminance(color: ParsedColor) {
const [r, g, b] = color.rgb;
return (
0.2126 * relativeChannel(r)
+ 0.7152 * relativeChannel(g)
+ 0.0722 * relativeChannel(b)
);
}
function contrastRatio(foreground: ParsedColor, background: ParsedColor) {
const a = luminance(foreground);
const b = luminance(background);
const [lighter, darker] = a >= b ? [a, b] : [b, a];
return (lighter + 0.05) / (darker + 0.05);
}
describe("theme contrast tokens", () => {
const themeVars = parseThemeVars();
const themeIds = Object.keys(themeVars);
it("keeps secondary text readable on the dashboard surfaces", () => {
for (const themeId of themeIds) {
const vars = themeVars[themeId];
const surface = parseColor(vars["app-surface"]);
const elevated = parseColor(vars["app-surface-elevated"]);
const muted = parseColor(vars["app-text-muted"]);
const soft = parseColor(vars["app-text-soft"]);
const faint = parseColor(vars["app-text-faint"]);
expect(
contrastRatio(muted, surface),
`${themeId} muted text should stay comfortably readable on the base surface`,
).toBeGreaterThanOrEqual(7);
expect(
contrastRatio(soft, surface),
`${themeId} soft text should stay readable on the base surface`,
).toBeGreaterThanOrEqual(5);
expect(
contrastRatio(faint, elevated),
`${themeId} faint text should still clear caption contrast on elevated surfaces`,
).toBeGreaterThanOrEqual(4.5);
}
});
it("preserves a descending text hierarchy in every theme", () => {
for (const themeId of themeIds) {
const vars = themeVars[themeId];
const surface = parseColor(vars["app-surface"]);
const muted = parseColor(vars["app-text-muted"]);
const soft = parseColor(vars["app-text-soft"]);
const faint = parseColor(vars["app-text-faint"]);
const mutedContrast = contrastRatio(muted, surface);
const softContrast = contrastRatio(soft, surface);
const faintContrast = contrastRatio(faint, surface);
expect(
mutedContrast,
`${themeId} muted text should remain stronger than soft text`,
).toBeGreaterThan(softContrast);
expect(
softContrast,
`${themeId} soft text should remain stronger than faint text`,
).toBeGreaterThan(faintContrast);
}
});
it("keeps semantic chips readable when translucent fills sit on app surfaces", () => {
const chipPairs = [
["app-attention-text", "app-attention-background"],
["app-success-text", "app-success-background"],
["app-danger-text", "app-danger-background"],
["app-info-text", "app-info-background"],
["role-leader-badge-text", "role-leader-badge-bg"],
["role-worker-badge-text", "role-worker-badge-bg"],
["stage-plan-text", "stage-plan-bg"],
["stage-review-text", "stage-review-bg"],
["stage-freeze-text", "stage-freeze-bg"],
["stage-execution-text", "stage-execution-bg"],
["stage-verification-text", "stage-verification-bg"],
] as const;
for (const themeId of themeIds) {
const vars = themeVars[themeId];
const surface = parseColor(vars["app-surface"]);
for (const [textToken, backgroundToken] of chipPairs) {
const text = parseColor(vars[textToken]);
const translucentBackground = parseColor(vars[backgroundToken]);
const background = blendColor(translucentBackground, surface);
expect(
contrastRatio(text, background),
`${themeId} ${textToken} on ${backgroundToken} should remain readable`,
).toBeGreaterThanOrEqual(4.5);
}
}
});
});