Империя рекламы — дизайн-система

Роль: Продуктовый дизайнер Период: 2026 — настоящее время Продукт: Сайт агентства, кабинет менеджера и Telegram-бот на одной библиотеке Платформа: Next.js, Tailwind v4, shadcn-компоненты; зеркало в Figma

Сайт, кабинет менеджера и бот «Империи рекламы» — один продукт, который я веду без команды. Систему я собрал в обратном порядке: не макет как источник правды для разработки, а слепок с кода, которым этот код можно проверять.

Сверка нашла то, чего не было видно ни в макете, ни в коде по отдельности: классы тёмной темы в кабинете, которые никогда не срабатывали, три высоты контролов вместо двух, пять «компонентов», оказавшихся сборками из примитивов, и 402 иконки с неправильной толщиной контура. Тёмная тема кабинета и выравнивание действий в строках таблиц появились в продукте именно отсюда.

Всё ниже — живая разметка на тех же переменных, что в проде: 199 переменных, 49 компонентов, 82 иконки, 171 экран: сайт — 82, CRM — 77, бот — 12.

Цветовые токены

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

Значения — из web/app/globals.css, в oklch. Образец слева — тот же цвет, что в проде, а не приближение из палитры Figma.

Semantic tokens — светлая тема

Пятнадцать переменных задают весь светлый интерфейс — сайт, кабинет менеджера и попапы.

ОбразецCSS-переменнаяЗначениеГде используется
--backgroundoklch(0.985 0 0)Фон страницы сайта и кабинета
--foregroundoklch(0.145 0 0)Основной текст и иконки
--cardoklch(1 0 0)Карточки, таблицы, поверхность попапа
--popoveroklch(1 0 0)Выпадающие меню, поповеры, календарь
--primaryoklch(0.546 0.245 262.881)Акцент: кнопки действия, ссылки, выбранный день
--primary-foregroundoklch(1 0 0)Текст и иконки на акцентной заливке
--secondaryoklch(0.97 0 0)Вторичная кнопка, бейдж «Вы»
--mutedoklch(0.97 0 0)Шапка таблицы, боковое меню, наведение у ghost
--muted-foregroundoklch(0.556 0 0)Подписи, второстепенный текст, плейсхолдеры
--accentoklch(0.97 0 0)Подсветка пункта меню под курсором и с клавиатуры
--borderoklch(0.922 0 0)Рамки карточек, разделители строк
--inputoklch(0.922 0 0)Рамка полей ввода и чекбоксов
--ringoklch(0.708 0 0)Кольцо фокуса с клавиатуры (через ring/50)
--sidebaroklch(0.985 0 0)Подложка бокового меню кабинета
--blob-mid · --blob-dark · --blob-lightпроизводные от --primaryГрадиентное пятно в хиро главной — считается от акцента, а не задаётся отдельно

Semantic tokens — тёмная тема

Тот же список ролей, другие значения. Акцент не меняется: синий — цвет марки, и менять его по темам значило бы, что у продукта две разные марки. У решения есть цена: на белом синий даёт контраст 5,3:1, на тёмном фоне — 3,8:1. Кнопкам и крупным элементам этого хватает, мелкому тексту ссылок в тёмной теме — нет.

ОбразецCSS-переменнаяЗначениеОтличие от светлой
--backgroundoklch(0.145 0 0)Почти чёрный вместо почти белого
--foregroundoklch(0.985 0 0)Инверсия пары фон/текст
--cardoklch(0.205 0 0)Карточка светлее фона, а не темнее
--mutedoklch(0.269 0 0)Шапки и подложки поднимаются ещё на ступень
--muted-foregroundoklch(0.708 0 0)Светлее, чтобы держать контраст на тёмном
--borderoklch(1 0 0 / 10%)Не серый, а полупрозрачный белый — рамка работает на любой подложке
--inputoklch(1 0 0 / 15%)То же для полей ввода
--primaryоклch без измененийЕдинственный токен, который в обеих темах одинаковый

