Перейти к содержанию

Цвета, размеры и семантические токены

Мастерская уже содержит первую кнопку и историю её использования. Теперь выделим токены оформления «Набора»: именованные значения, которыми пользуются компоненты. Токен помогает выразить роль цвета или интервала и изменить её согласованно. Мы будем работать с обычными CSS custom properties, чтобы читатель видел механизм без отдельного сервиса хранения дизайна и сложного генератора.

Набор повторяющихся шестнадцатеричных цветов ещё не является системой. Если одна и та же фиолетовая строка встречается в двадцати селекторах, при изменении бренда автору надо выяснять назначение каждого использования. Семантическое имя --color-primary сообщает роль основного действия. Оно позволяет менять значение, сохраняя назначение. Позднее светлая и тёмная темы получат разные значения одной роли.

Роль значения и его форма

В src/styles.css значения объявлены внутри .nabor-theme. Эта область является корнем оформления компонента, а не глобальным селектором всех страниц приложения. Класс устанавливается декоратором Storybook вокруг примера. Компоненты наследуют переменные от ближайшей области. Такое решение позволит библиотеке сосуществовать с другими стилями клиента и ограничить действие темы выбранной частью интерфейса.

Полный файл src/styles.css для этого состояния:

.nabor-theme {
  --color-surface: #ffffff;
  --color-text: #20242c;
  --color-muted: #475569;
  --color-primary: #6335b7;
  --color-on-primary: #ffffff;
  --color-danger: #ad1833;
  --color-line: #8993a5;
  --space-small: .5rem;
  --space-regular: 1rem;
  --radius-control: .5rem;
  color: var(--color-text);
  background: var(--color-surface);
  font-family: system-ui, sans-serif;
}
.nabor-button { font: inherit; padding: .65rem 1rem; border: 1px solid transparent; border-radius: var(--radius-control); cursor: pointer; }
.nabor-button--primary { color: var(--color-on-primary); background: var(--color-primary); }
.nabor-button--secondary { color: var(--color-text); background: var(--color-surface); border-color: var(--color-line); }
.nabor-button--danger { color: #fff; background: var(--color-danger); }
.nabor-button--compact { padding: .4rem .65rem; }
.nabor-button:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 3px; }
.nabor-button:disabled { opacity: .65; cursor: default; }

Полный файл src/Tokens.stories.tsx для этого состояния:

import type { Meta, StoryObj } from '@storybook/react-vite';
const meta = { title: 'Nabor/Tokens' } satisfies Meta;
export default meta;
export const Semantic: StoryObj = { render: () => <div><p style={{color:'var(--color-primary)'}}>Основное действие</p><p style={{color:'var(--color-danger)'}}>Ошибка</p><p style={{color:'var(--color-muted)'}}>Пояснение</p></div> };

--color-surface обозначает фон поверхности, --color-text — основной текст, --color-muted — пояснение. --color-primary и --color-on-primary образуют пару: фон главного действия и текст на нём. Нельзя автоматически использовать обычный --color-text поверх любого акцентного цвета, потому что контраст зависит от пары. Семантические токены должны описывать такие отношения, а не только перечислять приятные оттенки.

Для интервалов оставлены два значения, для радиуса — один токен управления. Эта библиотека пока мала, поэтому десятки ступеней шкалы только увеличили бы выбор без полезного контекста. Когда появится реальная потребность в плотной форме или крупной панели, набор можно расширить. Но сначала полезно увидеть, какие значения действительно повторяются и как они влияют на читаемость связанного интерфейса.

Наследование CSS-переменных

Кнопка обращается к var(--color-primary) и var(--radius-control). Браузер подставляет значение из каскада и наследования. Если компонент находится вне .nabor-theme, необходимые переменные могут отсутствовать. Поэтому область темы является частью договора использования. В будущем пакете потребитель должен подключить CSS и установить корневой класс, а не только импортировать React-функцию. Без этого разметка останется, но оформление будет неполным.

Можно добавить fallback во вторую часть var, однако он не всегда правильное решение. Постоянный запасной цвет способен скрыть забытый корень темы: кнопка выглядит приемлемо, но поле и сообщения уже используют иные значения. Для учебной библиотеки лучше явно документировать область наследования. Fallback подходит там, где отсутствие токена действительно является допустимым состоянием. Не превращайте его в способ замаскировать нарушение общего контракта.

