Перейти до змісту

DOP App — Тонке налаштування теми

Як крутити шрифти, кольори, density з єдиних точок контролю. Створено в session typography-overhaul (2026-05-09).

Локації джерел правди: - frontend/erp/src/theme/typography.ts — шрифти (розмір, вага, сім'я, line-height) + 3-рівневий color registry - frontend/erp/src/theme.ts — фон-палітри (tints), текстові кольори (light/dark), read-only input bg, density multipliers - frontend/erp/src/global.css — CSS-змінні шрифт-сімей + правило для :read-only inputs - frontend/erp/src/pages/SettingsPage/AppearanceSettings.tsx — UI вибору skin/tint/density


1. Архітектура — чому 4 файли, не один

Файл Що контролює Хто читає
typography.ts Шрифти (форма): розміри, ваги, line-height, сім'я React-компоненти через T.<role>
theme.ts Кольори (значення): фон, текст, read-only bg Mantine CSS resolver → --mantine-color-* змінні
global.css Глобальні CSS-правила (body { font-weight }, :read-only стиль) Браузер напряму
AppearanceSettings.tsx UI для skin/tint/density Юзер

Розділення: типографія = форма (як виглядає шрифт), кольори = значення (text/dimmed/error). Не змішуємо. Один T.listRow може мати text-color у звичайному рядку і dimmed у disabled.


2. Типографія — як крутити

2.1 Глобальний розмір (всі шрифти одночасно)

typography.ts → редагуй BASE_FONT_SIZE:

export const BASE_FONT_SIZE = 14;  // ← головний knob

Уся шкала fontSize.{xxs..xxl} рахується як BASE + delta. Зміниш на 16 → весь додаток стане крупнішим на 2px. Сценарії:

Значення Враження
13 Дуже щільно, 1С-стиль
14 Mantine sm equivalent, дефолт
15 Комфортніше для довгої роботи
16 Mantine md, "веб-стандарт"

2.2 Глобальна щільність по вертикалі

export const BASE_LINE_HEIGHT = 1.45;  // ← друга ручка
Значення Враження
1.30 Дуже щільно, термінал-стиль
1.45 Компактно (Mantine sm)
1.55 Стандарт (Mantine md), повітряніше

2.3 Конкретна роль (наприклад "header списку bold")

Кожна семантична роль — окремий токен у T:

listColumnHeader: {
  fontSize: fontSize.md,
  fontWeight: fontWeight.regular,  // ← хочеш bold? зміни на semibold/bold
  fontFamily: fontFamily.data,
  lineHeight: BASE_LINE_HEIGHT,
},

Зміна → автоматично пропагується в усі мігровані компоненти-споживачі (15+ списків через <ListTable>, ще 14+ кастомних list файлів).

2.4 Карта токенів — що де використовується

Токен Де
T.appBrand "Digital Operations Platform" в хедері
T.breadcrumb / breadcrumbCurrent хлібні крихти в хедері
T.navModule / navPlugin sidebar модулі / plugin-діти
T.navGroupLabel "Всі функції" drawer title
T.sectionGroupTitle / sectionCardTitle / sectionCardItem section dashboard (хаб модуля)
T.listColumnHeader / listRow / listRowTotals усі списки (документи + master data)
T.linesTableHeader шапки рядків у документі (LinesTable)
T.formFieldLabel / formFieldValue / formTab / formSectionTitle форми редагування
T.toolbarButton / toolbarMenuItem тулбари
T.reportColumnHeader / reportRow / reportSubtotals / reportTotals звіти
T.kpiLabel / kpiValue KPI-чіпи у footer'ах документів
T.caption дрібні підказки/мета
T.code inline monospace

2.5 Шрифтова сім'я (для скінів)

Сім'ї керуються через CSS-змінні в global.css:

:root {
  --eswf-font-ui:   var(--mantine-font-family);  /* chrome */
  --eswf-font-data: var(--mantine-font-family);  /* списки/звіти */
  --eswf-font-form: var(--mantine-font-family);  /* форми */
}

Скіни (наприклад terminal) перевизначають --eswf-font-ui на mono — і автоматично всі chrome-токени переключаються на mono без правки коду.


3. Кольори — 3 рівні + read-only bg

3.1 3 рівні текстового контрасту

theme.ts:

const LIGHT_TEXT_COLORS = {
  text:   '#1a1a1a',  // primary — Mantine c="text"
  muted:  '#4a4a4a',  // НОВИЙ middle-рівень
  dimmed: '#6a6a6a',  // soft — Mantine c="dimmed"
};

const DARK_TEXT_COLORS = {
  text:   '#e8e8e8',
  muted:  '#b0b0b0',
  dimmed: '#8a8a8a',
};

Як використовувати: - <Text c="text"> / c="dimmed" — Mantine props (працюють through CSS vars) - <Text style={{...T.caption, color: C.muted}}> — для middle-рівня (Mantine c= його не знає)

Як змінити: просто редагуй числа. Темну тему dimmed зробити темнішим? '#7a7a7a'. Зберігай — HMR підхопить, увесь додаток оновиться.

3.2 Per-tint конфігурація (light theme)

Кожен тинт (blue/green/yellow/neutral) має свій inputReadonlyBg у palettes:

blue: {
  body: '#e0e8f2',          // фон сторінки
  white: '#f0f4fa',          // editable input bg
  default: '#e8eef6',        // surface (Card/Paper)
  ...
  inputReadonlyBg: '#d8e0eb', // ← READ-ONLY input
},

Read-only фон навмисно темніший за white — щоб юзер бачив "це поле не редагується".

3.3 Як додати ще один рівень тексту (наприклад "subtle" між muted і dimmed)

  1. У theme.ts додай у обидва LIGHT_TEXT_COLORS і DARK_TEXT_COLORS:
    subtle: '#5a5a5a',  // light
    subtle: '#9a9a9a',  // dark
    
  2. У resolver додай у обидва blocks '--eswf-color-text-subtle': ...COLORS.subtle
  3. У typography.ts C registry додай:
    subtle: 'var(--eswf-color-text-subtle)',
    

Готово, можеш писати style={{ color: C.subtle }}.

3.4 Per-tint текст (опційно — продвинутий рівень)

Зараз text/muted/dimmed однакові для всіх 4 тинтів. Якщо захочеш різні: - Перенести LIGHT_TEXT_COLORS поля у TintPalette interface - Заповнити різні значення для blue/green/yellow/neutral - У resolver міняти LIGHT_TEXT_COLORS.textp.text

Цього ще не зроблено (одне значення для всіх вистачає в більшості випадків).


4. Density (щільність) — глобальний multiplier для відстаней

4.1 Як працює

theme.ts:

export const DENSITY_SCALE = {
  compact: 0.70,
  normal: 1.0,
  comfortable: 1.35,
} as const;

Multiplier застосовується до усіх Mantine spacing-токенів (xs/sm/md/lg/xl) через CSS resolver. Юзер обирає в Налаштуваннях → весь UI стискається/розширяється.

4.2 На що впливає (що реагує)

✅ Mantine gap="xs|sm|md|lg|xl" (Group, Stack, SimpleGrid) ✅ Mantine p|px|py|m|mx|my="xs|sm|md|lg|xl" ✅ Висоти input через --mantine-vertical-spacing-* ✅ Будь-яке var(--mantine-spacing-X) у custom CSS

4.3 На що НЕ впливає (поки)

T.<role>.fontSize — фіксовані px (Phase C завдання) ❌ em-padding (слідує за fontSize, не density) ❌ Hardcoded px (іконки size={14}, ROW_HEIGHT virtualizer-а)

Тому ефект density зараз помітний на формах/тулбарах, слабший на списках. Phase Ctodo.md) виправить — multiplier поширюватиметься і на typography. Тоді density-toggle = реальний 1С/Salesforce-style.

