History

fix(web): keep custom palettes readable

t3-code[bot] committed 3 months ago

Commit f0e4ec41 parent d3c46444 files · +10 −39Browse files

Unified · Split

4 files
  1. apps/web/index.html +2 −12
  2. apps/web/src/hooks/useThemeColors.test.ts +0 −9
  3. apps/web/src/hooks/useThemeColors.ts +2 −13
  4. apps/web/src/index.css +6 −5
apps/web/index.html+2−12
@@ -22,19 +22,9 @@ · 21 unmodified lines
22 const isHexColor = (value) =>22 const isHexColor = (value) =>
23 typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value.trim());23 typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value.trim());
24 if (isHexColor(colors?.accentColor)) {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 document.documentElement.style.setProperty(25 document.documentElement.style.setProperty(
36 "--theme-accent-foreground",26 "--theme-accent-seed",
37 luminance >= 0.179 ? "#000000" : "#ffffff",27 colors.accentColor.trim().toLowerCase(),
38 );28 );
39 }29 }
40 if (isHexColor(colors?.neutralColor)) {30 if (isHexColor(colors?.neutralColor)) {
apps/web/src/hooks/useThemeColors.test.ts+0−9
@@ -86,17 +86,8 @@ describe("theme colors", () => { · 85 unmodified lines
86 applyThemeColors({ accentColor: "#16a34a", neutralColor: "#78716c", contrast: 50 });86 applyThemeColors({ accentColor: "#16a34a", neutralColor: "#78716c", contrast: 50 });
87 87
88 expect(setProperty).toHaveBeenCalledWith("--theme-accent-seed", "#16a34a");88 expect(setProperty).toHaveBeenCalledWith("--theme-accent-seed", "#16a34a");
89 expect(setProperty).toHaveBeenCalledWith("--theme-accent-foreground", "#000000");
90 expect(setProperty).toHaveBeenCalledWith("--theme-neutral-seed", "#78716c");89 expect(setProperty).toHaveBeenCalledWith("--theme-neutral-seed", "#78716c");
91 expect(setProperty).toHaveBeenCalledWith("--theme-accent-strength", "16%");90 expect(setProperty).toHaveBeenCalledWith("--theme-accent-strength", "16%");
92 expect(setProperty).toHaveBeenCalledWith("--theme-border-strength", "16%");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});
apps/web/src/hooks/useThemeColors.ts+2−13
@@ -1,4 +1,5 @@
1import { useCallback, useSyncExternalStore } from "react";1import { useCallback, useSyncExternalStore } from "react";
2import { syncBrowserChromeTheme } from "./useTheme";
2 3
3export type ThemeColors = {4export type ThemeColors = {
4 accentColor: string;5 accentColor: string;
@@ -36,18 +37,6 @@ export function normalizeThemeColors(value: unknown): ThemeColors { · 31 unmodified lines
36 };37 };
37}38}
38 39
39export 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
51function getThemeContrastProperties(contrast: number): Record<string, string> {40function getThemeContrastProperties(contrast: number): Record<string, string> {
52 return {41 return {
53 "--theme-background-strength": `${4 + contrast * 0.12}%`,42 "--theme-background-strength": `${4 + contrast * 0.12}%`,
@@ -72,11 +61,11 @@ export function applyThemeColors(colors: ThemeColors): void { · 18 unmodified lines
72 if (typeof document === "undefined") return;61 if (typeof document === "undefined") return;
73 const rootStyle = document.documentElement.style;62 const rootStyle = document.documentElement.style;
74 rootStyle.setProperty("--theme-accent-seed", colors.accentColor);63 rootStyle.setProperty("--theme-accent-seed", colors.accentColor);
75 rootStyle.setProperty("--theme-accent-foreground", getThemeAccentForeground(colors.accentColor));
76 rootStyle.setProperty("--theme-neutral-seed", colors.neutralColor);64 rootStyle.setProperty("--theme-neutral-seed", colors.neutralColor);
77 for (const [property, value] of Object.entries(getThemeContrastProperties(colors.contrast))) {65 for (const [property, value] of Object.entries(getThemeContrastProperties(colors.contrast))) {
78 rootStyle.setProperty(property, value);66 rootStyle.setProperty(property, value);
79 }67 }
68 syncBrowserChromeTheme();
80}69}
81 70
82function emitChange(): void {71function emitChange(): void {
apps/web/src/index.css+6−5
@@ -854,7 +854,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil · 853 unmodified lines
854 color-scheme: light;854 color-scheme: light;
855 --radius: 0.625rem;855 --radius: 0.625rem;
856 --theme-accent-seed: #3b5bdb;856 --theme-accent-seed: #3b5bdb;
857 --theme-accent-foreground: #ffffff;
858 --theme-neutral-seed: #737373;857 --theme-neutral-seed: #737373;
859 --theme-background-strength: 10%;858 --theme-background-strength: 10%;
860 --theme-surface-strength: 14%;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 --card-foreground: var(--foreground);872 --card-foreground: var(--foreground);
874 --popover: var(--card);873 --popover: var(--card);
875 --popover-foreground: var(--foreground);874 --popover-foreground: var(--foreground);
876 --primary: var(--theme-accent-seed);875 /* Keep text, rings, and translucent primary controls readable for any seed.
877 --primary-foreground: var(--theme-accent-foreground);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 --secondary: color-mix(879 --secondary: color-mix(
879 in srgb,880 in srgb,
880 var(--theme-neutral-seed) var(--theme-control-strength),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 var(--background)933 var(--background)
933 );934 );
934 --popover-foreground: var(--foreground);935 --popover-foreground: var(--foreground);
935 --primary: var(--theme-accent-seed);936 --primary: color-mix(in oklch, var(--theme-accent-seed) 40%, var(--color-white));
936 --primary-foreground: var(--theme-accent-foreground);937 --primary-foreground: var(--color-black);
937 --secondary: color-mix(938 --secondary: color-mix(
938 in srgb,939 in srgb,
939 var(--theme-neutral-seed) var(--theme-surface-strength),940 var(--theme-neutral-seed) var(--theme-surface-strength),