From 8622cba9c6a933e034b942c3ea2d18e104ac7ccf Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Sat, 15 Aug 2026 17:44:42 +0200 Subject: [PATCH 1/4] refactor(web): simplify advanced theme controls --- .../components/settings/ThemeEditorPanel.tsx | 264 +++++++++++++----- apps/web/src/themePalette.ts | 159 +++++++++++ docs/user/keybindings.md | 4 +- 3 files changed, 360 insertions(+), 67 deletions(-) diff --git a/apps/web/src/components/settings/ThemeEditorPanel.tsx b/apps/web/src/components/settings/ThemeEditorPanel.tsx index 4d7300d1f050..94c75e9fdc53 100644 --- a/apps/web/src/components/settings/ThemeEditorPanel.tsx +++ b/apps/web/src/components/settings/ThemeEditorPanel.tsx @@ -20,8 +20,10 @@ import { parseThemeFile, removeCustomTheme, themeIdFromName, + updateThemeColorFamily, updateCustomTheme, type ThemeAppearance, + type ThemeColors, type ThemeColorRole, type ThemeDefinition, } from "../../themePalette"; @@ -42,62 +44,174 @@ import { type ThemeElementInspection, } from "./themeInspector"; -const THEME_EDITOR_PRIMARY_ROLES: ReadonlyArray = [ - "canvas", - "chrome", - "sidebar", - "surface", - "text", - "textMuted", - "placeholder", - "secondaryLabel", - "iconMuted", - "accent", - "messageSurface", - "messageAction", -]; - const THEME_EDITOR_SIMPLE_ROLES: ReadonlyArray = ["canvas", "accent"]; -const THEME_EDITOR_STATUS_ROLES: ReadonlyArray = [ - "error", - "errorForeground", - "errorSurface", - "warning", - "warningForeground", - "warningSurface", - "update", - "updateForeground", - "updateSurface", -]; - -const THEME_EDITOR_ADVANCED_ROLES = THEME_COLOR_ROLES.filter( - (role) => !THEME_EDITOR_PRIMARY_ROLES.includes(role) && !THEME_EDITOR_STATUS_ROLES.includes(role), -); +type ThemeEditorColorFamily = Readonly<{ + id: string; + label: string; + role: ThemeColorRole; + roles: ReadonlyArray; +}>; const THEME_EDITOR_ROLE_GROUPS: ReadonlyArray<{ id: string; title: string; - roles: ReadonlyArray; + families: ReadonlyArray; }> = [ { - id: "main", - title: "Main colors", - roles: THEME_EDITOR_PRIMARY_ROLES, + id: "foundation", + title: "Foundation", + families: [ + { + id: "background", + label: "Background", + role: "canvas", + roles: ["canvas", "chrome", "toolbar"], + }, + { id: "surface", label: "Surface", role: "surface", roles: ["surface"] }, + { + id: "raised-surface", + label: "Raised surface", + role: "surfaceRaised", + roles: ["surfaceRaised"], + }, + { + id: "overlay", + label: "Overlay", + role: "surfaceOverlay", + roles: ["surfaceOverlay"], + }, + { + id: "text", + label: "Text", + role: "text", + roles: ["text", "toolbarForeground", "toolbarControlForeground"], + }, + { + id: "muted-text", + label: "Muted text", + role: "mutedForeground", + roles: [ + "mutedForeground", + "placeholder", + "secondaryLabel", + "iconMuted", + "sidebarMutedForeground", + ], + }, + { + id: "border", + label: "Border", + role: "border", + roles: ["border", "toolbarBorder", "sidebarBorder"], + }, + { id: "input", label: "Input", role: "input", roles: ["input"] }, + ], }, { - id: "status", - title: "Status colors", - roles: THEME_EDITOR_STATUS_ROLES, + id: "brand-content", + title: "Brand & content", + families: [ + { + id: "subtle-surface", + label: "Subtle surface", + role: "secondary", + roles: ["secondary", "secondaryForeground", "muted", "toolbarControl"], + }, + { + id: "highlight-surface", + label: "Highlight surface", + role: "accentSurface", + roles: ["accentSurface", "accentSurfaceForeground", "toolbarControlHover"], + }, + { + id: "accent", + label: "Accent", + role: "accent", + roles: ["accent", "focus", "update", "updateForeground", "updateSurface", "terminalCursor"], + }, + { + id: "action", + label: "Action", + role: "messageAction", + roles: ["messageAction", "messageActionForeground", "messageActionHover"], + }, + { + id: "message-surface", + label: "Message surface", + role: "messageSurface", + roles: ["messageSurface", "messageForeground"], + }, + { + id: "code-surface", + label: "Code surface", + role: "codeBackground", + roles: ["codeBackground", "codeForeground"], + }, + ], }, { - id: "additional", - title: "Other colors", - roles: THEME_EDITOR_ADVANCED_ROLES, + id: "context", + title: "Context", + families: [ + { + id: "sidebar-background", + label: "Sidebar background", + role: "sidebar", + roles: ["sidebar", "sidebarForeground"], + }, + { + id: "sidebar-controls", + label: "Sidebar controls", + role: "sidebarControlSurface", + roles: ["sidebarControlSurface"], + }, + { + id: "sidebar-selection", + label: "Sidebar selection", + role: "sidebarRowSelected", + roles: ["sidebarRowHover", "sidebarRowActive", "sidebarRowSelected"], + }, + { + id: "terminal-background", + label: "Terminal background", + role: "terminalBackground", + roles: ["terminalBackground", "terminalForeground", "terminalSelection"], + }, + ], + }, + { + id: "status", + title: "Status", + families: [ + { + id: "error", + label: "Error", + role: "error", + roles: ["error", "errorForeground", "errorSurface"], + }, + { + id: "warning", + label: "Warning", + role: "warning", + roles: ["warning", "warningForeground", "warningSurface"], + }, + ], }, ]; -type ThemeEditorColors = Record; +const THEME_EDITOR_COLOR_FAMILIES = THEME_EDITOR_ROLE_GROUPS.flatMap((group) => group.families); +const THEME_EDITOR_COLOR_FAMILY_BY_ROLE = new Map( + THEME_EDITOR_COLOR_FAMILIES.flatMap((family) => + family.roles.map((role) => [role, family] as const), + ), +); + +function getThemeEditorColorFamily(role: ThemeColorRole): ThemeEditorColorFamily | null { + return THEME_EDITOR_COLOR_FAMILY_BY_ROLE.get(role) ?? null; +} + +type ThemeEditorColors = ThemeColors; type ThemeEditorColorsByAppearance = Record; // A draft with no source theme starts as the standard T3 Code look — the @@ -348,9 +462,11 @@ export function ThemeEditorPanel({ return { ...current, - [activeAppearance]: shouldManageColors - ? getManagedEditorColors(activeAppearance, nextColors) - : nextColors, + [activeAppearance]: isAdvanced + ? updateThemeColorFamily(activeAppearance, current[activeAppearance], role, value) + : shouldManageColors + ? getManagedEditorColors(activeAppearance, nextColors) + : nextColors, }; }); if (!isAdvanced && THEME_EDITOR_SIMPLE_ROLES.includes(role) && isThemeEditorColor(value)) { @@ -364,7 +480,8 @@ export function ThemeEditorPanel({ ); const selectThemeRole = useCallback((role: ThemeColorRole, reveal = false) => { - setSelectedRole(role); + const visibleRole = getThemeEditorColorFamily(role)?.role ?? role; + setSelectedRole(visibleRole); if (!THEME_EDITOR_SIMPLE_ROLES.includes(role)) { setIsAdvanced(true); setRoleQuery(""); @@ -373,7 +490,7 @@ export function ThemeEditorPanel({ requestAnimationFrame(() => { panelRef.current - ?.querySelector(`[data-theme-color-role="${role}"]`) + ?.querySelector(`[data-theme-color-role="${visibleRole}"]`) ?.scrollIntoView({ behavior: "smooth", block: "nearest" }); }); }, []); @@ -389,13 +506,15 @@ export function ThemeEditorPanel({ }, []); const selectedHighlightRoles = selectedRole - ? !isAdvanced && THEME_EDITOR_SIMPLE_ROLES.includes(selectedRole) - ? THEME_COLOR_ROLES.filter( - (role) => - colorsByAppearance[activeAppearance][role].trim().toLowerCase() === - colorsByAppearance[activeAppearance][selectedRole].trim().toLowerCase(), - ) - : [selectedRole] + ? isAdvanced + ? (getThemeEditorColorFamily(selectedRole)?.roles ?? [selectedRole]) + : THEME_EDITOR_SIMPLE_ROLES.includes(selectedRole) + ? THEME_COLOR_ROLES.filter( + (role) => + colorsByAppearance[activeAppearance][role].trim().toLowerCase() === + colorsByAppearance[activeAppearance][selectedRole].trim().toLowerCase(), + ) + : [selectedRole] : []; const selectedHighlightRolesKey = selectedHighlightRoles.join(","); @@ -490,7 +609,10 @@ export function ThemeEditorPanel({ }; const showInspection = (inspection: ThemeElementInspection) => { hoverInspection = inspection; - showThemeInspectorHover(inspection, getThemeRoleLabel(inspection.role)); + showThemeInspectorHover( + inspection, + getThemeEditorColorFamily(inspection.role)?.label ?? getThemeRoleLabel(inspection.role), + ); }; const handlePointerOver = (event: PointerEvent) => { const target = event.target; @@ -553,7 +675,11 @@ export function ThemeEditorPanel({ hoverFrame ??= requestAnimationFrame(() => { hoverFrame = null; if (hoverInspection) { - showThemeInspectorHover(hoverInspection, getThemeRoleLabel(hoverInspection.role)); + showThemeInspectorHover( + hoverInspection, + getThemeEditorColorFamily(hoverInspection.role)?.label ?? + getThemeRoleLabel(hoverInspection.role), + ); } }); }; @@ -854,19 +980,20 @@ export function ThemeEditorPanel({ ); const renderRoleFields = ( - roles: ReadonlyArray, + families: ReadonlyArray, gridClassName = "grid gap-2 sm:grid-cols-2", ) => (
- {roles.map((role) => ( + {families.map((family) => ( ))}
@@ -876,16 +1003,21 @@ export function ThemeEditorPanel({ const query = roleQuery.trim().toLowerCase(); const groups = THEME_EDITOR_ROLE_GROUPS.map((group) => ({ ...group, - roles: group.roles.filter( - (role) => !query || getThemeRoleLabel(role).toLowerCase().includes(query), + families: group.families.filter( + (family) => + !query || + [family.label, ...family.roles.map((role) => getThemeRoleLabel(role))] + .join(" ") + .toLowerCase() + .includes(query), ), - })).filter((group) => group.roles.length > 0); + })).filter((group) => group.families.length > 0); return isAdvanced ? (
{groups.map((group) => (

{group.title}

- {renderRoleFields(group.roles, "grid gap-1")} + {renderRoleFields(group.families, "grid gap-1")}
))} {groups.length === 0 ?

No matches.

: null} @@ -1018,7 +1150,7 @@ export function ThemeEditorPanel({ {isInspecting ? "Select an element · Esc to cancel" : selectedRole - ? `${getThemeRoleLabel(selectedRole)} · ${usageCount ?? 0} ${usageCount === 1 ? "use" : "uses"}` + ? `${isAdvanced ? (getThemeEditorColorFamily(selectedRole)?.label ?? getThemeRoleLabel(selectedRole)) : getThemeRoleLabel(selectedRole)} · ${usageCount ?? 0} ${usageCount === 1 ? "use" : "uses"}` : "Select a color below"}

)} diff --git a/apps/web/src/themePalette.ts b/apps/web/src/themePalette.ts index 7bf4f0426bd3..a2147ef3a7df 100644 --- a/apps/web/src/themePalette.ts +++ b/apps/web/src/themePalette.ts @@ -1468,6 +1468,165 @@ function themeActionColors( }; } +/** + * Update one Advanced-editor color family without normalizing the rest of an + * imported or hand-tuned palette. The editor exposes a representative role + * for each family; paired foregrounds and nearby states are derived only when + * that representative is changed. + */ +export function updateThemeColorFamily( + appearance: ThemeAppearance, + colors: ThemeColors, + role: ThemeColorRole, + value: string, +): ThemeColors { + const normalized = toCanonicalThemeColor(value); + if (!normalized) return { ...colors, [role]: value }; + + const canvas = parseThemeRgbColor( + colors.canvas, + appearance === "dark" ? { r: 24, g: 15, b: 27 } : { r: 250, g: 245, b: 250 }, + ); + const selected = parseThemeRgbColor(normalized, canvas); + const accent = parseThemeRgbColor(colors.accent, { r: 168, g: 67, b: 112 }); + const canvasIsDark = themeRelativeLuminance(canvas) < 0.179; + const terminalIsDark = themeRelativeLuminance(selected) < 0.179; + const colorOf = (color: ThemeRgbColor) => themeRgbToThemeColor(color); + const foregroundOn = (background: ThemeRgbColor) => colorOf(readableThemeForeground(background)); + const statusColors = () => { + const surface = mixThemeRgbColors(canvas, selected, canvasIsDark ? 0.16 : 0.08); + const foreground = solveOklchLightness( + themeRgbToOklch(selected), + surface, + 4.6, + canvasIsDark ? "lighter" : "darker", + ); + return { + foreground: themeOklchToThemeColor(foreground), + surface: colorOf(surface), + }; + }; + + switch (role) { + case "canvas": + return { ...colors, canvas: normalized, chrome: normalized, toolbar: normalized }; + case "surface": + case "surfaceRaised": + case "surfaceOverlay": + case "input": + case "sidebarControlSurface": + return { ...colors, [role]: normalized }; + case "text": + return { + ...colors, + text: normalized, + toolbarForeground: normalized, + toolbarControlForeground: normalized, + }; + case "mutedForeground": + return { + ...colors, + mutedForeground: normalized, + placeholder: normalized, + secondaryLabel: normalized, + iconMuted: normalized, + sidebarMutedForeground: normalized, + }; + case "border": + return { + ...colors, + border: normalized, + toolbarBorder: normalized, + sidebarBorder: normalized, + }; + case "secondary": + return { + ...colors, + secondary: normalized, + secondaryForeground: foregroundOn(selected), + muted: normalized, + toolbarControl: normalized, + }; + case "accentSurface": + return { + ...colors, + accentSurface: normalized, + accentSurfaceForeground: foregroundOn(selected), + toolbarControlHover: normalized, + }; + case "accent": { + const updateSurface = mixThemeRgbColors(canvas, selected, canvasIsDark ? 0.32 : 0.16); + return { + ...colors, + accent: normalized, + focus: normalized, + update: normalized, + updateForeground: foregroundOn(updateSurface), + updateSurface: colorOf(updateSurface), + terminalCursor: normalized, + }; + } + case "messageAction": + return { ...colors, ...themeActionColors(normalized) }; + case "messageSurface": + return { + ...colors, + messageSurface: normalized, + messageForeground: foregroundOn(selected), + }; + case "codeBackground": + return { + ...colors, + codeBackground: normalized, + codeForeground: foregroundOn(selected), + }; + case "sidebar": + return { + ...colors, + sidebar: normalized, + sidebarForeground: foregroundOn(selected), + }; + case "sidebarRowSelected": { + const sidebar = parseThemeRgbColor(colors.sidebar, canvas); + return { + ...colors, + sidebarRowHover: colorOf(mixThemeRgbColors(sidebar, selected, 0.5)), + sidebarRowActive: colorOf(mixThemeRgbColors(sidebar, selected, 0.8)), + sidebarRowSelected: normalized, + }; + } + case "terminalBackground": + return { + ...colors, + terminalBackground: normalized, + terminalForeground: foregroundOn(selected), + terminalSelection: colorOf( + mixThemeRgbColors(selected, accent, terminalIsDark ? 0.35 : 0.18), + ), + }; + case "error": { + const status = statusColors(); + return { + ...colors, + error: normalized, + errorForeground: status.foreground, + errorSurface: status.surface, + }; + } + case "warning": { + const status = statusColors(); + return { + ...colors, + warning: normalized, + warningForeground: status.foreground, + warningSurface: status.surface, + }; + } + default: + return { ...colors, [role]: normalized }; + } +} + export const GROVE_THEME: ThemeDefinition = { id: GROVE_THEME_ID, label: GROVE_THEME_LABEL, diff --git a/docs/user/keybindings.md b/docs/user/keybindings.md index 63264bed10b5..8e56a79a287d 100644 --- a/docs/user/keybindings.md +++ b/docs/user/keybindings.md @@ -43,8 +43,10 @@ Repeating either shortcut closes that search, and switching shortcuts replaces t `themeEditor.toggle` opens or closes the floating theme editor and defaults to `mod+alt+shift+t`. Select a color label to spotlight the elements that use it; select the label again to clear the spotlight. The swatch and hex field keep that color selected while you edit it. +Advanced mode groups related app tokens into a smaller set of color families. Changing a family +updates its paired text and interaction states while leaving every unrelated imported color intact. Use **Inspect** to pick an element in the app and reveal its color token. Inspect disarms after one -successful pick; its hover glow and badge preview the element and token that click will select. +successful pick; its hover glow and badge preview the element and color family that click will select. **Cancel** or `Escape` exits Inspect and clears its selection and spotlight. `rightPanel.toggleMaximized` maximizes or restores the open right panel. It has no default shortcut, From 3c87a61913a26c5d527cf08858401603bdf506f7 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Sat, 15 Aug 2026 17:51:07 +0200 Subject: [PATCH 2/4] fix(web): complete advanced theme families --- .../components/settings/ThemeEditorPanel.tsx | 21 ++++++++++++++++--- apps/web/src/themePalette.ts | 14 +++++++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/settings/ThemeEditorPanel.tsx b/apps/web/src/components/settings/ThemeEditorPanel.tsx index 94c75e9fdc53..58070db33550 100644 --- a/apps/web/src/components/settings/ThemeEditorPanel.tsx +++ b/apps/web/src/components/settings/ThemeEditorPanel.tsx @@ -92,6 +92,7 @@ const THEME_EDITOR_ROLE_GROUPS: ReadonlyArray<{ label: "Muted text", role: "mutedForeground", roles: [ + "textMuted", "mutedForeground", "placeholder", "secondaryLabel", @@ -128,7 +129,15 @@ const THEME_EDITOR_ROLE_GROUPS: ReadonlyArray<{ id: "accent", label: "Accent", role: "accent", - roles: ["accent", "focus", "update", "updateForeground", "updateSurface", "terminalCursor"], + roles: [ + "accent", + "accentForeground", + "focus", + "update", + "updateForeground", + "updateSurface", + "terminalCursor", + ], }, { id: "action", @@ -176,7 +185,13 @@ const THEME_EDITOR_ROLE_GROUPS: ReadonlyArray<{ id: "terminal-background", label: "Terminal background", role: "terminalBackground", - roles: ["terminalBackground", "terminalForeground", "terminalSelection"], + roles: [ + "terminalBackground", + "terminalForeground", + "terminalSelection", + "terminalScrollbar", + "terminalScrollbarHover", + ], }, ], }, @@ -482,7 +497,7 @@ export function ThemeEditorPanel({ const selectThemeRole = useCallback((role: ThemeColorRole, reveal = false) => { const visibleRole = getThemeEditorColorFamily(role)?.role ?? role; setSelectedRole(visibleRole); - if (!THEME_EDITOR_SIMPLE_ROLES.includes(role)) { + if (!THEME_EDITOR_SIMPLE_ROLES.includes(visibleRole)) { setIsAdvanced(true); setRoleQuery(""); } diff --git a/apps/web/src/themePalette.ts b/apps/web/src/themePalette.ts index a2147ef3a7df..cad829ff7563 100644 --- a/apps/web/src/themePalette.ts +++ b/apps/web/src/themePalette.ts @@ -1526,6 +1526,7 @@ export function updateThemeColorFamily( case "mutedForeground": return { ...colors, + textMuted: normalized, mutedForeground: normalized, placeholder: normalized, secondaryLabel: normalized, @@ -1559,6 +1560,7 @@ export function updateThemeColorFamily( return { ...colors, accent: normalized, + accentForeground: foregroundOn(selected), focus: normalized, update: normalized, updateForeground: foregroundOn(updateSurface), @@ -1595,15 +1597,23 @@ export function updateThemeColorFamily( sidebarRowSelected: normalized, }; } - case "terminalBackground": + case "terminalBackground": { + const terminalForeground = readableThemeForeground(selected); return { ...colors, terminalBackground: normalized, - terminalForeground: foregroundOn(selected), + terminalForeground: colorOf(terminalForeground), terminalSelection: colorOf( mixThemeRgbColors(selected, accent, terminalIsDark ? 0.35 : 0.18), ), + terminalScrollbar: colorOf( + mixThemeRgbColors(selected, terminalForeground, terminalIsDark ? 0.42 : 0.22), + ), + terminalScrollbarHover: colorOf( + mixThemeRgbColors(selected, terminalForeground, terminalIsDark ? 0.55 : 0.32), + ), }; + } case "error": { const status = statusColors(); return { From 77d5aaf2b9be818614f79d6dd65e62bfce714c2d Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Sat, 15 Aug 2026 17:58:55 +0200 Subject: [PATCH 3/4] fix(web): preserve theme family contrast --- apps/web/src/themePalette.ts | 60 ++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/apps/web/src/themePalette.ts b/apps/web/src/themePalette.ts index cad829ff7563..1c336a29d7e0 100644 --- a/apps/web/src/themePalette.ts +++ b/apps/web/src/themePalette.ts @@ -1480,29 +1480,36 @@ export function updateThemeColorFamily( role: ThemeColorRole, value: string, ): ThemeColors { - const normalized = toCanonicalThemeColor(value); - if (!normalized) return { ...colors, [role]: value }; + const parsedSelected = parseThemeColor(value); + if (!parsedSelected) return { ...colors, [role]: value }; + const normalized = formatOklchThemeColor(parsedSelected.color, parsedSelected.alpha); const canvas = parseThemeRgbColor( colors.canvas, appearance === "dark" ? { r: 24, g: 15, b: 27 } : { r: 250, g: 245, b: 250 }, ); - const selected = parseThemeRgbColor(normalized, canvas); + const selected = themeOklchToRgb(parsedSelected.color); + const selectedOn = (background: ThemeRgbColor) => + mixThemeRgbColors(background, selected, parsedSelected.alpha); + const selectedOnCanvas = selectedOn(canvas); const accent = parseThemeRgbColor(colors.accent, { r: 168, g: 67, b: 112 }); const canvasIsDark = themeRelativeLuminance(canvas) < 0.179; - const terminalIsDark = themeRelativeLuminance(selected) < 0.179; + const terminalIsDark = themeRelativeLuminance(selectedOnCanvas) < 0.179; const colorOf = (color: ThemeRgbColor) => themeRgbToThemeColor(color); const foregroundOn = (background: ThemeRgbColor) => colorOf(readableThemeForeground(background)); - const statusColors = () => { - const surface = mixThemeRgbColors(canvas, selected, canvasIsDark ? 0.16 : 0.08); - const foreground = solveOklchLightness( - themeRgbToOklch(selected), - surface, - 4.6, - canvasIsDark ? "lighter" : "darker", + const selectedToneOn = (background: ThemeRgbColor) => + themeOklchToThemeColor( + solveOklchLightness( + parsedSelected.color, + background, + 4.6, + themeRelativeLuminance(background) < 0.179 ? "lighter" : "darker", + ), ); + const statusColors = () => { + const surface = mixThemeRgbColors(canvas, selectedOnCanvas, canvasIsDark ? 0.16 : 0.08); return { - foreground: themeOklchToThemeColor(foreground), + foreground: selectedToneOn(surface), surface: colorOf(surface), }; }; @@ -1544,7 +1551,7 @@ export function updateThemeColorFamily( return { ...colors, secondary: normalized, - secondaryForeground: foregroundOn(selected), + secondaryForeground: foregroundOn(selectedOnCanvas), muted: normalized, toolbarControl: normalized, }; @@ -1552,18 +1559,18 @@ export function updateThemeColorFamily( return { ...colors, accentSurface: normalized, - accentSurfaceForeground: foregroundOn(selected), + accentSurfaceForeground: foregroundOn(selectedOnCanvas), toolbarControlHover: normalized, }; case "accent": { - const updateSurface = mixThemeRgbColors(canvas, selected, canvasIsDark ? 0.32 : 0.16); + const updateSurface = mixThemeRgbColors(canvas, selectedOnCanvas, canvasIsDark ? 0.32 : 0.16); return { ...colors, accent: normalized, - accentForeground: foregroundOn(selected), + accentForeground: foregroundOn(selectedOnCanvas), focus: normalized, update: normalized, - updateForeground: foregroundOn(updateSurface), + updateForeground: selectedToneOn(updateSurface), updateSurface: colorOf(updateSurface), terminalCursor: normalized, }; @@ -1574,43 +1581,44 @@ export function updateThemeColorFamily( return { ...colors, messageSurface: normalized, - messageForeground: foregroundOn(selected), + messageForeground: foregroundOn(selectedOnCanvas), }; case "codeBackground": return { ...colors, codeBackground: normalized, - codeForeground: foregroundOn(selected), + codeForeground: foregroundOn(selectedOnCanvas), }; case "sidebar": return { ...colors, sidebar: normalized, - sidebarForeground: foregroundOn(selected), + sidebarForeground: foregroundOn(selectedOnCanvas), }; case "sidebarRowSelected": { const sidebar = parseThemeRgbColor(colors.sidebar, canvas); + const selectedOnSidebar = selectedOn(sidebar); return { ...colors, - sidebarRowHover: colorOf(mixThemeRgbColors(sidebar, selected, 0.5)), - sidebarRowActive: colorOf(mixThemeRgbColors(sidebar, selected, 0.8)), + sidebarRowHover: colorOf(mixThemeRgbColors(sidebar, selectedOnSidebar, 0.5)), + sidebarRowActive: colorOf(mixThemeRgbColors(sidebar, selectedOnSidebar, 0.8)), sidebarRowSelected: normalized, }; } case "terminalBackground": { - const terminalForeground = readableThemeForeground(selected); + const terminalForeground = readableThemeForeground(selectedOnCanvas); return { ...colors, terminalBackground: normalized, terminalForeground: colorOf(terminalForeground), terminalSelection: colorOf( - mixThemeRgbColors(selected, accent, terminalIsDark ? 0.35 : 0.18), + mixThemeRgbColors(selectedOnCanvas, accent, terminalIsDark ? 0.35 : 0.18), ), terminalScrollbar: colorOf( - mixThemeRgbColors(selected, terminalForeground, terminalIsDark ? 0.42 : 0.22), + mixThemeRgbColors(selectedOnCanvas, terminalForeground, terminalIsDark ? 0.42 : 0.22), ), terminalScrollbarHover: colorOf( - mixThemeRgbColors(selected, terminalForeground, terminalIsDark ? 0.55 : 0.32), + mixThemeRgbColors(selectedOnCanvas, terminalForeground, terminalIsDark ? 0.55 : 0.32), ), }; } From 23b10313dc1ba281dd2d3e0067138a9f1b40d90d Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Sat, 15 Aug 2026 18:04:15 +0200 Subject: [PATCH 4/4] fix(web): preserve action transparency --- apps/web/src/themePalette.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/web/src/themePalette.ts b/apps/web/src/themePalette.ts index 1c336a29d7e0..ec75cbe5b758 100644 --- a/apps/web/src/themePalette.ts +++ b/apps/web/src/themePalette.ts @@ -1575,8 +1575,23 @@ export function updateThemeColorFamily( terminalCursor: normalized, }; } - case "messageAction": - return { ...colors, ...themeActionColors(normalized) }; + case "messageAction": { + const actionForeground = readableThemeForeground(selectedOnCanvas); + const towardOpposite = + actionForeground === THEME_LIGHT_FOREGROUND || actionForeground === THEME_WHITE_FOREGROUND + ? THEME_BLACK_FOREGROUND + : THEME_WHITE_FOREGROUND; + const actionHover = mixThemeRgbColors(selected, towardOpposite, 0.12); + return { + ...colors, + messageAction: normalized, + messageActionForeground: colorOf(actionForeground), + messageActionHover: formatOklchThemeColor( + themeRgbToOklch(actionHover), + parsedSelected.alpha, + ), + }; + } case "messageSurface": return { ...colors,