Перейти к содержимому

ADR-0003: Theme tokens and shared theme state

  • Статус: accepted
  • Дата: 2026-08-07

Публичный рендер playbook живёт на courses.digitable.life/playbook/ рядом с порталом. Тестирование нашло две группы проблем, и обе оказались следствием одного и того же — тема считалась делом каждого приложения по отдельности.

Светлая тема ломалась там, где цвет обходил переменную. Тема задана инверсией переменных Starlight: в светлой --sl-color-white становится почти чёрным, --sl-color-black — белым. Приём штатный, но правило, записавшее цвет литералом, при инверсии остаётся на месте, а всё вокруг него меняется местами. Замер по всем 19 страницам, светлая тема, WCAG 2.1:

Место Было Норма
Текст врезки о публичном рендере 1.33:1 4.5
Заголовок карточки цикла 1.77:1 4.5
Номер шага в карточке 2.30:1 4.5
Подпись в карточке 2.37:1 4.5
Заголовок врезки 2.54:1 4.5
Текст кнопки hero 4.07:1 4.5
code внутри ссылки 4.11:1 4.5

В тёмной теме те же страницы не давали ни одного нарушения: файл писался под неё, и литералы совпадали с её палитрой случайно, а не по замыслу.

Тема не переживала переход между приложениями. Портал хранит выбор в localStorage под ключом digitable:theme, Starlight — под starlight-theme. Домен общий, значит и хранилище общее, но ключи разные. Человек включал тёмную тему на портале, переходил в playbook и попадал в светлую — ту самую, которую доводили меньше. Отсюда и ощущение «багов много где»: читатель оказывался в теме, которую не выбирал.

Цвет в правиле компонента задаётся только токеном. Литералы живут в двух соседних блоках :root и html[data-theme='light'] и больше нигде. У каждого токена --pb-* есть значение в обеих темах, и оба стоят рядом: пропущенная пара видна при чтении файла, а не через полгода на чужом экране.

Контраст подтверждается числом. Норма — WCAG 2.1 AA: 4.5:1 для обычного текста, 3:1 для крупного текста и элементов интерфейса. Значение токена выбирается из расчёта, а не на глаз; расчёт записывается рядом с токеном, если он объясняет выбор.

Тема — одно состояние на весь домен. Ключ портала digitable:theme считается общим. Playbook читает его до первой отрисовки и зеркалит в ключ Starlight, а свой переключатель зеркалит обратно. «Авто» удаляет общий ключ: у портала нет третьего значения, и пустой ключ он читает как системную тему. При расхождении выигрывает ключ портала — каждая запись playbook в него зеркалится, поэтому разойтись они могут только если позже меняли портал.

  • Передавать тему параметром адреса. Ломает кэш и ссылки, оставляет мусор в адресной строке, не работает при заходе по прямой ссылке.
  • Оставить светлую тему как есть и починить найденные места. Чинит симптом: следующая кнопка с литералом сломается так же, и найдут её снова руками.
  • Отказаться от светлой темы. У части читателей она включается системной настройкой; отказ означает, что им достанется худший из двух рендеров.

Положительные:

  • нарушений контраста на 19 страницах в обеих темах и на 390/1024 px — ноль (1441 и 952 замеренных текстовых узла соответственно);
  • переход портал ↔ playbook сохраняет выбор темы в обе стороны;
  • новый компонент не может сломаться прежним способом, не нарушив явное правило файла;
  • у splash-страницы появился переключатель темы на телефоне — раньше его там не было вовсе, потому что Starlight переносит правую группу шапки в мобильное меню, а у splash меню нет.

Trade-offs:

  • акцент светлой темы пришлось затемнить с #167e77 до #12716b: светлее этого он не проходит AA сразу в трёх ролях — ссылка на фоне страницы, ссылка внутри inline-кода и белый текст на заливке акцентом;
  • переопределение компонентов Starlight (ThemeProvider, ThemeSelect, SiteTitle) привязывает playbook к их внутреннему контракту: при мажорном обновлении Starlight переопределения нужно перечитать;
  • на ширине уже 360 px название сайта в шапке обрезается: пять элементов в строку туда не помещаются.

Проверка: на каждой странице в обеих темах контраст любого текста не ниже нормы, а переход портал → playbook → портал сохраняет выбранную тему. Замер делается на живой странице, а не по коду.

Пересмотр — при мажорном обновлении Starlight или при появлении третьей темы.