ADR-0003: Theme tokens and shared theme state
- Статус: accepted
- Дата: 2026-08-07
Context
Заголовок раздела «Context»Публичный рендер 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 и попадал в светлую — ту самую, которую
доводили меньше. Отсюда и ощущение «багов много где»: читатель оказывался в
теме, которую не выбирал.
Decision
Заголовок раздела «Decision»Цвет в правиле компонента задаётся только токеном. Литералы живут в двух
соседних блоках :root и html[data-theme='light'] и больше нигде. У каждого
токена --pb-* есть значение в обеих темах, и оба стоят рядом: пропущенная
пара видна при чтении файла, а не через полгода на чужом экране.
Контраст подтверждается числом. Норма — WCAG 2.1 AA: 4.5:1 для обычного текста, 3:1 для крупного текста и элементов интерфейса. Значение токена выбирается из расчёта, а не на глаз; расчёт записывается рядом с токеном, если он объясняет выбор.
Тема — одно состояние на весь домен. Ключ портала digitable:theme
считается общим. Playbook читает его до первой отрисовки и зеркалит в ключ
Starlight, а свой переключатель зеркалит обратно. «Авто» удаляет общий ключ:
у портала нет третьего значения, и пустой ключ он читает как системную тему.
При расхождении выигрывает ключ портала — каждая запись playbook в него
зеркалится, поэтому разойтись они могут только если позже меняли портал.
Alternatives
Заголовок раздела «Alternatives»- Передавать тему параметром адреса. Ломает кэш и ссылки, оставляет мусор в адресной строке, не работает при заходе по прямой ссылке.
- Оставить светлую тему как есть и починить найденные места. Чинит симптом: следующая кнопка с литералом сломается так же, и найдут её снова руками.
- Отказаться от светлой темы. У части читателей она включается системной настройкой; отказ означает, что им достанется худший из двух рендеров.
Consequences
Заголовок раздела «Consequences»Положительные:
- нарушений контраста на 19 страницах в обеих темах и на 390/1024 px — ноль (1441 и 952 замеренных текстовых узла соответственно);
- переход портал ↔ playbook сохраняет выбор темы в обе стороны;
- новый компонент не может сломаться прежним способом, не нарушив явное правило файла;
- у splash-страницы появился переключатель темы на телефоне — раньше его там не было вовсе, потому что Starlight переносит правую группу шапки в мобильное меню, а у splash меню нет.
Trade-offs:
- акцент светлой темы пришлось затемнить с
#167e77до#12716b: светлее этого он не проходит AA сразу в трёх ролях — ссылка на фоне страницы, ссылка внутри inline-кода и белый текст на заливке акцентом; - переопределение компонентов Starlight (
ThemeProvider,ThemeSelect,SiteTitle) привязывает playbook к их внутреннему контракту: при мажорном обновлении Starlight переопределения нужно перечитать; - на ширине уже 360 px название сайта в шапке обрезается: пять элементов в строку туда не помещаются.
Verification or review date
Заголовок раздела «Verification or review date»Проверка: на каждой странице в обеих темах контраст любого текста не ниже нормы, а переход портал → playbook → портал сохраняет выбранную тему. Замер делается на живой странице, а не по коду.
Пересмотр — при мажорном обновлении Starlight или при появлении третьей темы.