Unified · Split
| @@ -22,19 +22,9 @@ · 21 unmodified lines | ||
| 22 | 22 | const isHexColor = (value) => |
| 23 | 23 | typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value.trim()); |
| 24 | 24 | if (isHexColor(colors?.accentColor)) { |
| 25 | − const accentColor = colors.accentColor.trim().toLowerCase(); | |
| 26 | − const channels = [1, 3, 5].map((offset) => | |
| 27 | − Number.parseInt(accentColor.slice(offset, offset + 2), 16), | |
| 28 | − ); | |
| 29 | − const [red, green, blue] = channels.map((channel) => { | |
| 30 | − const value = channel / 255; | |
| 31 | − return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4; | |
| 32 | − }); | |
| 33 | − const luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue; | |
| 34 | − document.documentElement.style.setProperty("--theme-accent-seed", accentColor); | |
| 35 | 25 | document.documentElement.style.setProperty( |
| 36 | − "--theme-accent-foreground", | |
| 37 | − luminance >= 0.179 ? "#000000" : "#ffffff", | |
| 26 | + "--theme-accent-seed", | |
| 27 | + colors.accentColor.trim().toLowerCase(), | |
| 38 | 28 | ); |
| 39 | 29 | } |
| 40 | 30 | if (isHexColor(colors?.neutralColor)) { |
| @@ -86,17 +86,8 @@ describe("theme colors", () => { · 85 unmodified lines | ||
| 86 | 86 | applyThemeColors({ accentColor: "#16a34a", neutralColor: "#78716c", contrast: 50 }); |
| 87 | 87 | |
| 88 | 88 | expect(setProperty).toHaveBeenCalledWith("--theme-accent-seed", "#16a34a"); |
| 89 | − expect(setProperty).toHaveBeenCalledWith("--theme-accent-foreground", "#000000"); | |
| 90 | 89 | expect(setProperty).toHaveBeenCalledWith("--theme-neutral-seed", "#78716c"); |
| 91 | 90 | expect(setProperty).toHaveBeenCalledWith("--theme-accent-strength", "16%"); |
| 92 | 91 | expect(setProperty).toHaveBeenCalledWith("--theme-border-strength", "16%"); |
| 93 | 92 | }); |
| 94 | − | |
| 95 | − it("chooses a readable foreground for light and dark accents", async () => { | |
| 96 | − vi.stubGlobal("window", createWindow()); | |
| 97 | − const { getThemeAccentForeground } = await import("./useThemeColors"); | |
| 98 | − | |
| 99 | − expect(getThemeAccentForeground("#facc15")).toBe("#000000"); | |
| 100 | − expect(getThemeAccentForeground("#1d4ed8")).toBe("#ffffff"); | |
| 101 | − }); | |
| 102 | 93 | }); |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | import { useCallback, useSyncExternalStore } from "react"; |
| 2 | +import { syncBrowserChromeTheme } from "./useTheme"; | |
| 2 | 3 | |
| 3 | 4 | export type ThemeColors = { |
| 4 | 5 | accentColor: string; |
| @@ -36,18 +37,6 @@ export function normalizeThemeColors(value: unknown): ThemeColors { · 31 unmodified lines | ||
| 36 | 37 | }; |
| 37 | 38 | } |
| 38 | 39 | |
| 39 | −export function getThemeAccentForeground(accentColor: string): "#000000" | "#ffffff" { | |
| 40 | − const channels = [1, 3, 5].map((offset) => | |
| 41 | − Number.parseInt(accentColor.slice(offset, offset + 2), 16), | |
| 42 | − ); | |
| 43 | − const [red = 0, green = 0, blue = 0] = channels.map((channel) => { | |
| 44 | − const value = channel / 255; | |
| 45 | − return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4; | |
| 46 | − }); | |
| 47 | − const luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue; | |
| 48 | − return luminance >= 0.179 ? "#000000" : "#ffffff"; | |
| 49 | −} | |
| 50 | − | |
| 51 | 40 | function getThemeContrastProperties(contrast: number): Record<string, string> { |
| 52 | 41 | return { |
| 53 | 42 | "--theme-background-strength": `${4 + contrast * 0.12}%`, |
| @@ -72,11 +61,11 @@ export function applyThemeColors(colors: ThemeColors): void { · 18 unmodified lines | ||
| 72 | 61 | if (typeof document === "undefined") return; |
| 73 | 62 | const rootStyle = document.documentElement.style; |
| 74 | 63 | rootStyle.setProperty("--theme-accent-seed", colors.accentColor); |
| 75 | − rootStyle.setProperty("--theme-accent-foreground", getThemeAccentForeground(colors.accentColor)); | |
| 76 | 64 | rootStyle.setProperty("--theme-neutral-seed", colors.neutralColor); |
| 77 | 65 | for (const [property, value] of Object.entries(getThemeContrastProperties(colors.contrast))) { |
| 78 | 66 | rootStyle.setProperty(property, value); |
| 79 | 67 | } |
| 68 | + syncBrowserChromeTheme(); | |
| 80 | 69 | } |
| 81 | 70 | |
| 82 | 71 | function emitChange(): void { |
| @@ -854,7 +854,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil · 853 unmodified lines | ||
| 854 | 854 | color-scheme: light; |
| 855 | 855 | --radius: 0.625rem; |
| 856 | 856 | --theme-accent-seed: #3b5bdb; |
| 857 | − --theme-accent-foreground: #ffffff; | |
| 858 | 857 | --theme-neutral-seed: #737373; |
| 859 | 858 | --theme-background-strength: 10%; |
| 860 | 859 | --theme-surface-strength: 14%; |
| @@ -873,8 +872,10 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil · 12 unmodified lines | ||
| 873 | 872 | --card-foreground: var(--foreground); |
| 874 | 873 | --popover: var(--card); |
| 875 | 874 | --popover-foreground: var(--foreground); |
| 876 | − --primary: var(--theme-accent-seed); | |
| 877 | − --primary-foreground: var(--theme-accent-foreground); | |
| 875 | + /* Keep text, rings, and translucent primary controls readable for any seed. | |
| 876 | + Raw seed color still drives the more expressive accent surfaces below. */ | |
| 877 | + --primary: color-mix(in oklch, var(--theme-accent-seed) 46%, var(--color-black)); | |
| 878 | + --primary-foreground: var(--color-white); | |
| 878 | 879 | --secondary: color-mix( |
| 879 | 880 | in srgb, |
| 880 | 881 | var(--theme-neutral-seed) var(--theme-control-strength), |
| @@ -932,8 +933,8 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil · 51 unmodified lines | ||
| 932 | 933 | var(--background) |
| 933 | 934 | ); |
| 934 | 935 | --popover-foreground: var(--foreground); |
| 935 | − --primary: var(--theme-accent-seed); | |
| 936 | − --primary-foreground: var(--theme-accent-foreground); | |
| 936 | + --primary: color-mix(in oklch, var(--theme-accent-seed) 40%, var(--color-white)); | |
| 937 | + --primary-foreground: var(--color-black); | |
| 937 | 938 | --secondary: color-mix( |
| 938 | 939 | in srgb, |
| 939 | 940 | var(--theme-neutral-seed) var(--theme-surface-strength), |