From 40bcb68030c12a37a9cada61142654bb29378ab3 Mon Sep 17 00:00:00 2001 From: cyphercodes Date: Wed, 29 Apr 2026 06:57:49 +0300 Subject: [PATCH] Improve dark diff contrast Co-authored-by: Hermes Agent (GPT-5.5) Signed-off-by: cyphercodes --- web_src/css/repo.css | 5 ++ ...eme-gitea-dark-protanopia-deuteranopia.css | 4 +- .../themes/theme-gitea-dark-tritanopia.css | 2 +- web_src/css/themes/theme-gitea-dark.css | 8 +-- web_src/js/utils/theme-contrast.test.ts | 66 +++++++++++++++++++ 5 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 web_src/js/utils/theme-contrast.test.ts diff --git a/web_src/css/repo.css b/web_src/css/repo.css index bd436e89b0..2b8a97543f 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -1728,6 +1728,11 @@ tbody.commit-list { margin-left: 0 !important; } +.diff-file-header .tw-text-diff-added-fg, +.diff-file-header .tw-text-diff-removed-fg { + font-weight: var(--font-weight-semibold); +} + .diff-stats-bar { display: inline-block; background-color: var(--color-diff-removed-fg); /* the background is used as "text foreground color" */ diff --git a/web_src/css/themes/theme-gitea-dark-protanopia-deuteranopia.css b/web_src/css/themes/theme-gitea-dark-protanopia-deuteranopia.css index 01f53e3908..b08c417b29 100644 --- a/web_src/css/themes/theme-gitea-dark-protanopia-deuteranopia.css +++ b/web_src/css/themes/theme-gitea-dark-protanopia-deuteranopia.css @@ -8,11 +8,11 @@ gitea-theme-meta-info { /* red/green colorblind-friendly colors */ :root { - --color-diff-added-fg: #58a6ff; + --color-diff-added-fg: #65adff; --color-diff-added-linenum-bg: #243d5d; --color-diff-added-row-bg: #132339; --color-diff-added-word-bg: #214d87; - --color-diff-removed-fg: #f0883e; + --color-diff-removed-fg: #f2964f; --color-diff-removed-linenum-bg: #5b361c; --color-diff-removed-row-bg: #3c2419; --color-diff-removed-word-bg: #824e1f; diff --git a/web_src/css/themes/theme-gitea-dark-tritanopia.css b/web_src/css/themes/theme-gitea-dark-tritanopia.css index dd4e0502c4..3e8ef4e0fb 100644 --- a/web_src/css/themes/theme-gitea-dark-tritanopia.css +++ b/web_src/css/themes/theme-gitea-dark-tritanopia.css @@ -8,7 +8,7 @@ gitea-theme-meta-info { /* blue/yellow colorblind-friendly colors */ :root { - --color-diff-added-fg: #58a6ff; + --color-diff-added-fg: #65adff; --color-diff-added-linenum-bg: #243d5d; --color-diff-added-row-bg: #132339; --color-diff-added-word-bg: #214d87; diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index 188a30cae0..45e8bcd8bd 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -146,14 +146,14 @@ gitea-theme-meta-info { --color-grey-light: #898d96; --color-gold: #b1983b; --color-white: #ffffff; - --color-diff-added-fg: #87ab63; + --color-diff-added-fg: #93b373; --color-diff-added-linenum-bg: #274227; --color-diff-added-row-bg: #203224; --color-diff-added-row-border: #314a37; --color-diff-added-word-bg: #3c653c; --color-diff-moved-row-bg: #818044; --color-diff-moved-row-border: #bcca6f; - --color-diff-removed-fg: #cc4848; + --color-diff-removed-fg: #ff8585; --color-diff-removed-linenum-bg: #482121; --color-diff-removed-row-bg: #301e1e; --color-diff-removed-row-border: #634343; @@ -253,8 +253,8 @@ gitea-theme-meta-info { --color-syntax-keyword: #ff8854; --color-syntax-bool: #25bbc9; --color-syntax-control: #dd9e17; - --color-syntax-name: #c7a618; - --color-syntax-type: #eb8cb3; + --color-syntax-name: #fabd2f; + --color-syntax-type: #fabd2f; --color-syntax-number: #63b2dd; --color-syntax-operator: #ff8854; --color-syntax-regexp: #b89de4; diff --git a/web_src/js/utils/theme-contrast.test.ts b/web_src/js/utils/theme-contrast.test.ts new file mode 100644 index 0000000000..89c554c469 --- /dev/null +++ b/web_src/js/utils/theme-contrast.test.ts @@ -0,0 +1,66 @@ +import {readFile} from 'node:fs/promises'; +import * as path from 'node:path'; + +type CssVariables = Record; + +async function loadThemeVariables(fileName: string, baseVariables: CssVariables = {}): Promise { + const themePath = path.join(import.meta.dirname, '../../css/themes', fileName); + const css = await readFile(themePath, 'utf8'); + const variables = {...baseVariables}; + for (const match of css.matchAll(/(--[\w-]+):\s*(#[\dA-Fa-f]{6});/g)) { + variables[match[1]] = match[2]; + } + return variables; +} + +function relativeLuminance(hex: string): number { + const rgb = [ + Number.parseInt(hex.slice(1, 3), 16), + Number.parseInt(hex.slice(3, 5), 16), + Number.parseInt(hex.slice(5, 7), 16), + ].map((value) => { + const channel = value / 255; + return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4; + }); + return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]; +} + +function contrastRatio(foreground: string, background: string): number { + const foregroundLuminance = relativeLuminance(foreground); + const backgroundLuminance = relativeLuminance(background); + return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / + (Math.min(foregroundLuminance, backgroundLuminance) + 0.05); +} + +function expectWcagAaaContrast(label: string, foreground: string, background: string): void { + expect(contrastRatio(foreground, background), label).toBeGreaterThanOrEqual(7); +} + +test('dark diff stat colors have WCAG AAA contrast', async () => { + const darkVariables = await loadThemeVariables('theme-gitea-dark.css'); + const colorblindVariables = await loadThemeVariables('theme-gitea-dark-protanopia-deuteranopia.css', darkVariables); + const tritanopiaVariables = await loadThemeVariables('theme-gitea-dark-tritanopia.css', darkVariables); + const themes: Array<[string, CssVariables, string]> = [ + ['dark added', darkVariables, '--color-diff-added-fg'], + ['dark removed', darkVariables, '--color-diff-removed-fg'], + ['dark protanopia/deuteranopia added', colorblindVariables, '--color-diff-added-fg'], + ['dark protanopia/deuteranopia removed', colorblindVariables, '--color-diff-removed-fg'], + ['dark tritanopia added', tritanopiaVariables, '--color-diff-added-fg'], + ]; + + for (const [label, variables, colorVariable] of themes) { + expectWcagAaaContrast(label, variables[colorVariable], darkVariables['--color-body']); + } +}); + +test('dark syntax name colors have WCAG AAA contrast on diff rows', async () => { + const variables = await loadThemeVariables('theme-gitea-dark.css'); + const backgrounds = ['--color-diff-added-row-bg', '--color-diff-removed-row-bg'] as const; + const foregrounds = ['--color-syntax-name', '--color-syntax-type'] as const; + + for (const foreground of foregrounds) { + for (const background of backgrounds) { + expectWcagAaaContrast(`${foreground} on ${background}`, variables[foreground], variables[background]); + } + } +});