From 20f1bed33655e5f9e2a87e2a2b6ae542aaad21bf Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 8 Nov 2026 17:09:29 +0400 Subject: [PATCH] =?UTF-8?q?fix(table):=20=D0=BC=D0=B8=D0=BD=D1=83=D1=81=20?= =?UTF-8?q?=D0=BA=D1=80=D0=B0=D1=81=D0=BD=D1=8B=D0=B9=20=D0=B8=20=D0=B2=20?= =?UTF-8?q?=D0=BD=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=B8=D1=80?= =?UTF-8?q?=D1=83=D0=B5=D0=BC=D1=8B=D1=85=20=D1=8F=D1=87=D0=B5=D0=B9=D0=BA?= =?UTF-8?q?=D0=B0=D1=85=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B8=20=D0=B2?= =?UTF-8?q?=D0=B2=D0=BE=D0=B4=D0=B0=20(AURORA-1892)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Возврат тестирования 06.11, критерий «любое отрицательное число выделено цветом»: NOT_EDITABLE_INPUT_STYLES (AURORA-1890) разворачивался после цвета текста и перекрашивал минус в серый #5F6B7A — в расчётных колонках вне перечня отрицательные значения оставались серыми. Цвет текста теперь решает resolveCellTextColor (utils/cellTextColor.js, с тестами): минус — красный всегда, серый — только для неотрицательных значений. Co-Authored-By: Claude Opus 5.5 --- .../RealtimeTable/Cell/Cell/Cell.jsx | 39 ++++-------------- .../RealtimeTable/utils/cellTextColor.js | 40 +++++++++++++++++++ .../RealtimeTable/utils/cellTextColor.test.js | 29 ++++++++++++++ 3 files changed, 77 insertions(+), 31 deletions(-) create mode 100644 web/src/components/RealtimeTable/utils/cellTextColor.js create mode 100644 web/src/components/RealtimeTable/utils/cellTextColor.test.js diff --git a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx index e97cd3c..39f37f7 100644 --- a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx +++ b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx @@ -1,6 +1,7 @@ import { Tooltip } from '@mui/material'; import React, { useMemo, useCallback, useState } from 'react'; import { useCellLock } from '../../contexts/RealtimeContext'; +import { resolveCellTextColor } from '../../utils/cellTextColor'; import { formatNumber } from '../../utils/numberFormat'; import { TEXT_COLUMN_IDS } from '../../utils/moneyUtils'; @@ -33,11 +34,6 @@ const INVALID_STYLES = { color: '#C60C0C', }; -// AURORA-1892: отрицательные суммы должны быть явно отличимы от нуля и обычных чисел, -// в том числе на тёмных фонах палитры (например ROOT красной палитры #933634). -const NEGATIVE_COLOR_ON_LIGHT = '#C62828'; -const NEGATIVE_COLOR_ON_DARK = '#FFCDD2'; - // AURORA-1890: редактируемая ячейка строки INPUT — белый фон и заметная // синяя рамка (вместо прежней почти незаметной полупрозрачной 2px #d8e4ea94). // Толщина рамки растёт при наведении (1px → 2px, см. isHovered в CellComponent), курсор — text. @@ -50,10 +46,11 @@ const EDITABLE_BORDER_COLOR = '#7FA7C9'; // AURORA-1890: нередактируемая ячейка строки INPUT (вычисляемая или вне // текущего этапа) — приглушённый фон и текст, чтобы визуально отличаться от // редактируемой белой ячейки без отдельной палитры цветов. +// Цвет текста (в т.ч. приглушённый серый) задаёт resolveCellTextColor: здесь его нет, +// чтобы стиль не перекрывал красный цвет отрицательных значений (AURORA-1892). const NOT_EDITABLE_INPUT_STYLES = { cursor: 'default', backgroundColor: '#F4F5F7', - color: '#5F6B7A', }; // Строки ITEM/GROUP/ROOT: вне скоупа AURORA-1890 — сохраняем прежнее поведение. @@ -104,26 +101,6 @@ const OVER_LIMIT_CHIP_STYLES = { const NAME_COLUMN_IDS = new Set(['data.header.name', 'data.name']); -const getColorBrightness = (hexColor) => { - if (!hexColor) return 255; - const color = hexColor.replace('#', ''); - let r; - let g; - let b; - if (color.length === 3) { - r = Number.parseInt(color[0] + color[0], 16); - g = Number.parseInt(color[1] + color[1], 16); - b = Number.parseInt(color[2] + color[2], 16); - } else if (color.length === 6) { - r = Number.parseInt(color.substring(0, 2), 16); - g = Number.parseInt(color.substring(2, 4), 16); - b = Number.parseInt(color.substring(4, 6), 16); - } else { - return 255; - } - return 0.299 * r + 0.587 * g + 0.114 * b; -}; - // Оптимизированная функция подсветки const createHighlightedContent = (originalValue, displayValue, searchQueries) => { const cleanQueries = searchQueries.filter(Boolean); @@ -213,11 +190,11 @@ const CellComponent = ({ // AURORA-1890: занятая коллегой ячейка больше не красится в серый — // обычный brightness-based цвет текста, отличие только в рамке/бейдже. - const textColor = useMemo(() => { - const brightness = getColorBrightness(backgroundColor); - if (isNegative) return brightness < 128 ? NEGATIVE_COLOR_ON_DARK : NEGATIVE_COLOR_ON_LIGHT; - return brightness < 128 ? '#ffffff' : '#000000'; - }, [backgroundColor, isNegative]); + const isMutedInput = isInputRow && !isEditable; + const textColor = useMemo( + () => resolveCellTextColor({ backgroundColor, isNegative, isMutedInput }), + [backgroundColor, isNegative, isMutedInput], + ); // AURORA-1890: нередактируемый стиль зависит от типа строки (INPUT получает // приглушённую заливку, ITEM/GROUP/ROOT — прежнее поведение без изменений). diff --git a/web/src/components/RealtimeTable/utils/cellTextColor.js b/web/src/components/RealtimeTable/utils/cellTextColor.js new file mode 100644 index 0000000..583ce33 --- /dev/null +++ b/web/src/components/RealtimeTable/utils/cellTextColor.js @@ -0,0 +1,40 @@ +// Цвет текста ячейки таблицы (вынесено из Cell.jsx, чтобы покрыть тестами без DOM). + +// AURORA-1892: отрицательные суммы должны быть явно отличимы от нуля и обычных чисел, +// в том числе на тёмных фонах палитры (например ROOT красной палитры #933634). +export const NEGATIVE_COLOR_ON_LIGHT = '#C62828'; +export const NEGATIVE_COLOR_ON_DARK = '#FFCDD2'; + +// AURORA-1890: приглушённый текст нередактируемой ячейки строки ввода (фон #F4F5F7). +export const MUTED_INPUT_TEXT_COLOR = '#5F6B7A'; + +export const getColorBrightness = (hexColor) => { + if (!hexColor) return 255; + const color = hexColor.replace('#', ''); + let r; + let g; + let b; + if (color.length === 3) { + r = Number.parseInt(color[0] + color[0], 16); + g = Number.parseInt(color[1] + color[1], 16); + b = Number.parseInt(color[2] + color[2], 16); + } else if (color.length === 6) { + r = Number.parseInt(color.substring(0, 2), 16); + g = Number.parseInt(color.substring(2, 4), 16); + b = Number.parseInt(color.substring(4, 6), 16); + } else { + return 255; + } + return 0.299 * r + 0.587 * g + 0.114 * b; +}; + +// AURORA-1892 (возврат 06.11): минус красный в любой ячейке. Раньше серый цвет +// нередактируемой ячейки строки ввода (AURORA-1890) перекрывал красный, и в расчётных +// колонках отрицательные значения оставались серыми. Фон такой ячейки всегда светлый +// (#F4F5F7), поэтому для неё — красный для светлого фона. +export const resolveCellTextColor = ({ backgroundColor, isNegative, isMutedInput }) => { + if (isMutedInput) return isNegative ? NEGATIVE_COLOR_ON_LIGHT : MUTED_INPUT_TEXT_COLOR; + const isDark = getColorBrightness(backgroundColor) < 128; + if (isNegative) return isDark ? NEGATIVE_COLOR_ON_DARK : NEGATIVE_COLOR_ON_LIGHT; + return isDark ? '#ffffff' : '#000000'; +}; diff --git a/web/src/components/RealtimeTable/utils/cellTextColor.test.js b/web/src/components/RealtimeTable/utils/cellTextColor.test.js new file mode 100644 index 0000000..7dea8eb --- /dev/null +++ b/web/src/components/RealtimeTable/utils/cellTextColor.test.js @@ -0,0 +1,29 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + MUTED_INPUT_TEXT_COLOR, + NEGATIVE_COLOR_ON_DARK, + NEGATIVE_COLOR_ON_LIGHT, + resolveCellTextColor, +} from './cellTextColor.js'; + +// AURORA-1892 (возврат 06.11): в расчётных (нередактируемых) ячейках строки ввода +// минус оставался серым — серый цвет AURORA-1890 перекрывал красный. +test('a negative value in a non-editable input cell is red, not muted grey', () => { + assert.equal(resolveCellTextColor({ backgroundColor: '#FFFFFF', isNegative: true, isMutedInput: true }), NEGATIVE_COLOR_ON_LIGHT); +}); + +test('a non-negative value in a non-editable input cell keeps the muted grey', () => { + assert.equal(resolveCellTextColor({ backgroundColor: '#FFFFFF', isNegative: false, isMutedInput: true }), MUTED_INPUT_TEXT_COLOR); +}); + +test('negative values follow background brightness outside muted input cells', () => { + assert.equal(resolveCellTextColor({ backgroundColor: '#FFFFFF', isNegative: true, isMutedInput: false }), NEGATIVE_COLOR_ON_LIGHT); + assert.equal(resolveCellTextColor({ backgroundColor: '#933634', isNegative: true, isMutedInput: false }), NEGATIVE_COLOR_ON_DARK); +}); + +test('regular values are black on light and white on dark backgrounds', () => { + assert.equal(resolveCellTextColor({ backgroundColor: '#FFFFFF', isNegative: false, isMutedInput: false }), '#000000'); + assert.equal(resolveCellTextColor({ backgroundColor: '#933634', isNegative: false, isMutedInput: false }), '#ffffff'); + assert.equal(resolveCellTextColor({ backgroundColor: undefined, isNegative: false, isMutedInput: false }), '#000000'); +});