Fix automatic readable text contrast for public and admin themes.
Local Build and Deploy / deploy (push) Successful in 55s
Local Build and Deploy / deploy (push) Successful in 55s
Derive admin/public text colors from WCAG contrast, unify ThemeVars CSS emission, and keep navbar overrides in sync with readable vars. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
9d4d409b77
commit
d7278c77f9
9 files changed
+205
-176
No files matched your search
@@ -130,6 +130,48 @@ describe("deriveAdminPalette", () => {
|
||||
expect(admin.canvas).not.toBe("#ff00ff");
|
||||
expect(admin.surface).not.toBe("#00ff00");
|
||||
});
|
||||
|
||||
it("repairs default light text when admin canvas is forced light", () => {
|
||||
const admin = deriveAdminPalette(
|
||||
{ color_primary: "#f59e0b" },
|
||||
{
|
||||
canvas: "#ffffff",
|
||||
surface: "#f8fafc",
|
||||
surfaceElevated: "#f1f5f9",
|
||||
sidebar: "#ffffff",
|
||||
},
|
||||
);
|
||||
expect(contrastRatio(admin.text, admin.canvas)).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrastRatio(admin.text, admin.surface)).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrastRatio(admin.muted, admin.surface)).toBeGreaterThanOrEqual(
|
||||
4.5,
|
||||
);
|
||||
expect(
|
||||
contrastRatio(admin.sidebarText, admin.sidebar),
|
||||
).toBeGreaterThanOrEqual(4.5);
|
||||
expect(
|
||||
contrastRatio(admin.accentForeground, admin.accent),
|
||||
).toBeGreaterThanOrEqual(4.5);
|
||||
});
|
||||
|
||||
it("repairs hostile admin text overrides on dark canvas", () => {
|
||||
const admin = deriveAdminPalette(
|
||||
{ color_primary: "#111827" },
|
||||
{
|
||||
canvas: "#0b0f19",
|
||||
surface: "#151b27",
|
||||
text: "#0b0f19",
|
||||
muted: "#111827",
|
||||
},
|
||||
);
|
||||
expect(contrastRatio(admin.text, admin.canvas)).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrastRatio(admin.muted, admin.surface)).toBeGreaterThanOrEqual(
|
||||
4.5,
|
||||
);
|
||||
expect(
|
||||
contrastRatio(admin.accentForeground, admin.accent),
|
||||
).toBeGreaterThanOrEqual(4.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("derivePublicForegrounds", () => {
|
||||
|
||||
+38
-22
@@ -31,7 +31,7 @@ export function deriveAdminPalette(
|
||||
colors: PublicThemeColors,
|
||||
overrides?: Partial<AdminPalette>,
|
||||
): AdminPalette {
|
||||
const accent = colors.color_primary ?? "#f59e0b";
|
||||
const accent = overrides?.accent ?? colors.color_primary ?? "#f59e0b";
|
||||
const defaults: AdminPalette = {
|
||||
canvas: "#0b0f19",
|
||||
surface: "#151b27",
|
||||
@@ -40,8 +40,8 @@ export function deriveAdminPalette(
|
||||
muted: "#94a3b8",
|
||||
border: "#2a3448",
|
||||
accent,
|
||||
accentText: readableColor(accent, ["#0b0f19", "#151b27", "#1e2635"]),
|
||||
accentForeground: readableColor("#ffffff", [accent]),
|
||||
accentText: "#f59e0b",
|
||||
accentForeground: "#ffffff",
|
||||
success: "#22c55e",
|
||||
warning: "#eab308",
|
||||
error: "#ef4444",
|
||||
@@ -51,29 +51,35 @@ export function deriveAdminPalette(
|
||||
overlay: "rgba(2, 6, 23, 0.72)",
|
||||
focusRing: accent,
|
||||
};
|
||||
if (!overrides) return defaults;
|
||||
return {
|
||||
|
||||
const merged: AdminPalette = {
|
||||
...defaults,
|
||||
...overrides,
|
||||
accent,
|
||||
accentText: readableColor(accent, [
|
||||
overrides.canvas ?? defaults.canvas,
|
||||
overrides.surface ?? defaults.surface,
|
||||
overrides.surfaceElevated ?? defaults.surfaceElevated,
|
||||
]),
|
||||
accentForeground: readableColor("#ffffff", [accent]),
|
||||
success: overrides.success ?? defaults.success,
|
||||
warning: overrides.warning ?? defaults.warning,
|
||||
error: overrides.error ?? defaults.error,
|
||||
info: overrides.info ?? defaults.info,
|
||||
sidebarText: readableColor(overrides.text ?? defaults.text, [
|
||||
overrides.sidebar ?? defaults.sidebar,
|
||||
overrides.canvas ?? defaults.canvas,
|
||||
overrides.surface ?? defaults.surface,
|
||||
]),
|
||||
overlay: defaults.overlay,
|
||||
overlay: overrides?.overlay ?? defaults.overlay,
|
||||
focusRing: accent,
|
||||
};
|
||||
|
||||
const surfaces = [
|
||||
merged.canvas,
|
||||
merged.surface,
|
||||
merged.surfaceElevated,
|
||||
] as const;
|
||||
const preferredText = merged.text;
|
||||
const preferredMuted = merged.muted;
|
||||
|
||||
return {
|
||||
...merged,
|
||||
text: readableColor(preferredText, surfaces),
|
||||
muted: readableColor(preferredMuted, [merged.canvas, merged.surface]),
|
||||
accentText: readableColor(accent, surfaces),
|
||||
accentForeground: readableColor("#ffffff", [accent]),
|
||||
sidebarText: readableColor(preferredText, [
|
||||
merged.sidebar,
|
||||
merged.canvas,
|
||||
merged.surface,
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
export function derivePublicForegrounds(c: PublicThemeColors) {
|
||||
@@ -181,7 +187,17 @@ export function readableColor(
|
||||
}
|
||||
|
||||
function parseHex(value: string): Rgb | null {
|
||||
const match = /^#([0-9a-f]{6})$/i.exec(value.trim());
|
||||
const trimmed = value.trim();
|
||||
const short = /^#([0-9a-f]{3})$/i.exec(trimmed);
|
||||
if (short) {
|
||||
const [r, g, b] = short[1].split("");
|
||||
return [
|
||||
Number.parseInt(r + r, 16),
|
||||
Number.parseInt(g + g, 16),
|
||||
Number.parseInt(b + b, 16),
|
||||
];
|
||||
}
|
||||
const match = /^#([0-9a-f]{6})$/i.exec(trimmed);
|
||||
if (!match) return null;
|
||||
return [
|
||||
Number.parseInt(match[1].slice(0, 2), 16),
|
||||
|
||||
@@ -40,12 +40,14 @@ export function adminPaletteCss(admin: AdminPalette): string {
|
||||
`--admin-sidebar-background:${admin.sidebar};`,
|
||||
`--admin-sidebar-text-readable:${admin.sidebarText};`,
|
||||
`--admin-sidebar-muted-readable:color-mix(in srgb, var(--admin-sidebar-text-readable) 60%, transparent);`,
|
||||
`--admin-input-text-readable:${readableColor(admin.text, [admin.canvas, admin.surface])};`,
|
||||
`--admin-input-text-readable:${admin.text};`,
|
||||
`--admin-canvas:${admin.canvas};`,
|
||||
`--admin-surface:${admin.surface};`,
|
||||
`--admin-surface-elevated:${admin.surfaceElevated};`,
|
||||
`--admin-text:${admin.text};`,
|
||||
`--admin-text-readable:${admin.text};`,
|
||||
`--admin-text-muted:${admin.muted};`,
|
||||
`--admin-text-muted-readable:${admin.muted};`,
|
||||
`--admin-border:${admin.border};`,
|
||||
`--admin-accent:${admin.accent};`,
|
||||
`--admin-accent-text:${admin.accentText};`,
|
||||
@@ -104,9 +106,7 @@ export function themePaletteCss(
|
||||
`--color-${status}-subtle:color-mix(in srgb, ${palette[`color_${status}`]} 14%, ${palette.color_surface});`,
|
||||
`--color-${status}-border:color-mix(in srgb, ${palette[`color_${status}`]} 38%, transparent);`,
|
||||
]),
|
||||
adminPaletteCss(admin),
|
||||
);
|
||||
if (adminOverrides || selector !== ":root") {
|
||||
declarations.push(adminPaletteCss(admin));
|
||||
}
|
||||
return `${selector}{${declarations.join("")}}`;
|
||||
}
|
||||
Reference in new issue
Block a user