Переименование токена также имеет последствия. Если часть потребителей переопределяет --color-primary, удаление этого имени сломает их настройку без TypeScript-ошибки. CSS-переменные входят в публичный интерфейс оформления, когда библиотека разрешает их использовать. Поэтому при выпуске нужно описывать не только React-пропсы, но и поддерживаемые токены. Изменение внутреннего значения и удаление публичного имени являются разными видами совместимости.

История семантической палитры

В Tokens.stories.tsx три короткие строки используют роли основного действия, ошибки и пояснения. Это не полный каталог цветов, а наглядное сопоставление значения с задачей. История живёт внутри того же декоратора, что кнопка. Если позднее выбрать тёмную тему, пример покажет новые значения без изменения кода самой истории. Тем самым мы проверяем архитектурное направление: содержимое знает роли, тема назначает конкретные цвета.

Не делайте весь текст ошибки красным лишь ради визуальной заметности. Значение --color-danger помогает оформлению, но смысл ошибки должен оставаться в словах и разметке. Пользователь, различающий цвета иначе, должен понимать проблему из сообщения. Этот принцип особенно важен в дальнейшем поле ввода: текст пояснения, aria-invalid и связь сообщения с полем дополнят палитру. Токен не может заменить семантический контракт элемента.

Интервалы в rem связаны с корневым размером шрифта. Это удобная основа для изменения масштаба текста, но не автоматическая гарантия адаптивности. Компонент способен иметь жёсткую ширину и переполниться независимо от единицы отступа. Поэтому на поздних историях мы отдельно рассмотрим длинные подписи и ограниченную область. Размерные токены лишь делают общие решения видимыми и согласованными.

Изменение одного акцента

Представим смену основного цвета. Если заменить значение --color-primary внутри корня, все потребляющие его элементы получат новый фон или контур. При этом нужно отдельно проверить пару --color-on-primary: прежний белый текст не обязательно подходит новому светлому фону. Набор двух токенов помогает назвать задачу, однако выбранные числа всё равно требуют проверки реального контраста. В этой работе измерения доступности не выполнялись.

Другая ситуация: дизайнер хочет отличить удаление от сохранения. Не надо вводить один токен на каждую отдельную кнопку страницы. Семантическая роль опасного действия допускает --color-danger; отдельные действия используют её через вариант компонента. Такое распределение уменьшает количество случайных имен и даёт потребителю понятный словарь. Следующий урок введёт эти варианты в контракт Button, сохранив токены как механизм оформления.

К концу урока цвета, интервалы и радиус перестали быть набором повторяющихся литералов. Они получили область действия и семантические имена. При чтении будущего компонента можно спросить, какую роль он использует, а при настройке темы — какие значения назначены этой роли. Это связывает код, дизайн и документацию одним небольшим договором, который будет расширяться вместе с настоящими потребностями «Набора».

Токен и локальное исключение

Если одна история требует большой внешней рамки для объяснения, её можно задать на демонстрационном контейнере. Не обязательно добавлять новый системный токен для единственного учебного выделения. Токен становится полезным, когда выражает повторяемую роль и поддерживаемый договор. Иначе каталог имён превращается в историю случайных макетов, в которой потребитель не понимает правильный выбор.

Напротив, повторение одного и того же значения во всех состояниях поля является поводом найти общую роль. Например, контур нейтрального элемента можно связать с уже существующим --color-line. Важно не объединять значения лишь потому, что числа сейчас совпадают: разные роли могут разойтись при смене темы. Сначала объясните назначение, затем решайте, общий ли это токен. Так система сохранит смысл и после редизайна.

Оглавление серии, общая подборка и полный архив исходников. Для этого урока выберите lesson-02.

Первичные источники: Storybook CSF, React Vite integration, Аргументы историй, Темы и globals, Декораторы, Accessibility addon, Автоматическая документация, React useId, HTML dialog.

Редакционный статус: локальный черновик. Учебные команды, приложение и проверки не запускались; описанные результаты ожидаемые. Профиль версий закреплён, разрешённый lockfile не создавался.