authorgravatar for chris@xenforo.comChris Deeming <chris@xenforo.com> 2026-08-10 17:06:55+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-10 12:06:55-04:00
log0a7c662d39329eeb3cffe00d66a31f1a8241b3d7
tree5c7080cb67e84ec872983324d950140f7a286d52
parentb91a000a17ddb4937397151a0a39a07990ad6473
signature Signed by PGP key B5690EEEBB952194

fix(web): improve built-in theme contrast (#6000)


8 files changed, 51 insertions(+), 36 deletions(-)

apps/web/index.html+2-2
......@@ -37,7 +37,7 @@
3737 light: {
3838 background: "#fdf7fd",
3939 foreground: "#501854",
40 accent: "#e33f86",
40 accent: "#db2777",
4141 chrome: "#fdf7fd",
4242 },
4343 dark: {
......@@ -54,7 +54,7 @@
5454 light: {
5555 background: "#fdf7fd",
5656 foreground: "#501854",
57 accent: "#e33f86",
57 accent: "#db2777",
5858 chrome: "#fdf7fd",
5959 },
6060 dark: {
apps/web/src/components/chat/ModelPickerSidebar.tsx+1-1
......@@ -29,7 +29,7 @@ const SELECTED_INDICATOR_CLASS =
2929 "pointer-events-none absolute -right-1 top-1/2 z-10 h-5 w-0.75 -translate-y-1/2 rounded-l-full bg-primary";
3030const BADGE_BASE_CLASS =
3131 "pointer-events-none absolute -right-0.5 top-0.5 z-10 flex size-3.5 items-center justify-center rounded-full bg-transparent shadow-sm ";
32const NEW_BADGE_CLASS = `${BADGE_BASE_CLASS} text-update `;
32const NEW_BADGE_CLASS = `${BADGE_BASE_CLASS} text-update-foreground `;
3333
3434/** Opens toward the rail so the list stays readable (not over the model names). */
3535const PICKER_TOOLTIP_SIDE = "left" as const;
apps/web/src/components/settings/ProviderInstanceCard.tsx+1-1
......@@ -619,7 +619,7 @@ export function ProviderInstanceCard({
619619 "size-5 rounded-sm p-0",
620620 versionAdvisory.emphasis === "strong"
621621 ? "text-warning hover:text-warning"
622 : "text-update hover:text-update",
622 : "text-update-foreground hover:text-update-foreground",
623623 )}
624624 aria-label="Update available — view details"
625625 >
apps/web/src/components/settings/themeInspector.test.ts+3
......@@ -31,6 +31,9 @@ describe("changedThemePaintKinds", () => {
3131describe("themeRoleFromUtilityClass", () => {
3232 it("maps semantic utilities to their actual palette role", () => {
3333 expect(themeRoleFromUtilityClass("text-foreground", "foreground")).toBe("text");
34 expect(themeRoleFromUtilityClass("text-muted-foreground", "foreground")).toBe(
35 "mutedForeground",
36 );
3437 expect(themeRoleFromUtilityClass("bg-primary/90", "background")).toBe("messageAction");
3538 expect(themeRoleFromUtilityClass("ring-ring", "border")).toBe("focus");
3639 });
apps/web/src/components/settings/themeInspector.ts+1-1
......@@ -38,7 +38,7 @@ const THEME_UTILITY_ROLES: Readonly<Partial<Record<string, ThemeColorRole>>> = {
3838 secondary: "secondary",
3939 "secondary-foreground": "secondaryForeground",
4040 muted: "muted",
41 "muted-foreground": "textMuted",
41 "muted-foreground": "mutedForeground",
4242 placeholder: "placeholder",
4343 "secondary-label": "secondaryLabel",
4444 "icon-muted": "iconMuted",
apps/web/src/index.css+5-5
......@@ -932,7 +932,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
932932 --destructive: var(--error);
933933 --border: var(--color-zinc-200);
934934 --input: var(--color-zinc-300);
935 --ring: oklch(0.488 0.217 264);
935 --ring: var(--primary);
936936 --destructive-foreground: var(--error-foreground);
937937 --info: var(--color-blue-500);
938938 --info-foreground: var(--color-blue-700);
......@@ -976,7 +976,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
976976 --card-foreground: var(--color-neutral-100);
977977 --popover: color-mix(in srgb, var(--background) 94%, var(--color-white));
978978 --popover-foreground: var(--color-neutral-100);
979 --primary: oklch(0.588 0.217 264);
979 --primary: oklch(0.571 0.21 264);
980980 --primary-foreground: var(--color-white);
981981 --secondary: --alpha(var(--color-white) / 4%);
982982 --secondary-foreground: var(--color-neutral-100);
......@@ -998,7 +998,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
998998 --destructive: var(--error);
999999 --border: --alpha(var(--color-white) / 6%);
10001000 --input: --alpha(var(--color-white) / 8%);
1001 --ring: oklch(0.588 0.217 264);
1001 --ring: var(--primary);
10021002 --destructive-foreground: var(--error-foreground);
10031003 --info: var(--color-blue-500);
10041004 --info-foreground: var(--color-blue-400);
......@@ -1109,7 +1109,7 @@ html.dark[data-theme-id] {
11091109 --secondary: var(--app-theme-secondary);
11101110 --secondary-foreground: var(--app-theme-secondary-foreground);
11111111 --muted: var(--app-theme-muted);
1112 --muted-foreground: var(--app-theme-text-muted);
1112 --muted-foreground: var(--app-theme-muted-foreground);
11131113 --placeholder: var(--app-theme-placeholder);
11141114 --secondary-label: var(--app-theme-secondary-label);
11151115 --icon-muted: var(--app-theme-icon-muted);
......@@ -1359,7 +1359,7 @@ html[data-theme-id] [data-app-sidebar] {
13591359 --accent: var(--app-theme-accent-surface);
13601360 --accent-foreground: var(--app-theme-accent-surface-foreground);
13611361 --muted: var(--app-theme-muted);
1362 --muted-foreground: var(--app-theme-text-muted);
1362 --muted-foreground: var(--app-theme-muted-foreground);
13631363 --border: var(--app-theme-border);
13641364 --input: var(--app-theme-input);
13651365 --sidebar: var(--app-theme-sidebar);
apps/web/src/themePalette.test.ts+13-8
......@@ -113,6 +113,8 @@ describe("theme files", () => {
113113 expect(contrastRatio(colors.text, colors.canvas)).toBeGreaterThanOrEqual(7);
114114 expect(contrastRatio(colors.textMuted, colors.canvas)).toBeGreaterThanOrEqual(4.5);
115115 expect(contrastRatio(colors.textMuted, colors.canvas)).toBeLessThan(5.5);
116 expect(contrastRatio(colors.mutedForeground, colors.muted)).toBeGreaterThanOrEqual(4.5);
117 expect(contrastRatio(colors.placeholder, colors.surfaceRaised)).toBeGreaterThanOrEqual(4.5);
116118 expect(colors.secondaryLabel).toBe(colors.textMuted);
117119 expect(contrastRatio(colors.accentForeground, colors.accent)).toBeGreaterThanOrEqual(4.5);
118120 expect(
......@@ -169,7 +171,7 @@ describe("theme files", () => {
169171 colors: {
170172 canvas: "#07152f",
171173 accent: "#67c2ff",
172 placeholder: "#8f8699",
174 placeholder: "#968d9f",
173175 },
174176 });
175177 });
......@@ -315,9 +317,7 @@ describe("theme files", () => {
315317 4.5,
316318 );
317319 expect(contrastRatio(colors.sidebarForeground, colors.sidebar)).toBeGreaterThanOrEqual(4.5);
318 // The live light primary is intended for filled controls and clears the
319 // non-text UI-component threshold rather than normal-text AA.
320 expect(contrastRatio(colors.accentForeground, colors.accent)).toBeGreaterThanOrEqual(3);
320 expect(contrastRatio(colors.accentForeground, colors.accent)).toBeGreaterThanOrEqual(4.5);
321321 }
322322 });
323323
......@@ -340,9 +340,7 @@ describe("theme files", () => {
340340 1,
341341 );
342342 }
343 expect(contrastRatio(colors!.accentForeground, colors!.accent)).toBeGreaterThanOrEqual(
344 theme === T3_CHAT_THEME ? 3 : 4.5,
345 );
343 expect(contrastRatio(colors!.accentForeground, colors!.accent)).toBeGreaterThanOrEqual(4.5);
346344 expect(
347345 contrastRatio(colors!.toolbarControlForeground, colors!.toolbarControl),
348346 ).toBeGreaterThanOrEqual(4.5);
......@@ -351,7 +349,14 @@ describe("theme files", () => {
351349 ).toBeGreaterThanOrEqual(4.5);
352350 expect(
353351 contrastRatio(colors!.messageActionForeground, colors!.messageAction),
354 ).toBeGreaterThanOrEqual(theme === T3_CHAT_THEME ? 3 : 4.5);
352 ).toBeGreaterThanOrEqual(4.5);
353 expect(
354 contrastRatio(colors!.messageActionForeground, colors!.messageActionHover),
355 ).toBeGreaterThanOrEqual(4.5);
356 expect(contrastRatio(colors!.mutedForeground, colors!.muted)).toBeGreaterThanOrEqual(4.5);
357 expect(contrastRatio(colors!.placeholder, colors!.surfaceRaised)).toBeGreaterThanOrEqual(
358 4.5,
359 );
355360 }
356361 }
357362 });
apps/web/src/themePalette.ts+25-18
......@@ -325,6 +325,7 @@ function legacyThemeMode(theme: ThemePreference): ThemeAppearance | null {
325325// Measured from the live t3.chat default theme. Translucent chat surfaces are
326326// flattened over --chat-background so this opaque palette reproduces the
327327// pixels users see after T3 Chat's blur and noise layers are composited.
328// Foreground pairs deviate where necessary to keep normal text at WCAG AA.
328329const T3_CHAT_LIGHT_COLORS: ThemeColors = {
329330 canvas: "#fdf7fd",
330331 // T3 Code's workspace header belongs to the chat panel, so keep it seamless
......@@ -346,31 +347,31 @@ const T3_CHAT_LIGHT_COLORS: ThemeColors = {
346347 border: "#eee1ed",
347348 input: "#e7c1dc",
348349 focus: "#db2777",
349 accent: "#e33f86",
350 accent: "#db2777",
350351 accentForeground: "#ffffff",
351352 secondary: "#f1c4e6",
352353 secondaryForeground: "#77347c",
353354 muted: "#eaa7cb",
354 mutedForeground: "#ac1668",
355 placeholder: "#ad83b0",
355 mutedForeground: "#8d1255",
356 placeholder: "#8b5f90",
356357 secondaryLabel: "#ac1668",
357358 iconMuted: "#ac1668",
358359 error: "#f7086c",
359360 errorForeground: "#9d174d",
360361 errorSurface: "#fde4f1",
361362 warning: "#f59e0b",
362 warningForeground: "#b45309",
363 warningForeground: "#b05109",
363364 warningSurface: "#fcf0ea",
364 update: "#e33f86",
365 update: "#db2777",
365366 updateForeground: "#ac1668",
366367 updateSurface: "#fadfef",
367368 accentSurface: "#f3e6f5",
368369 accentSurfaceForeground: "#454554",
369370 messageSurface: "#f7def2",
370371 messageForeground: "#492c61",
371 messageAction: "#e33f86",
372 messageAction: "#db2777",
372373 messageActionForeground: "#ffffff",
373 messageActionHover: "#d56698",
374 messageActionHover: "#c12269",
374375 // T3 Chat uses a light lavender code surface in light mode. Keeping the
375376 // dark plum pair here also leaked the dark palette into T3 Code's diffs.
376377 codeBackground: "#f5ecf9",
......@@ -422,7 +423,7 @@ const T3_CHAT_DARK_COLORS: ThemeColors = {
422423 secondaryForeground: "#d4c7e1",
423424 muted: "#423a45",
424425 mutedForeground: "#e7d0dd",
425 placeholder: "#8f8699",
426 placeholder: "#968d9f",
426427 secondaryLabel: "#e7d0dd",
427428 iconMuted: "#d4c7e1",
428429 error: "#9d174d",
......@@ -549,8 +550,8 @@ const T3_CODE_DARK_THEME_COLORS: ThemeColors = {
549550 textMuted: "#818181",
550551 border: "#191919",
551552 input: "#1e1e1e",
552 focus: "#366ffb",
553 accent: "#366ffb",
553 focus: "#346bf1",
554 accent: "#346bf1",
554555 accentForeground: "#ffffff",
555556 secondary: "#141414",
556557 secondaryForeground: "#f5f5f5",
......@@ -565,16 +566,16 @@ const T3_CODE_DARK_THEME_COLORS: ThemeColors = {
565566 warning: "#fe9a00",
566567 warningForeground: "#ffb900",
567568 warningSurface: "#312108",
568 update: "#366ffb",
569 update: "#346bf1",
569570 updateForeground: "#51a2ff",
570 updateSurface: "#121c35",
571 updateSurface: "#121b34",
571572 accentSurface: "#141414",
572573 accentSurfaceForeground: "#f5f5f5",
573574 messageSurface: "#141414",
574575 messageForeground: "#f5f5f5",
575 messageAction: "#366ffb",
576 messageAction: "#346bf1",
576577 messageActionForeground: "#ffffff",
577 messageActionHover: "#3265e3",
578 messageActionHover: "#3061d9",
578579 codeBackground: "#111111",
579580 codeForeground: "#f5f5f5",
580581 sidebar: "#000000",
......@@ -941,12 +942,14 @@ export function createVividThemeColors(
941942 const sidebarRgb = themeOklchToRgb(sidebar);
942943 const surface = surfaceAt(0.015);
943944 const surfaceRaised = surfaceAt(0.05);
945 const surfaceRaisedRgb = themeOklchToRgb(surfaceRaised);
944946 const surfaceOverlay = surfaceAt(0.075);
945947 const border = surfaceAt(dark ? 0.16 : 0.12, Math.min(0.07, accent.C * 0.35));
946948 const input = surfaceAt(dark ? 0.21 : 0.16, Math.min(0.08, accent.C * 0.4));
947949 const secondary = surfaceAt(dark ? 0.1 : 0.06, Math.min(0.09, accent.C * 0.5));
948950 const secondaryRgb = themeOklchToRgb(secondary);
949951 const muted = surfaceAt(dark ? 0.06 : 0.04, Math.min(0.06, accent.C * 0.35));
952 const mutedRgb = themeOklchToRgb(muted);
950953 const accentSurface = surfaceAt(dark ? 0.13 : 0.08, Math.min(0.11, accent.C * 0.55));
951954 const accentSurfaceRgb = themeOklchToRgb(accentSurface);
952955 const messageSurface = surfaceAt(dark ? 0.16 : 0.1, Math.min(0.13, accent.C * 0.6));
......@@ -958,6 +961,8 @@ export function createVividThemeColors(
958961 themeRgbToHexColor(
959962 themeOklchToRgb(solveOklchLightness(textBase, surfaceRgb, 4.6, dark ? "lighter" : "darker")),
960963 );
964 const mutedForeground = foregroundOn(mutedRgb);
965 const placeholder = foregroundOn(surfaceRaisedRgb);
961966
962967 const actionHover: ThemeOklch = { ...action, L: action.L + (dark ? 0.06 : -0.06) };
963968
......@@ -986,8 +991,8 @@ export function createVividThemeColors(
986991 secondary: hex(secondary),
987992 secondaryForeground: foregroundOn(secondaryRgb),
988993 muted: hex(muted),
989 mutedForeground: themeRgbToHexColor(textMutedRgb),
990 placeholder: themeRgbToHexColor(textMutedRgb),
994 mutedForeground,
995 placeholder,
991996 secondaryLabel: themeRgbToHexColor(textMutedRgb),
992997 iconMuted: themeRgbToHexColor(textMutedRgb),
993998 update: themeRgbToHexColor(accentRgb),
......@@ -1174,6 +1179,8 @@ export function createManagedThemeColors(
11741179 const surfaceOverlay = mixThemeRgbColors(canvas, text, appearance === "dark" ? 0.18 : 0.06);
11751180 const secondary = mixThemeRgbColors(canvas, accent, appearance === "dark" ? 0.2 : 0.08);
11761181 const muted = mixThemeRgbColors(canvas, accent, appearance === "dark" ? 0.13 : 0.06);
1182 const mutedForeground = readableThemeText(muted, text, 1, 4.6);
1183 const placeholder = readableThemeText(surfaceRaised, text, 1, 4.6);
11771184 const accentSurface = mixThemeRgbColors(canvas, accent, appearance === "dark" ? 0.3 : 0.14);
11781185 const messageSurface = mixThemeRgbColors(canvas, accent, appearance === "dark" ? 0.36 : 0.18);
11791186 const toolbarControl = mixThemeRgbColors(chrome, accent, appearance === "dark" ? 0.2 : 0.08);
......@@ -1244,8 +1251,8 @@ export function createManagedThemeColors(
12441251 secondary: themeRgbToHexColor(secondary),
12451252 secondaryForeground: themeRgbToHexColor(readableThemeForeground(secondary)),
12461253 muted: themeRgbToHexColor(muted),
1247 mutedForeground: themeRgbToHexColor(textMuted),
1248 placeholder: themeRgbToHexColor(textMuted),
1254 mutedForeground: themeRgbToHexColor(mutedForeground),
1255 placeholder: themeRgbToHexColor(placeholder),
12491256 secondaryLabel: themeRgbToHexColor(textMuted),
12501257 iconMuted: themeRgbToHexColor(textMuted),
12511258 accentSurface: themeRgbToHexColor(accentSurface),