From c86be7bf73d0141e0606d259ada6453c0d8e3dec Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Mon, 21 Sep 2026 21:10:10 +0300 Subject: [PATCH] fix(ui): improve financial text contrast TD-010: use readable semantic text colors on financial surfaces while preserving signs and state meanings. --- src/pages/account/ui/AccountPage.module.css | 4 +- src/shared/theme/contrast.test.ts | 45 +++++++++++++++++++++ src/shared/theme/tokens.json | 6 +-- src/shared/ui/EntryCard.module.css | 6 +-- 4 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 src/shared/theme/contrast.test.ts diff --git a/src/pages/account/ui/AccountPage.module.css b/src/pages/account/ui/AccountPage.module.css index f5aa21c..c097c31 100644 --- a/src/pages/account/ui/AccountPage.module.css +++ b/src/pages/account/ui/AccountPage.module.css @@ -63,11 +63,11 @@ } .positive { - color: var(--mantine-color-green-6); + color: var(--wl-color-positive); } .negative { - color: var(--mantine-color-red-6); + color: var(--wl-color-negative); } .expandedCard { diff --git a/src/shared/theme/contrast.test.ts b/src/shared/theme/contrast.test.ts new file mode 100644 index 0000000..129b98b --- /dev/null +++ b/src/shared/theme/contrast.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import tokens from "./tokens.json"; + +function luminance(color: string) { + const channels = [1, 3, 5] + .map((index) => Number.parseInt(color.slice(index, index + 2), 16) / 255) + .map((value) => + value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4, + ); + return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722; +} +describe("financial text contrast", () => { + it.each(["positive", "negative"] as const)( + "keeps %s readable on journal, position and tinted card surfaces", + (tone) => { + const role = tokens.color.roles[tone]; + const palettes = tokens.color.palettes; + const foreground = + palettes[role.color as keyof typeof palettes][role.shade]; + const surfaces = [ + palettes.gray[0], + ...Object.entries(palettes) + .filter(([name]) => name !== "dark") + .map( + ([, palette]) => + `#${[1, 3, 5] + .map((index) => + Math.round( + Number.parseInt(palette[0].slice(index, index + 2), 16) * + 0.45 + + 255 * 0.55, + ) + .toString(16) + .padStart(2, "0"), + ) + .join("")}`, + ), + ]; + for (const background of surfaces) + expect( + (luminance(background) + 0.05) / (luminance(foreground) + 0.05), + ).toBeGreaterThanOrEqual(4.5); + }, + ); +}); diff --git a/src/shared/theme/tokens.json b/src/shared/theme/tokens.json index 32cab6d..4f5d284 100644 --- a/src/shared/theme/tokens.json +++ b/src/shared/theme/tokens.json @@ -53,7 +53,7 @@ "#40c057", "#37b24d", "#2f9e44", - "#2b8a3e" + "#237032" ], "red": [ "#fff5f5", @@ -151,12 +151,12 @@ }, "positive": { "color": "green", - "shade": 6, + "shade": 9, "description": "Положительный результат; дополните знаком и текстом" }, "negative": { "color": "red", - "shade": 6, + "shade": 9, "description": "Ошибка / отрицательный результат; дополните текстом" }, "warning": { diff --git a/src/shared/ui/EntryCard.module.css b/src/shared/ui/EntryCard.module.css index efdcb3e..cfb27d0 100644 --- a/src/shared/ui/EntryCard.module.css +++ b/src/shared/ui/EntryCard.module.css @@ -19,15 +19,15 @@ } .positive { - color: var(--mantine-color-green-6); + color: var(--wl-color-positive); } .negative { - color: var(--mantine-color-red-6); + color: var(--wl-color-negative); } .neutral { - color: var(--mantine-color-dimmed); + color: var(--mantine-color-gray-7); } @media (max-width: 36em) {