Статусы

Три роли, которые не сводятся к акценту: получилось, требует внимания, опасное действие. До системы это были литералы green на сайте и emerald в кабинете — один и тот же смысл двумя разными цветами.

ОбразецПеременнаяСветлая / тёмнаяГде используется
--successgreen-600 / green-400«Открыто сейчас» на сайте, «Есть», «Оплачена» в кабинете
--warningamber-800 / amber-300Текст бейджа «не оплачено»
--warning-mutedamber-100 / amber-950 40%Строка неразобранного клиента при наведении
--warning-subtleamber-50 / amber-950 20%Строка неразобранного клиента в покое
--destructivered-600 / red-400Удаление, ошибки валидации, кнопка в подтверждении

Как это выглядит в интерфейсе

Светлая
Оплачена Не оплачено Ошибка Вы Тег
Тёмная
Оплачена Не оплачено Ошибка Вы Тег

Типографика

Один шрифт — Inter — и шкала Tailwind без собственных размеров. Своя шкала здесь была бы лишней сущностью: каждый новый кегль пришлось бы заводить и в коде, и в макете.

Ниже — те ступени, которые реально встречаются в продукте, и роль каждой. Всё, чего в списке нет, в интерфейсе не используется.

text-5xl · 48/1.1 · 700Заголовок страницы
text-3xl · 30/1.2 · 600Заголовок секции
text-xl · 20/1.4 · 500Заголовок карточки
text-base · 16/1.5 · 400Основной текст страницы услуги
text-sm · 14/1.45 · 400Текст интерфейса: поля, таблицы, кнопки
text-xs · 12/1.4 · 400Подписи, метаданные, подсказки
text-[10px] · 500Группа в боковом меню
СтупеньНачертаниеГде используется
text-5xl / text-4xlBold, трекинг −0.02emЗаголовок главной и страницы услуги; на 390 падает до text-3xl
text-3xl / text-2xlSemi BoldЗаголовки секций сайта, заголовок статьи
text-xlMediumЗаголовок карточки направления и карточки работы
text-baseRegularТекст страниц сайта и статьи блога
text-smRegular / MediumВесь кабинет менеджера: таблицы, формы, кнопки
text-xsRegularПодписи под значениями, источник клиента, счётчики

Сетка, отступы, радиусы

Сайт и кабинет живут по разным законам: у сайта — контейнер с полями, которые растут вместе с экраном, у кабинета — фиксированная колонка меню и таблица во всю оставшуюся ширину.

Контейнер сайта вынесен в одну утилиту site-container: до неё те же четыре класса были повторены в 21 секции, и правка полей требовала обхода всех страниц.

ПараметрЗначениеКомментарий
site-containermax-w-6xl, от 2xl — 96remЕдинственный контейнер публичных страниц
Поля контейнера16 → 24 (sm) → 32 (lg) pxРастут ступенями, а не процентом ширины
Брейкпоинты в макете390 и 1440Две ширины вместо трёх: 768 не заводили сознательно
Боковое меню кабинета210 px, подложка mutedНа 390 уезжает в шторку
--radius0.625rem = 10 pxБазовый радиус; остальные считаются от него
Шкала радиусовsm 6 · md 8 · lg 10 · xl 14 · 2xl 18Кнопка — lg, карточка — xl, бейдж — полный круг

Отступы и радиусы вживую

1 · 4px 2 · 8px 3 · 12px 4 · 16px 6 · 24px 8 · 32px 12 · 48px 16 · 64px
sm · 6px md · 8px lg · 10px xl · 14px 2xl · 18px full

Плотность и тач-таргеты

Один и тот же компонент обязан быть компактным под мышью и крупным под пальцем. Держать для этого две версии компонента — значит однажды поправить одну и забыть вторую.

