diff --git a/apps/desktop/e2e/__screenshots__/landing.spec.ts/landing-default.png b/apps/desktop/e2e/__screenshots__/landing.spec.ts/landing-default.png index bf13d92f..a1774676 100644 Binary files a/apps/desktop/e2e/__screenshots__/landing.spec.ts/landing-default.png and b/apps/desktop/e2e/__screenshots__/landing.spec.ts/landing-default.png differ diff --git a/apps/desktop/e2e/__screenshots__/tools.spec.ts/tool-select.png b/apps/desktop/e2e/__screenshots__/tools.spec.ts/tool-select.png index 63163073..83133e6a 100644 Binary files a/apps/desktop/e2e/__screenshots__/tools.spec.ts/tool-select.png and b/apps/desktop/e2e/__screenshots__/tools.spec.ts/tool-select.png differ diff --git a/apps/desktop/src/renderer/index.css b/apps/desktop/src/renderer/index.css index c6b8ecbb..74dc36f0 100644 --- a/apps/desktop/src/renderer/index.css +++ b/apps/desktop/src/renderer/index.css @@ -114,6 +114,12 @@ --editor-text-component-overlay-hover-b: rgba(255, 151, 186, 0.4); } +/* Only Shift Light shows the coloured lockup; other themes fill it with a tint of their text colour. */ +:root[data-color-theme]:not([data-color-theme="shift-light"]) { + --color-logo-lid: color-mix(in srgb, currentColor 22%, transparent); + --color-logo-body: color-mix(in srgb, currentColor 22%, transparent); +} + /* Lifts a bordered card off the page with theme-aware shadow colours. */ @utility card-shadow { box-shadow: diff --git a/apps/desktop/src/renderer/src/assets/launcher-lockup-nightly.svg b/apps/desktop/src/renderer/src/assets/launcher-lockup-nightly.svg index d69498b3..71868757 100644 --- a/apps/desktop/src/renderer/src/assets/launcher-lockup-nightly.svg +++ b/apps/desktop/src/renderer/src/assets/launcher-lockup-nightly.svg @@ -1,34 +1,13 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/apps/desktop/src/renderer/src/assets/launcher-lockup.svg b/apps/desktop/src/renderer/src/assets/launcher-lockup.svg index ac06fb0b..3636260d 100644 --- a/apps/desktop/src/renderer/src/assets/launcher-lockup.svg +++ b/apps/desktop/src/renderer/src/assets/launcher-lockup.svg @@ -1,34 +1,13 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/apps/desktop/src/renderer/src/lib/themes/index.test.ts b/apps/desktop/src/renderer/src/lib/themes/index.test.ts index 57c77ded..8e0b88db 100644 --- a/apps/desktop/src/renderer/src/lib/themes/index.test.ts +++ b/apps/desktop/src/renderer/src/lib/themes/index.test.ts @@ -59,6 +59,14 @@ describe("color themes", () => { // Dracula's accent is a pale cyan, so text on it must be dark, not white. expect(properties.get("--color-on-accent")).toBe(properties.get("--color-background")); + // Solarized's mid-tone blue scores slightly higher with dark text, but its light base reads better. + applyResolvedTheme(resolveThemeSelection("solarized-dark", "light"), root); + expect(properties.get("--color-on-accent")).toBe("#fdf6e3"); + + // Text on Gruvbox Light's teal uses the palette's cream rather than pure white. + applyResolvedTheme(resolveThemeSelection("gruvbox-light", "dark"), root); + expect(properties.get("--color-on-accent")).toBe("#fbf1c7"); + applyResolvedTheme(resolveThemeSelection("shift-light", "dark"), root); expect(root.dataset.theme).toBe("light"); expect(properties.has("--color-background")).toBe(false); diff --git a/apps/desktop/src/renderer/src/lib/themes/index.ts b/apps/desktop/src/renderer/src/lib/themes/index.ts index d20af88c..3b7637fa 100644 --- a/apps/desktop/src/renderer/src/lib/themes/index.ts +++ b/apps/desktop/src/renderer/src/lib/themes/index.ts @@ -306,7 +306,7 @@ function colorThemeVariables(theme: ColorTheme): Record { "--color-sidebar-icon": secondary, "--color-accent": palette.base0D, "--color-on-accent": readableOn(palette.base0D, [ - "#ffffff", + lightest(palette.base00, palette.base07), darkest(palette.base00, palette.base07), ]), "--color-error": palette.base08, @@ -363,14 +363,29 @@ function colorThemeVariables(theme: ColorTheme): Record { }; } -/** Returns the candidate text colour with the highest WCAG contrast against `background`. */ -function readableOn(background: string, candidates: readonly string[]): string { +/** How close a preferred candidate's contrast must come to the best one to still win. */ +const PREFERRED_CONTRAST_MARGIN = 0.8; + +/** + * Picks the candidate with the highest WCAG contrast on `background`, except that the + * first candidate wins whenever it comes within 20% of the best. WCAG contrast + * over-favours dark text on saturated mid-tone accents, where light text reads better. + */ +function readableOn(background: string, [preferred, ...others]: readonly string[]): string { const backgroundLuminance = relativeLuminance(background); const contrast = (color: string) => { const [lighter, darker] = [relativeLuminance(color), backgroundLuminance].sort((a, b) => b - a); return (lighter + 0.05) / (darker + 0.05); }; - return candidates.reduce((best, color) => (contrast(color) > contrast(best) ? color : best)); + const best = others.reduce( + (best, color) => (contrast(color) > contrast(best) ? color : best), + preferred, + ); + return contrast(preferred) >= contrast(best) * PREFERRED_CONTRAST_MARGIN ? preferred : best; +} + +function lightest(first: string, second: string): string { + return relativeLuminance(first) >= relativeLuminance(second) ? first : second; } function darkest(first: string, second: string): string {