4.4 Як змінити "розмах" multiplier'а

Просто переписати числа:

export const DENSITY_SCALE = {
  compact: 0.5,   // -50%, дуже щільно
  normal: 1.0,
  comfortable: 1.6, // +60%, дуже просторо
};

Збережи → HMR оновить.

4.5 Auto-detect

При першому вході виконується detectDensityFromScreen():

≥ 2400px → comfortable  (27" QHD/4K @100%, ultrawide)
1400-2399px → normal    (стандарт desktop/laptop)
< 1400px → compact      (малий 13-14" laptop)

Юзер може override toggle. Кнопка "Визначити для цього екрана" перезапускає детект (наприклад при переключенні на інший монітор).


5. em-spacing — як зробити, щоб відступ слідував за шрифтом

5.1 Принцип

<NavLink
  py="0.4em"                          // 0.4 × локальний fontSize
  style={{ fontSize: T.sectionCardItem.fontSize }}  // встановлює "локальний"
/>

Em-units резолвляться відносно fontSize самого елемента. Якщо встановити fontSize на батька (наприклад Card) — всі em-діти всередині рахуватимуться від нього.

5.2 Конвертація px → em (cheat sheet)

Припускаючи fontSize ≈ 16px:

px em
4px 0.25em
5px 0.3em
6px 0.4em
8px 0.5em
10px 0.65em

LineHeight в ratio (unitless): | px (в context fontSize=16) | ratio | |---|---| | 22px | 1.4 | | 24px | 1.5 | | 26px | 1.6 | | 32px | 2.0 |

5.3 Що вже мігровано на em

  • <ListTable> shared shell — рядки/шапки
  • <MasterDataCustomList> shared shell
  • SubgroupCard у SectionDashboardPage
  • 27 кастомних list-файлів (CELL_SX/HEAD_SX) з власними щільностями

5.4 Як мігрувати ще один компонент

Приклад: SubtableLine з py={6}, gap={4}. Конверсія:

<NavLink
  py="0.4em"           // 6px при fz=15
  gap="0.25em"         // 4px
  style={{ fontSize: T.sectionCardItem.fontSize }}  //  обов'язково для em-context
/>

Без style.fontSize em резолвиться відносно Mantine-дефолту батька — і твоя зміна T.sectionCardItem не вплине.


6. Швидкі сценарії

"Хочу щоб усі шрифти були крупнішими"

BASE_FONT_SIZE = 16 у typography.ts.

"Хочу щоб у темній темі dimmed-текст був яскравішим"

DARK_TEXT_COLORS.dimmed = '#a8a8a8' у theme.ts.

"Хочу щоб шапки списків були bold (зараз як рядки)"

T.listColumnHeader.fontWeight = fontWeight.semibold (або bold) у typography.ts.

"Хочу щоб read-only поля сильніше виділялись"

palettes.<tint>.inputReadonlyBg зробити темнішим у theme.ts.

"Хочу більший розмах density (compact реально дрібний, comfortable реально просторий)"

DENSITY_SCALE.compact = 0.5, comfortable = 1.6 у theme.ts.

"Хочу замінити шрифтову сім'ю в chrome (header/sidebar)"

→ У global.css --eswf-font-ui: 'Roboto Mono', monospace; або у скіні (наприклад terminal.ts): '--eswf-font-ui': "'JetBrains Mono', monospace".

"Хочу кастомний middle-color для тексту (subtle / hint)"

→ Див. § 3.3 — додавання нового рівня в C-registry.


7. Запобіжники — чого НЕ робити

❌ Не додавай color всередину T.<role> токенів — мішатиме форму і значення (один токен → один колір не масштабується) ❌ Не міняй Mantine theme.fontSizes.{xs..xl} напряму — зламає Tooltip/Notification внутрішні стилі ❌ Не використовуй магічні fontSize: NN інлайн у нових компонентах — додавай новий semantic токен у T або переуся існуючий ❌ Не пиши padding: '6px 10px' у нових list-cell стилях — використовуй '0.4em 0.65em' для density-aware behavior


8. Як перевіряти зміни

cd c:/eswf/frontend/erp
npx tsc --noEmit                  # type-check

Vite HMR підхоплює зміни в typography.ts/theme.ts моментально без перезавантаження. Особливо корисно при крутінні чисел — побачиш ефект одразу на всіх сторінках, які відкриті в tabs.


9. Roadmap

  • Phase A — em-spacing на cards/lists (зроблено)
  • Phase B — global density toggle для Mantine spacing (зроблено)
  • 🔮 Phase C — typography density-aware (відкладено, див. § Deferred / Ideas нижче)
  • 🔲 Per-tint text colors (опційно, за бажанням)
  • 🔲 FixedAssetFields.tsx cleanup (unused FORM_FW)

🔮 Deferred / Ideas

Phase C — typography density-aware

Мотивація: density-toggle (compact/normal/comfortable) у Налаштуваннях впливає лише на Mantine --mantine-spacing-* змінні (через CSS resolver, ×0.7/1.0/1.35 multiplier). Шрифти НЕ масштабуються разом — тому ефект density візуально слабкий: на формах помітно, на списках слабо (бо там em-padding слідує за T.listRow.fontSize, який фіксований). Після цього density-toggle = реальний 1С/Salesforce-style "Compact / Normal / Comfortable" — один тумблер тисне ВСЕ. Чому відкладено: поточний ефект density вже корисний (Mantine spacing scaling помітне); типографічна частина — polish, не блокер. Два шляхи реалізації: (A) точний — замінити числові fontSize у токенах T на CSS calc-strings типу 'calc(14px * var(--eswf-density, 1))', em-padding автоматично слідує; (B) грубий — на :root встановити font-size: calc(16px * var(--eswf-density, 1)), всі rem-units реагують, але typography.ts на px-числах все одно треба чіпати. Рекомендований — A. Estimated: ~1 день. Trigger: нічим зайнятись у backlog АБО прямий запит користувача на «зробити density реально помітним на списках».