В коде это вариант touch-target: на сенсорном экране высота поднимается до 44 px, с мышью остаётся исходной. Кабинет менеджера из правила исключён атрибутом data-compact-ui — это внутренний инструмент, за компьютером, и там крупные кнопки только съедают строки таблицы.

Мышь — 32 px
Палец — 44 px
Кабинет — 36 px, тач-правило выключено
ЗонаВысотаПочему так
Публичный сайт, мышь32 pxКомпактно, кнопка не доминирует над текстом
Публичный сайт, сенсор44 pxМинимум тач-таргета iOS HIG; включается атрибутом на <html>
Кабинет менеджера36 pxЕдиная высота шапки, тулбаров и форм
Действия внутри строки28 pxЕдинственное исключение: строка должна оставаться узкой

Компоненты

Компонентом становится то, что в коде лежит отдельным файлом. Секции страниц, формы и попапы собираются из примитивов прямо на экране — иначе библиотека начинает повторять вёрстку, а не описывать её.

Демонстрации ниже — не картинки из Figma, а разметка с теми же токенами, что в продукте. Каждая пара показывает светлую и тёмную тему: одни и те же классы, разные значения переменных.

Разбор карточки ContentCard: слева примитивы, в центре собранный компонент с панелью свойств, справа варианты и экземпляры
Внутри карточки нет ничего своего: иконка, Badge, Button и текстовые стили — отдельные примитивы, которые живут и вне неё.

Примитивы

Button components/ui/button.tsx

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

Размеры

Светлая
xs · 24 sm · 28 default · 32 lg · 36 icon · 32 icon-sm · 28
Тёмная
xs · 24 sm · 28 default · 32 lg · 36 icon · 32 icon-sm · 28

Варианты

Светлая
default outline secondary ghost destructive link nav · active toggle · pressed chip search
Тёмная
default outline secondary ghost destructive link nav · active toggle · pressed chip search
ВариантЗаливка и рамкаГде используется
default--primary, рамка прозрачнаяГлавное действие: «Написать в Telegram», «Добавить клиента»
outline--background + рамка --borderВторостепенные действия, фильтры, экспорт
secondary--secondaryОтмена в попапах
ghostпрозрачная, при наведении --mutedИконки действий в строке таблицы
destructive--destructive 10%, текст --destructiveУдаление; сплошную заливку красным приберегли для подтверждения
linkбез фона, текст --primaryСсылки внутри текста; наведение — сменой цвета, не подчёркиванием
navнаведение красит в --backgroundБоковое меню кабинета: сам сайдбар уже muted, ghost на нём не читался
togglearia-pressed заливает акцентом целикомЛайк и дизлайк: включённое состояние раньше читалось блёкло
chip--card + тень, без рамкиВыбранный фильтр на серой панели
search--foreground 3% → 8%Триггер поиска в шапке: фон от foreground темнит в светлой теме и светлит в тёмной

Состояния

default hover · 80% active · сдвиг на 1px focus-visible · ring 3 disabled · 50%
СостояниеЧто меняетсяПочему так
hoverзаливка до 80%Одинаково работает на всех вариантах
activetranslate-y 1pxФизическое «нажатие»; у кнопок с выпадающим меню отключено
focus-visiblering 3px, --ring 50%Кольцо — тень, а не рамка: не входит в размер и не двигает соседей
aria-expandedкак hoverНа сенсорном экране hover не существует — без этого открытый дропдаун не давал обратной связи
disabledopacity 50%Клики не проходят
aria-invalidрамка и кольцо --destructiveОшибка валидации в формах кабинета

Badge components/ui/badge.tsx

Метка состояния или тега. Никогда не кликабельна — для кликов есть chip-вариант кнопки.

Светлая
НовыйВыНаружная рекламаОплаченаЕщё не закрыта
Тёмная
НовыйВыНаружная рекламаОплаченаЕщё не закрыта

Input, Label, Textarea components/ui/input.tsx · label.tsx · textarea.tsx

Поле формы высотой 36 px — та же высота, что у кнопки сохранения рядом. Ошибка живёт не в рамке, а в подписи под полем: рамка только помечает, текст объясняет.

