diff --git a/templates/repo/diff/stats.tmpl b/templates/repo/diff/stats.tmpl
index 09ce9b3478..dca72dbb4e 100644
--- a/templates/repo/diff/stats.tmpl
+++ b/templates/repo/diff/stats.tmpl
@@ -6,8 +6,8 @@
{{if or .Addition .Deletion}}
- {{if .Addition}}+{{.Addition}}{{end}}
- {{if .Deletion}}-{{.Deletion}}{{end}}
+ {{if .Addition}}+{{.Addition}}{{end}}
+ {{if .Deletion}}-{{.Deletion}}{{end}}
{{/* if the denominator is zero, then the float result is "width: NaNpx", as before, it just works */}}
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 f07d94cc63..13493ae69a 100644
--- a/web_src/css/themes/theme-gitea-dark.css
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -163,14 +163,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;
@@ -270,8 +270,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..ca9f393c96
--- /dev/null
+++ b/web_src/js/utils/theme-contrast.test.ts
@@ -0,0 +1,66 @@
+import {readFile} from 'node:fs/promises';
+import {join} from 'node:path';
+
+type CssVariables = Record;
+
+async function loadThemeVariables(fileName: string, baseVariables: CssVariables = {}): Promise {
+ const themePath = 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]);
+ }
+ }
+});