import { describe, expect, it } from "vite-plus/test"; import { EMBER_THEME, GROVE_THEME, IRIS_THEME, OCEAN_THEME, T3_CHAT_THEME, themeColorToHex, type ThemeColors, } from "../../themePalette"; import { clerkAppearance } from "./clerkAppearance"; function contrastRatio(first: string, second: string): number { const toRgb = (value: string) => { const hex = themeColorToHex(value)?.slice(1, 7); if (!hex) throw new Error(`Expected a theme color, received ${value}`); return [0, 1, 2].map( (channel) => Number.parseInt(hex.slice(channel * 2, channel * 2 + 2), 16) / 255, ); }; const luminance = (value: string) => toRgb(value) .map((channel) => (channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4)) .reduce((sum, channel, index) => sum + channel * [0.2126, 0.7152, 0.0722][index]!, 0); const lighter = Math.max(luminance(first), luminance(second)); const darker = Math.min(luminance(first), luminance(second)); return (lighter + 0.05) / (darker + 0.05); } function mixThemeColors(first: string, second: string, firstWeight: number): string { const channels = [first, second].map((value) => { const hex = themeColorToHex(value)?.slice(1, 7); if (!hex) throw new Error(`Expected a theme color, received ${value}`); return [0, 1, 2].map((channel) => Number.parseInt(hex.slice(channel * 2, channel * 2 + 2), 16)); }); const mixed = channels[0]!.map((channel, index) => Math.round(channel * firstWeight + channels[1]![index]! * (1 - firstWeight)), ); return `#${mixed.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`; } const builtInThemeModes = [ T3_CHAT_THEME, GROVE_THEME, OCEAN_THEME, EMBER_THEME, IRIS_THEME, ].flatMap((theme) => [theme.colors, theme.variants?.dark].filter((colors): colors is ThemeColors => !!colors), ); describe("clerkAppearance", () => { it("maps theme colors without overriding Clerk's component structure", () => { expect(clerkAppearance).toEqual({ variables: { colorPrimary: "var(--update-foreground)", colorPrimaryForeground: "var(--card)", colorDanger: "var(--error)", colorSuccess: "var(--success)", colorWarning: "var(--warning)", colorNeutral: "var(--foreground)", colorForeground: "var(--foreground)", colorMuted: "color-mix(in srgb, var(--card) 98%, var(--foreground))", colorMutedForeground: "var(--muted-foreground)", colorBackground: "var(--card)", colorInputForeground: "var(--foreground)", colorInput: "var(--secondary)", colorRing: "var(--ring)", }, elements: { formFieldErrorText: { color: "var(--error-foreground)" }, formFieldWarningText: { color: "var(--warning-foreground)" }, formFieldSuccessText: { color: "var(--success-foreground)" }, otpCodeFieldErrorText: { color: "var(--error-foreground)" }, otpCodeFieldSuccessText: { color: "var(--success-foreground)" }, }, }); }); it.each(builtInThemeModes)("keeps Clerk text readable across a built-in palette", (colors) => { const mutedSurface = mixThemeColors(colors.surface, colors.text, 0.98); expect(contrastRatio(colors.text, colors.surface)).toBeGreaterThanOrEqual(4.5); expect(contrastRatio(colors.mutedForeground, mutedSurface)).toBeGreaterThanOrEqual(4.5); expect(contrastRatio(colors.text, colors.secondary)).toBeGreaterThanOrEqual(4.5); expect(contrastRatio(colors.updateForeground, colors.surface)).toBeGreaterThanOrEqual(4.5); expect(contrastRatio(colors.errorForeground, colors.surface)).toBeGreaterThanOrEqual(4.5); expect(contrastRatio(colors.warningForeground, colors.surface)).toBeGreaterThanOrEqual(4.5); }); });