Светлая
Имя клиентаКак обращаться в переписке EmailПохоже, адрес неполный Комментарий
Тёмная
Имя клиентаКак обращаться в переписке EmailФокус с клавиатуры Комментарий

Checkbox components/ui/checkbox.tsx

Используется в настройке колонок таблицы и в согласиях. Размер 16 px, но область нажатия — вся строка с подписью.

Светлая
Организация Источник Подписка
Тёмная
Организация Источник Подписка

Tabs components/ui/tabs.tsx

Переключение подмножества одного списка: «Все» и «Мои» у клиентов, статусы у сделок. Не навигация — раздел при этом не меняется.

Светлая
Тёмная

Table components/ui/table.tsx

Строка, ячейка и заголовок — три отдельных примитива. Строкой целиком компонент не стал: свойства вложенных экземпляров в Figma нельзя пробросить наружу, и переключение колонок ломалось бы.

ИмяОрганизацияМенеджерПДнДействия
Иван ПетровКофейня «Пар»Мария ВыЕсть
Анна СмирноваООО «Север»Не назначенНет
Сергей ВолковАвтосервис «Гараж»ПётрЕсть
ЭлементЗначениеРешение
Шапка--muted, текст --muted-foregroundШапка не должна конкурировать с данными
Ячейкаpadding 12px, 13/1.4Одна высота строки во всех таблицах кабинета
Свободный клиент--warning-subtle → --warning-mutedПодсветка строки целиком: неразобранного видно периферийным зрением
Колонка действийвыравнивание вправоПравка в код: раньше действия прижимались к центру пустой колонки

Класс строки меню вынесен в menuItemClassName и переиспользуется в подборщиках клиента, подрядчика и тегов — до этого одна и та же строка была сверстана в пяти местах по-своему.

Светлая
Открыть карточку
Написать в Telegram
Удалить
Тёмная
Открыть карточку
Написать в Telegram
Удалить

Notice components/ui/notice.tsx

Плашка-сообщение внутри страницы: объясняет состояние, а не требует действия. Всплывающих тостов в продукте нет — сообщение остаётся на экране, пока причина не исчезла.

Светлая
Клиентов пока нет — карточка появится здесь, как только клиент напишет боту и примет согласие на обработку ПДн.
Клиент не закреплён ни за кем: пока его не взяли, он не в работе.
Не удалось сохранить: проверьте обязательные поля.
Тёмная
Клиентов пока нет — карточка появится здесь, как только клиент напишет боту и примет согласие на обработку ПДн.
Клиент не закреплён ни за кем: пока его не взяли, он не в работе.
Не удалось сохранить: проверьте обязательные поля.

Dialog components/ui/dialog.tsx

Одна форма на один заход: заполнил, сохранил, вернулся. Попапы не заводятся отдельными компонентами — каждый собирается из поля, кнопки и этой поверхности.

Светлая
Удалить шаблон? Запись уедет в корзину. Восстановить её оттуда сможет любой сотрудник, стереть насовсем — только администратор.
Тёмная
Удалить шаблон? Запись уедет в корзину. Восстановить её оттуда сможет любой сотрудник, стереть насовсем — только администратор.

Skeleton components/ui/skeleton.tsx

Заглушка загрузки повторяет геометрию того, что появится: не серый прямоугольник вообще, а конкретные строки таблицы.

Доменные компоненты

Их немного и это осознанно: компонентом становится то, что повторяется в разных местах продукта, а не каждая секция страницы.

ContentCard components/content-card.tsx

Одна карточка на работу из портфолио и на статью блога. Разница между ними — вариант, а не второй компонент: у статьи дата прижата к низу.

Светлая
Вывеска для кофейни «Пар»ВывескиМонтаж
Как выбрать вывеску для фасада12 сентября · 6 мин
Тёмная
Вывеска для кофейни «Пар»ВывескиМонтаж
Как выбрать вывеску для фасада12 сентября · 6 мин

DirectionCard components/direction-card.tsx

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

Светлая
Наружная реклама Наружка, которую видно с дороги и с тротуара: вывески, буквы, крышные установки. ВывескиСветовые буквыКрышные установки
Тёмная
Наружная реклама Наружка, которую видно с дороги и с тротуара: вывески, буквы, крышные установки. ВывескиСветовые буквыКрышные установки

ContactCta components/contact-cta.tsx

Единственная точка заявки на сайте: формы захвата нет вообще. Кнопка уводит в бота диплинком, который несёт услугу и место перехода; запасной выход в браузере — кликабельный телефон.

Светлая
Напишите нам и рассчитайте стоимость наружной рекламы за 15 минут Или позвоните нам (4)
Тёмная
Напишите нам и рассчитайте стоимость наружной рекламы за 15 минут Или позвоните нам (4)

Боковое меню кабинета с группами: CRM, контент, администрирование. Группы не украшение — у дизайнера половина пунктов просто не появляется.

Светлая
CRM Клиенты Сделки Задачи Контент Портфолио Полезные статьи
Тёмная
CRM Клиенты Сделки Задачи Контент Портфолио Полезные статьи

WebAppBottomNav components/web-app-bottom-nav.tsx

Внутри Telegram и Max шапка занята мессенджером, поэтому разделы уезжают вниз — в зону большого пальца. В браузере этой навигации нет: там её роль играет попап поиска.

Светлая
Главная Портфолио Услуги Контакты
Тёмная
Главная Портфолио Услуги Контакты

Иконки

Один набор — lucide, 82 иконки. Толщина контура не подбирается на глаз, а считается: 2 × размер / 24. У 16 px это 1,33, у 14 — 1,17, у 12 — 1.

Проверка скриптом нашла 402 иконки с неправильной толщиной. И сразу же — обратный случай: у карточек направлений в коде стоит strokeWidth={1.5}, и 289 «починенных» иконок пришлось вернуть. Правило без исключения сломало бы то, что сделано осознанно.

24 px · 2,0 16 px · 1,33 14 px · 1,17 12 px · 1,0 Исключение: 1,5 у карточек направлений

Figma ↔ код

В Figma та же система разложена на восемь коллекций переменных. Тема и плотность — режимы коллекции, а не оси вариантов: иначе матрица кнопки удвоилась бы дважды.

199 переменных, 49 компонентов и 82 иконки; 21 набор с вариантами даёт 483 варианта — из них 361 у одной кнопки, потому что вариант × размер × состояние перемножаются.

КоллекцияРежимыПеременныхЧто в ней
Primitives—22Сырые значения, на которые ссылается семантика
ThemeLight / Dark31Роли: фон, текст, акцент, статусы
ComponentLight / Dark46Точечные исключения, включая цвета с запечённой прозрачностью
Radius—10Шкала скруглений от --radius
Spacing—29Отступы и зазоры
DensityFine / Touch12Высоты контролов под мышь и под палец
Typography—27Кегли, межстрочные, начертания
BreakpointMobile / Desktop22Размеры и видимость, зависящие от ширины

Что в Figma сделано иначе, чем в коде

МестоКак в кодеКак в макете и почему
Прозрачные слоиbg-foreground/5Отдельный токен с запечённой прозрачностью: привязанная к переменной заливка игнорирует прозрачность слоя
Кольцо фокусаring-3 ring-ring/50Тень с разбросом 3: не входит в размер и не двигает соседей
Обрезка содержимогоoverflow-hiddenВключена только там, где класс есть в коде: иначе срезает тени
Строка таблицыкомпонент TableRowВ макете компонентом не стала: свойства вложенных экземпляров нельзя пробросить наружу
Маркеры тем форумаэмодзи ❗ и 🔵В макете — глифы Inter: Figma не рендерит цветные эмодзи ни в экспорте, ни в превью